目录
[1. 什么是单链表?](#1. 什么是单链表?)
[2. 单链表的代码表示](#2. 单链表的代码表示)
[3. 单链表的基本操作](#3. 单链表的基本操作)
[3.1 初始化链表](#3.1 初始化链表)
[3.2 插入结点(头插法)](#3.2 插入结点(头插法))
[3.3 插入结点(尾插法)](#3.3 插入结点(尾插法))
[3.4 遍历链表](#3.4 遍历链表)
[4. 单链表的优缺点](#4. 单链表的优缺点)
代码:*L=(LinkList)malloc(sizeof(LNode))
[1. malloc 的作用](#1. malloc 的作用)
[2. sizeof(LNode) 的作用](#2. sizeof(LNode) 的作用)
[3. 类型转换 (LinkList)](#3. 类型转换 (LinkList))
[4. *L 的含义](#4. *L 的含义)
[5. 整体流程](#5. 整体流程)
[6. 实际效果](#6. 实际效果)
[7. 常见问题解答](#7. 常见问题解答)
[Q1:为什么用 malloc 而不是直接声明变量?](#Q1:为什么用 malloc 而不是直接声明变量?)
[Q2:头结点的 data 字段有意义吗?](#Q2:头结点的 data 字段有意义吗?)
[Q3:为什么要用二级指针 LinkList* L?](#Q3:为什么要用二级指针 LinkList* L?)
[8. 图解过程](#8. 图解过程)
[DestoryLinkList 函数原理详解](#DestoryLinkList 函数原理详解)
[1. 函数参数 LinkList* L(二级指针)](#1. 函数参数 LinkList* L(二级指针))
[2. while (*L != NULL) 循环](#2. while (*L != NULL) 循环)
[3. 销毁过程图解](#3. 销毁过程图解)
[4. 最终效果](#4. 最终效果)
[5. 为什么需要这样实现?](#5. 为什么需要这样实现?)
[6. 对比 ClearLinkList(清空链表)](#6. 对比 ClearLinkList(清空链表))
[7. 常见问题](#7. 常见问题)
[Q1:为什么用 while (*L) 而不是 while ((*L)->next)?](#Q1:为什么用 while (*L) 而不是 while ((*L)->next)?)
[8. 代码验证](#8. 代码验证)
cpp
#include <stdio.h>
#include <stdlib.h>
//函数结果状态代码
#define OK 1
#define ERROR 0
typedef int Status;//函数返回状态,ok,error
typedef int Elemtype;//链表元素为整形
typedef struct Lnode//定义结构体
{
Elemtype data;//数据域
struct Lnode* next;//指针域
}Lnode,*LinkList;//单个结点,整个链表(指向结点的指针)
//初始化链表(建立一个头结点)
Status InitLinkList(LinkList* L){
*L=(LinkList)malloc(sizeof(Lnode));//分配头结点内存
if(*L==NULL){
return ERROR;//判断是否分配成功
}
(*L)->next=NULL;//头结点的指针域为空
return OK;
}
//判断链表是否为空
Status IsEmptyLinkList(const LinkList* L){
if((*L)->next==NULL){
return ERROR;
}else{
return OK;
}
}
//销毁链表
Status DestoryLinkList(LinkList* L){
LinkList p;//定义一个临时的指向结点的指针
while (*L!=NULL)
{
p=*L;//储存原来的指针(结点)
*L=(*L)->next;//往后移动结点
free(p);//释放原来的指针
}
return OK;
}
//链表的插入,头插法
Status CreateLinkList_h(LinkList* L,int n){
InitLinkList(L);//创建头结点
for(int i=0;i<n;i++){
LinkList newlnode;//创建一个新结点
newlnode=(Lnode*)malloc(sizeof(Lnode));//为新节点分配内存
if(newlnode==NULL){
return ERROR;//判断是否分配成功
}
printf("请输入数据:\n");
scanf("%d",&newlnode->data);
newlnode->next=(*L)->next;//使新结点指向原指针
(*L)->next=newlnode;//使头指针指向新结点
}
return OK;
}
//链表的插入,尾插入
Status CreateLinkList_r(LinkList* L,int n){
InitLinkList(L);//创建头结点
LinkList p=*L;//定义临时尾结点
for(int i=0;i<n;i++){
LinkList newlnode;
newlnode=(Lnode*)malloc(sizeof(Lnode));//给新结点分配内存
if(newlnode==NULL){
return ERROR;//判断是否分配成功
}
printf("请输入数据:\n");
scanf("%d",&newlnode->data);
newlnode->next=NULL;//使新结点指向空
p->next=newlnode;//使原结点指向新结点
p=p->next;//后移一次,定义新的尾结点
}
return OK;
}
//查看链表
Status ShowLinkList(const LinkList* L){
Lnode* p=(*L)->next;//定义个临时结点
if(p==NULL){
printf("链表为空!\n");
return OK;
}
int i=1;
while (p!=NULL)
{
printf("%d : %d\n", i, p->data); // 打印序号和数据
i++; // 序号递增
p = p->next; // p 移动到下一个结点
}
return OK;
}
//查看第i个元素
Status LocatElem(const LinkList* L,int i){
int j=i;//赋值给j
i=1;//初始化i
LinkList p=(*L)->next;//创建临时结点表示第一个结点
if(p==NULL){
printf("链表为空!\n");//判断链表是否为空
return OK;
}
//逐步后移,直到i和j相等
while (i!=j)
{
i++;
p=p->next;
}
printf("第%d个 : %d\n", j, p->data); // 打印第i个序号,和数据
return OK;
}
//主函数
int main(){
LinkList mylist;
mylist=NULL;
//CreateLinkList_h(&mylist,3);//头插
CreateLinkList_r(&mylist,3);//尾插
ShowLinkList(&mylist);
LocatElem(&mylist,2);
}
下面来解释相关知识点和部分代码:
单链表是数据结构中最基础的一种链式存储结构,非常适合新手学习指针和动态内存管理的概念。下面我会用最易懂的方式讲解单链表的核心知识。
1. 什么是单链表?
单链表就像一列火车:
每节车厢(结点)包含两部分:货物(数据)和连接钩(指针)
车头(头结点)不装货物,只负责带领整列火车
最后一节车厢的连接钩是空的(NULL)
2. 单链表的代码表示
typedef struct Node { int data; // 数据域(可以是任意类型) struct Node* next; // 指针域(指向下一个结点) } Node, *LinkedList; // Node表示单个结点,LinkedList表示整个链表
3. 单链表的基本操作
3.1 初始化链表
LinkedList initList() { LinkedList L = (LinkedList)malloc(sizeof(Node)); // 创建头结点 L->next = NULL; // 初始为空链表 return L; }
3.2 插入结点(头插法)
void headInsert(LinkedList L, int data) { Node* newNode = (Node*)malloc(sizeof(Node)); // 创建新结点 newNode->data = data; newNode->next = L->next; // 新结点指向原第一个结点 L->next = newNode; // 头结点指向新结点 }
特点:新结点总是插在最前面,链表顺序与插入顺序相反
3.3 插入结点(尾插法)
void tailInsert(LinkedList L, int data) { Node* p = L; while(p->next != NULL) { // 找到最后一个结点 p = p->next; } Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; newNode->next = NULL; p->next = newNode; // 最后一个结点指向新结点 }
特点:新结点总是插在最后面,链表顺序与插入顺序相同
3.4 遍历链表
void printList(LinkedList L) { Node* p = L->next; // 跳过头结点 while(p != NULL) { printf("%d ", p->data); p = p->next; // 移动到下一个结点 } printf("\n"); }
4. 单链表的优缺点
优点:
插入/删除速度快(O(1)时间复杂度)
不需要预先知道数据规模
不需要连续的内存空间
缺点:
查找速度慢(O(n)时间复杂度)
需要额外的空间存储指针
下面来解释代码,更加了解单链表:
代码:*L=(LinkList)malloc(sizeof(LNode))
这行代码是单链表初始化中的核心操作,涉及指针、动态内存分配和类型转换。让我们用新手能理解的方式逐步拆解:
1.
malloc
的作用
malloc
是内存分配函数(memory allocation),它的作用是从堆(Heap)内存中申请一块指定大小的内存空间。
比喻:就像在仓库里预订一个储物柜,告诉管理员你需要多大的空间。
语法 :
void* malloc(size_t size);
参数
size
:需要申请的字节数返回值:成功时返回指向分配内存的指针,失败返回
NULL
2.
sizeof(LNode)
的作用
sizeof
是一个运算符,用来计算数据类型或变量占用的字节数。
sizeof(LNode)
:计算结构体LNode
的大小。
假设
LNode
包含一个int
(4字节)和一个指针(8字节,64位系统),则sizeof(LNode) = 12
字节。意义 :告诉
malloc
要申请一个足够存放LNode
的内存块。
3. 类型转换
(LinkList)
malloc
返回的是void*
(通用指针),需要强制转换为LinkList
类型。
LinkList
是什么 ?根据代码中的定义:
typedef struct Signle_Link_List LNode, *LinkList;
LNode
是结构体类型(结点)
LinkList
是LNode*
的别名(指向结点的指针)转换目的 :明确告诉编译器,这块内存将被当作
LinkList
(即指向LNode
的指针)使用。
4.
*L
的含义函数的参数是
LinkList* L
(二级指针):
LinkList* L
可以理解为:指向头指针的指针。
*L
解引用后得到的是头指针(LinkList
类型)。操作目的 :通过
*L = ...
修改外部的头指针,使其指向新分配的内存。
5. 整体流程
这行代码的完整意义是:
申请内存 :在堆内存中申请一块大小为
sizeof(LNode)
的内存。类型转换 :将返回的
void*
转换为LinkList
类型(即LNode*
)。赋值给头指针 :让外部的头指针
*L
指向这块内存。
6. 实际效果
这行代码执行后:
创建了一个头结点 :头结点的
data
字段未初始化(可能是垃圾值),但next
指针会被初始化为NULL
(见后续代码(*L)->next = NULL;
)。链表结构:
*L(头指针) │ ▼ [头结点] → next = NULL
7. 常见问题解答
Q1:为什么用
malloc
而不是直接声明变量?
答 :链表结点需要动态增减,
malloc
允许在运行时按需申请内存。对比:
LNode node; // 栈内存,函数结束后自动释放 LNode* p = malloc(...); // 堆内存,需要手动释放(用 free)
Q2:头结点的
data
字段有意义吗?
- 答 :在标准实现中,头结点的
data
通常不存储有效数据(仅作为链表入口),但代码中可能用它存储元信息(如长度)。Q3:为什么要用二级指针
LinkList* L
?
答:需要修改外部传入的头指针的值。C语言中,若想通过函数修改指针的值,必须传递指针的地址(即二级指针)。
示例:
void Init(LinkList* L) { *L = malloc(...); // 修改外部的头指针 } int main() { LinkList myList; // 此时 myList 是野指针 Init(&myList); // 传递头指针的地址 }
8. 图解过程
Before malloc: +------+ | L | --> 随机值(野指针) +------+ After malloc: +------+ +---------------------+ | L | --> | data(未初始化) | +------+ | next = NULL | +---------------------+
DestoryLinkList
函数原理详解这个函数的作用是 销毁整个单链表 ,包括 头结点 和所有 数据结点,并释放它们占用的内存。让我们一步步解析它的工作原理:
1. 函数参数
LinkList* L
(二级指针)
LinkList
是LNode*
的别名(指向结点的指针)。
LinkList* L
是一个 指向头指针的指针 (二级指针),目的是 修改外部的头指针 ,使其最终变为NULL
。
- 如果只传
LinkList L
(一级指针),函数内部修改L
不会影响外部的头指针,导致内存泄漏。
2.
while (*L != NULL)
循环
循环条件 :只要
*L
(当前头指针)不为NULL
,就继续释放内存。循环过程:
p = *L
:临时指针p
保存当前要释放的结点(头结点或数据结点)。
*L = (*L)->next
:让头指针*L
指向下一个结点(相当于链表"跳过"当前结点)。
free(p)
:释放p
指向的结点内存。
3. 销毁过程图解
假设链表结构如下:
头指针 *L │ ▼ [头结点] → [结点1] → [结点2] → NULL
循环步骤:
第一次循环:
p = *L
(p
指向头结点)
*L = (*L)->next
(头指针*L
指向结点1)
free(p)
(释放头结点)
*L → [结点1] → [结点2] → NULL
第二次循环:
p = *L
(p
指向结点1)
*L = (*L)->next
(头指针*L
指向结点2)
free(p)
(释放结点1)
*L → [结点2] → NULL
第三次循环:
p = *L
(p
指向结点2)
*L = (*L)->next
(头指针*L
指向NULL
)
free(p)
(释放结点2)
*L → NULL
循环结束:
*L
为NULL
,退出循环。
4. 最终效果
链表被完全销毁:所有结点(包括头结点)的内存被释放。
头指针
*L
被置为NULL
:防止外部代码误用已释放的内存(避免野指针)。
5. 为什么需要这样实现?
防止内存泄漏:必须逐个释放所有结点,否则未释放的内存会一直占用堆空间。
安全性 :将头指针置为
NULL
,避免后续代码误操作已释放的内存。
6. 对比
ClearLinkList
(清空链表)
ClearLinkList
只释放数据结点,保留头结点(链表可复用):
Status ClearLinkList(LinkList* L) { LNode* p, *q; q = (*L)->next; // q 指向第一个数据结点 while (q != NULL) { p = q; q = q->next; free(p); } (*L)->next = NULL; // 头结点的 next 置空 return OK; }
区别:
DestoryLinkList
:销毁整个链表(头结点+数据结点)。
ClearLinkList
:只清空数据结点,保留头结点。
7. 常见问题
Q1:为什么用
while (*L)
而不是while ((*L)->next)
?
- 答 :
*L
是当前头指针,需要释放所有结点(包括头结点)。如果检查(*L)->next
,会漏掉头结点。Q2:如果链表为空(只有头结点),会发生什么?
- 答 :
*L
指向头结点,第一次循环释放头结点后,*L
被置为NULL
,循环结束。Q3:为什么不用递归实现?
- 答:递归实现可能因链表过长导致栈溢出。迭代(循环)更安全高效。
8. 代码验证
可以通过打印结点地址验证释放过程:
Status DestoryLinkList(LinkList* L) { LNode* p; while (*L != NULL) { p = *L; printf("Freeing node at address: %p\n", p); // 打印释放的结点地址 *L = (*L)->next; free(p); } return OK; }
总结
核心操作:循环遍历链表,逐个释放结点,并更新头指针。
关键点 :二级指针修改头指针、
free
释放内存、防止野指针。适用场景:当确定链表不再使用时调用,避免内存泄漏。
CreateLinkList_H
函数原理详解(头插法创建单链表)这个函数的作用是 用头插法创建一个包含
n
个结点的单链表 。特点是 新结点总是插入在头结点之后 ,因此链表的顺序与输入顺序 相反。下面逐步解析其工作原理:
1. 函数参数
LinkList* L
:二级指针,用于修改外部的头指针。
int n
:要创建的结点数量。
2. 创建头结点
*L = (LinkList)malloc(sizeof(LNode)); // 分配头结点内存 (*L)->next = NULL; // 头结点的 next 初始化为 NULL
作用:初始化一个空链表,只有头结点(不存储实际数据)。
图示:
*L(头指针) │ ▼ [头结点] → NULL
3. 头插法循环(
for (i = n; i > 0; i--)
)循环
n
次,每次创建一个新结点并插入到 头结点之后。步骤拆解:
申请新结点内存:
LNode* newlnode = (LNode*)malloc(sizeof(LNode));
- 为新结点分配内存,并通过
scanf
输入数据。插入新结点:
newlnode->next = (*L)->next; // 新结点的 next 指向原第一个结点 (*L)->next = newlnode; // 头结点的 next 指向新结点
- 关键点 :新结点插入后成为链表的 第一个数据结点。
插入过程图示:
初始状态(只有头结点):
[头结点] → NULL
插入第一个结点(值为 1):
[头结点] → [1] → NULL
插入第二个结点(值为 2):
[头结点] → [2] → [1] → NULL
插入第三个结点(值为 3):
[头结点] → [3] → [2] → [1] → NULL
4. 输入顺序与链表顺序的关系
输入顺序 :假设依次输入
1, 2, 3
。链表顺序 :
3 → 2 → 1
(与输入相反)。原因:每次新结点都插入在链表头部。
5. 与尾插法(
CreateLinkList_R
)的区别
头插法 尾插法 新结点插入头结点之后 新结点追加到链表末尾 链表顺序与输入顺序相反 链表顺序与输入顺序相同 无需维护尾指针 需要维护尾指针 tail
时间复杂度:O(n)(每次插入为 O(1)) 时间复杂度:O(n)
6. 关键代码解析
newlnode->next = (*L)->next; // 新结点的 next 指向原第一个结点 (*L)->next = newlnode; // 头结点的 next 指向新结点
类比:像排队时每次都让新来的人站到队伍最前面。
操作顺序 :必须先设置
newlnode->next
,再修改(*L)->next
,否则会丢失原链表的引用。
7. 内存管理注意事项
每个
malloc
分配的内存必须在链表销毁时通过free
释放(如调用DestoryLinkList
)。如果输入
n
为 0,函数会创建一个只有头结点的空链表。
8. 示例输入输出
输入:
CreateLinkList_H(&myList, 3); // 依次输入:10, 20, 30
链表结构:
头结点 → [30] → [20] → [10] → NULL
9. 常见问题
Q1:为什么头插法会导致顺序相反?
- 答:每次新结点都插入在链表头部,类似"后来居上"。
Q2:如果
n
为负数会发生什么?
- 答:循环不会执行,链表只有头结点(需在函数开头添加参数检查)。
Q3:头插法的时间复杂度是多少?
- 答 :O(n),因为每个结点的插入操作是 O(1),共
n
次。
总结
核心思想:通过每次在头部插入新结点构建链表。
特点:简单高效,但顺序与输入相反。
适用场景:不需要保持输入顺序,或需要频繁在头部插入的场景(如栈的实现)。
ShowLinkList
函数原理详解(显示单链表内容)这个函数的作用是 遍历并打印单链表中的所有数据结点,同时显示每个结点的序号。如果链表为空,会提示用户。以下是逐步解析:
1. 函数参数
const LinkList* L
const
修饰符:表示不会修改链表内容(安全保护)。
LinkList* L
:二级指针,用于访问头结点(但这里只读不修改)。
2. 初始化指针
p
LNode* p = (*L)->next; // p 指向第一个数据结点(跳过头结点)
为什么从
(*L)->next
开始 ?头结点(
*L
)不存储实际数据,它的next
才指向第一个有效结点。
3. 检查链表是否为空
if (!p) { // 等价于 if (p == NULL) puts("The LinkList is empty"); return; }
- 逻辑 :如果
p
为NULL
,说明头结点的next
为空,链表无数据结点。
4. 遍历链表并打印数据
int i = 1; // 结点序号从1开始 while (p != NULL) { // 遍历直到链表末尾 printf("%d : %d\n", i, p->data); // 打印序号和数据 i++; // 序号递增 p = p->next; // p 移动到下一个结点 }
关键点:
p->data
:当前结点的数据。
p = p->next
:指针后移,实现遍历。
5. 示例输出
假设链表结构:
头结点 → [10] → [20] → [30] → NULL
调用
ShowLinkList(&list)
输出:
1 : 10 2 : 20 3 : 30
如果链表为空,输出:
The LinkList is empty
6. 遍历过程图解
初始状态: p = 头结点->next → [10] → [20] → [30] → NULL 第一次循环: 打印 1:10,p 移动到 [20] 第二次循环: 打印 2:20,p 移动到 [30] 第三次循环: 打印 3:30,p 移动到 NULL(循环结束)
7. 为什么用
while (p)
而不是while (p->next)
?
while (p)
:确保当前结点p
有效时才打印数据(包括最后一个结点)。如果写成
while (p->next)
,会漏掉最后一个结点的数据!
8. 时间复杂度
- O(n) :需要遍历所有
n
个数据结点,每个结点访问一次。
9. 安全性注意事项
const
保护:防止函数内意外修改链表。空指针检查 :避免访问
NULL->next
(已通过if (!p)
处理)。
总结
功能:按顺序显示链表所有结点的数据和序号。
关键操作 :指针遍历 (
p = p->next
)、空链表检查。适用场景:调试、查看链表内容、交互式程序输出。
CreateLinkList_R
函数原理详解(尾插法创建单链表)这个函数的作用是 用尾插法创建一个包含
n
个结点的单链表 。特点是 新结点总是插入在链表末尾 ,因此链表的顺序与输入顺序 一致。下面逐步解析其工作原理:
1. 函数参数
LinkList* L
:二级指针,用于修改外部的头指针。
int n
:要创建的结点数量。
2. 创建头结点
*L = (LinkList)malloc(sizeof(LNode)); // 分配头结点内存 (*L)->next = NULL; // 头结点的 next 初始化为 NULL
作用:初始化一个空链表,只有头结点(不存储实际数据)。
图示:
*L(头指针) │ ▼ [头结点] → NULL
3. 尾指针
p
的初始化
LNode* p = *L; // p 初始指向头结点
p
的作用 :始终指向当前链表的 最后一个结点(尾结点)。初始时 :链表只有头结点,所以
p
指向头结点。
4. 尾插法循环(
for (i = n; i > 0; i--)
)循环
n
次,每次创建一个新结点并插入到 链表末尾。步骤拆解:
申请新结点内存:
LNode* newlnode = (LNode*)malloc(sizeof(LNode));
- 为新结点分配内存,并通过
scanf
输入数据。初始化新结点:
newlnode->next = NULL; // 新结点的 next 置空(因为它将是新的尾结点)
插入新结点到末尾:
p->next = newlnode; // 原尾结点的 next 指向新结点 p = newlnode; // p 移动到新结点(更新尾指针)
- 关键点 :通过
p
直接找到链表末尾,实现 O(1) 时间复杂度的插入。插入过程图示:
初始状态(只有头结点):
[头结点] → NULL p → [头结点]
插入第一个结点(值为 1):
[头结点] → [1] → NULL p → [1]
插入第二个结点(值为 2):
[头结点] → [1] → [2] → NULL p → [2]
插入第三个结点(值为 3):
[头结点] → [1] → [2] → [3] → NULL p → [3]
5. 输入顺序与链表顺序的关系
输入顺序 :假设依次输入
1, 2, 3
。链表顺序 :
1 → 2 → 3
(与输入一致)。原因:每次新结点都追加到链表尾部。
6. 与头插法(
CreateLinkList_H
)的区别
尾插法 头插法 新结点插入链表末尾 新结点插入头结点之后 链表顺序与输入顺序相同 链表顺序与输入顺序相反 需要维护尾指针 p
无需维护尾指针 时间复杂度:O(n) 时间复杂度:O(n)
7. 关键代码解析
p->next = newlnode; // 将新结点链接到末尾 p = newlnode; // 更新尾指针
类比:像排队时每次都让新来的人站到队伍最后面。
必要性 :必须更新
p
,否则下次插入无法找到新的末尾。
8. 内存管理注意事项
每个
malloc
分配的内存必须在链表销毁时通过free
释放(如调用DestoryLinkList
)。如果输入
n
为 0,函数会创建一个只有头结点的空链表。
9. 示例输入输出
输入:
CreateLinkList_R(&myList, 3); // 依次输入:10, 20, 30
链表结构:
头结点 → [10] → [20] → [30] → NULL
10. 常见问题
Q1:为什么需要尾指针
p
?
- 答 :直接通过头指针找到链表末尾需要 O(n) 时间,而维护
p
可以在 O(1) 时间内访问末尾。Q2:如果
p
不更新会怎样?
- 答:所有新结点都会插入到原尾结点之后,但原尾结点不会更新,导致链表断裂。
Q3:尾插法的时间复杂度是多少?
- 答 :O(n),因为每个结点的插入操作是 O(1),共
n
次。
总结
核心思想:通过维护尾指针,每次在链表末尾插入新结点。
特点:保持输入顺序,适合需要顺序一致的场景。
关键操作 :尾指针更新 (
p = newlnode
)、内存分配与释放。
运行结果如下:
cpp
请输入数据:
90
请输入数据:
60
请输入数据:
45
1 : 90
2 : 60
3 : 45
第2个 : 60
请按任意键继续. . .
注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!