(C语言)单链表(2.0)数据结构(指针,单链表教程)
#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){
printf("该链表为空!\n");
return ERROR;
}else{
printf("该链表不为空!\n");
return OK;
}
}
//求链表的长度
Status LenLinkList(const LinkList* L){
int i=0;
Lnode* p;
p=(*L)->next;
while (p!=NULL)
{
i++;
p=p->next;
}
printf("该链表的长度为:%d\n",i);
return OK;
}
//清空链表
Status ClearLinkList(LinkList* L){
Lnode* p;
Lnode* q;
p=(*L)->next;
while (p!=NULL)
{
q=p;
p=p->next;
free(q);
}
(*L)->next=NULL;
printf("该链表已清空!\n");
return OK;
}
//销毁链表
Status DestoryLinkList(LinkList* L){
LinkList p;//定义一个临时的指向结点的指针
while (*L!=NULL)
{
p=*L;//储存原来的指针(结点)
*L=(*L)->next;//往后移动结点
free(p);//释放原来的指针
}
printf("该链表已销毁!\n");
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;
printf("该链表的数据为:\n");
while (p!=NULL)
{
printf("%d : %d\n", i, p->data); // 打印序号和数据
i++; // 序号递增
p = p->next; // p 移动到下一个结点
}
return OK;
}
//单链表插入,在第i个结点之前插入一个结点
Status InsertLinkList(LinkList *L, int i, Elemtype value)
{
Lnode* p;
p = (*L)->next;
int j = 1;
while (p && j < i - 1) //找到第i-1个结点
{
j++;
p = p->next;
}
if (!p || j > i - 1) //i大于表长+1,或者小于1,插入位置非法
return ERROR;
Lnode* newlnode;
newlnode = (Lnode*)malloc(sizeof(Lnode));
newlnode->data = value;
newlnode->next = p->next;
p->next = newlnode;
return OK;
}
//删除第i个结点
Status DelLinkList(LinkList* L,int i){
int j=i;
i=2;
Lnode* p=(*L)->next;
Lnode* q;
while (j!=i)
{
i++;
p=p->next;
}
q=p->next;
p->next=q->next;
free(q);
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,4);//头插
CreateLinkList_r(&mylist,4);//尾插
ShowLinkList(&mylist);
InsertLinkList(&mylist, 3, 9);
ShowLinkList(&mylist);
LenLinkList(&mylist);
DelLinkList(&mylist,3);
ShowLinkList(&mylist);
LocatElem(&mylist,2);
IsEmptyLinkList(&mylist);
ClearLinkList(&mylist);
DestoryLinkList(&mylist);
}
上篇博客我们解释了一部分单链表的原理,今天继续完善单链表的查询和删除等操作:
单链表插入操作的原理详解(新手友好版)
1. 函数功能
这个函数的作用是:在单链表的第 i 个位置前插入一个新结点,新结点的值为
value
。
2. 关键变量解析
LinkList *L
:指向头指针的指针(二级指针),用于访问整个链表
int i
:要插入的位置(从1开始计数)
Elemtype value
:新结点的值
Lnode* p
:临时指针,用于遍历链表
Lnode* newlnode
:新创建的结点
3. 插入过程分步解析
步骤1:定位插入位置的前驱结点
Lnode* p = (*L)->next; // p从第一个数据结点开始(跳过头结点) int j = 1; // 计数器从1开始 while (p && j < i-1) { // 寻找第i-1个结点 p = p->next; j++; }作用:
让指针p
最终指向 第i-1个结点(即要插入位置的前一个结点)。示例:
如果要在第3个位置前插入,p
需要指向第2个结点:复制
头结点 → [结点1] → [结点2] → [结点3] → NULL ↑ p(此时i=3, j=2)
步骤2:检查位置合法性
if (!p || j > i-1) return ERROR; // 插入位置非法可能出错的情况:
p == NULL
:i
超出链表长度(如链表只有2个结点,但想插入到第4个位置前)
j > i-1
:i < 1
(如传入i = 0
)
步骤3:创建新结点
Lnode* newlnode = (Lnode*)malloc(sizeof(Lnode)); newlnode->data = value; // 设置新结点的值内存变化:
在堆内存中分配一个新结点,并存储数据value
。
步骤4:链接新结点
newlnode->next = p->next; // 新结点指向原第i个结点 p->next = newlnode; // 前驱结点指向新结点关键操作:
newlnode->next = p->next
:
让新结点的指针指向原来第i个位置的结点。
p->next = newlnode
:
让前驱结点(第i-1个结点)指向新结点。图示(在第2个位置前插入新结点):
插入前: 头结点 → [A] → [B] → [C] → NULL ↑ p(指向第1个结点) 插入后: 头结点 → [A] → [新结点] → [B] → [C] → NULL
4. 时间复杂度分析
最好情况:插入到第1个位置前 → O(1)
最坏情况:插入到链表末尾 → O(n)
平均情况:O(n)
5. 易错点提醒
边界条件:
插入位置
i = 1
(第一个数据结点前)插入位置
i = 表长+1
(相当于尾插)内存泄漏:
如果插入失败(如位置非法),需确保已分配的newlnode
被释放:if (!p || j > i-1) { free(newlnode); // 防止内存泄漏 return ERROR; }头结点的作用:
头结点保证了即使插入位置i=1
,也能统一处理(无需特殊代码)。
6. 完整示例
假设现有链表:
头结点 → [10] → [20] → [30] → NULL
调用InsertLinkList(&L, 2, 15)
后:复制
头结点 → [10] → [15] → [20] → [30] → NULL
7. 为什么需要二级指针?
如果插入位置是第1个结点前,需要修改头结点的
next
指针。通过
LinkList* L
(二级指针)可以修改外部头指针的值。
8. 对比头插法和尾插法
操作 时间复杂度 特点 按位置插入 O(n) 通用,但需要遍历 头插法 O(1) 始终插入头部,顺序与输入相反 尾插法 O(n) 顺序与输入一致
总结
核心思想:通过遍历找到插入位置的前驱结点,修改指针链接关系。
关键步骤:定位前驱、创建结点、重新链接。
学习建议:
画图辅助理解指针变化
尝试手动模拟插入过程
结合调试工具观察内存变化
单链表删除第i个结点的原理详解(新手版)
1. 函数功能
这个函数的作用是:删除单链表中第i个数据结点,并释放其内存空间。
2. 关键变量解析
LinkList* L
:指向头指针的指针(二级指针)
int i
:要删除的结点位置(从1开始计数)
Lnode* p
:临时指针,最终指向第i-1个结点
Lnode* q
:临时指针,指向要删除的第i个结点
3. 删除过程分步解析
步骤1:初始化指针
int j = i; // 保存目标位置i i = 1; // 从第1个位置开始查找 Lnode* p = (*L); // p初始指向头结点作用:
准备从头结点开始遍历,寻找第i-1个结点。步骤2:定位前驱结点
while (j != i) { // 循环直到i == j i++; p = p->next; }实际效果:
让指针p
最终指向第i-1个结点(要删除结点的前一个结点)。示例(删除第3个结点):
初始:p → 头结点, i=1, j=3 第1次循环:p → 第1个结点, i=2 第2次循环:p → 第2个结点, i=3 (循环结束)步骤3:执行删除操作
q = p->next; // q指向要删除的第i个结点 p->next = q->next; // 绕过要删除的结点 free(q); // 释放内存关键操作图示:
删除前: [第i-1个结点] → [第i个结点(q)] → [第i+1个结点] ↑ p->next 删除后: [第i-1个结点] → [第i+1个结点]
4. 时间复杂度分析
最好情况:删除第1个结点 → O(1)
最坏情况:删除最后一个结点 → O(n)
平均情况:O(n)
5. 正确执行流程示例
链表结构:
头结点 → [10] → [20] → [30] → NULL操作:
DelLinkList(&L, 2)
执行过程:
p
初始指向头结点第一次循环后:
p
指向第1个结点(值为10)循环结束(
pos=2
等于目标位置)删除
p->next
(值为20的结点)最终链表:
复制
头结点 → [10] → [30] → NULL
6. 关键注意事项
必须检查
p->next
:
避免删除不存在的结点导致程序崩溃。必须释放内存:
防止内存泄漏(memory leak)。头结点的作用:
保证删除第1个结点时也能统一处理(不需要特殊逻辑)。
7. 常见问题解答
Q:为什么
p
初始指向头结点而不是第一个数据结点?
A:因为要删除第i个结点需要修改它的前驱结点的next
指针。当i=1时,前驱结点就是头结点。Q:如果链表为空会怎样?
A:修正后的代码会因!p->next
条件返回ERROR,避免访问空指针。Q:为什么删除操作是O(n)时间复杂度?
A:因为需要遍历链表找到要删除的位置。只有删除头结点时是O(1)。
总结
核心原理:通过遍历找到待删除结点的前驱结点,修改指针关系后释放内存。
三个关键步骤:定位前驱、绕过目标、释放内存。
学习建议:
用纸笔画出指针变化过程
使用调试工具单步执行观察变量
尝试实现其他操作(如按值删除)
单链表清空操作的原理详解(新手友好版)
1. 函数功能
这个函数的作用是:清空单链表中的所有数据结点,但保留头结点,使链表恢复到初始空表状态。
2. 关键变量解析
LinkList* L
:指向头指针的指针(二级指针)
Lnode* p
:工作指针,用于遍历链表(初始指向第一个数据结点)
Lnode* q
:临时指针,用于保存待释放的结点
3. 清空过程分步解析
步骤1:初始化指针
Lnode* p = (*L)->next; // p指向第一个数据结点(跳过头结点)作用:从第一个实际存储数据的结点开始处理。
步骤2:循环释放所有结点
while (p != NULL) { q = p; // q记录当前要释放的结点 p = p->next; // p移动到下一个结点 free(q); // 释放当前结点内存 }内存变化过程:
初始状态: [头结点] → [结点1] → [结点2] → ... → [结点N] → NULL ↑ p 每次循环: 1. q = p(记录要释放的结点) 2. p = p->next(p后移) 3. free(q)(释放原结点)步骤3:重置头结点指针
(*L)->next = NULL; // 头结点的next置空作用:将链表恢复为只有头结点的初始状态。
步骤4:输出提示
printf("该链表已清空!\n");用户体验:提示用户操作已完成。
4. 时间复杂度分析
时间复杂度:O(n)
需要遍历所有n个数据结点,每个结点的释放操作是O(1)。
5. 内存管理关键点
必须逐个释放:
每个通过malloc
分配的结点都必须通过free
释放,否则会导致内存泄漏。指针顺序不能错:
必须先保存
p
到q
然后才能移动
p = p->next
最后释放
q
头结点保留:
清空后头结点仍然存在,可以继续使用该链表。
6. 与销毁链表的区别
清空链表 销毁链表 保留头结点 释放包括头结点在内的所有结点 链表可继续使用 链表不可再使用 (*L)->next = NULL
*L = NULL
7. 示例演示
清空前链表:
头结点 → [10] → [20] → [30] → NULL执行过程:
p指向[10]
释放[10],p移动到[20]
释放[20],p移动到[30]
释放[30],p变为NULL
头结点next置空
清空后链表:
头结点 → NULL
8. 常见问题解答
Q:为什么不清空头结点?
A:保留头结点可以复用链表,避免重复初始化。若需要完全销毁应调用DestoryLinkList
。Q:如果链表已经是空的会怎样?
A:p
初始为NULL,while循环直接跳过,仅执行(*L)->next = NULL
。Q:能否用递归实现清空?
A:技术上可以,但不推荐。递归深度过大会导致栈溢出,且效率不如迭代。
9. 代码改进建议
增加安全性检查:
if (*L == NULL) return ERROR; // 检查链表是否已初始化
优化提示信息:
printf("成功清空%d个结点\n", count); // 可添加计数器统计释放的结点数
总结
核心思想:遍历链表逐个释放数据结点,最后重置头结点指针。
关键操作:指针顺序移动、内存释放、头结点重置。
学习建议:
配合画图理解指针变化
在调试模式下观察内存地址变化
对比清空与销毁的操作差异
运行结果如下:
请输入数据:
1
请输入数据:
2
请输入数据:
3
请输入数据:
4
该链表的数据为:
1 : 1
2 : 2
3 : 3
4 : 4
该链表的数据为:
1 : 1
2 : 2
3 : 9
4 : 3
5 : 4
该链表的长度为:5
该链表的数据为:
1 : 1
2 : 2
3 : 3
4 : 4
第2个 : 2
该链表不为空!
该链表已清空!
该链表已销毁!
请按任意键继续. . .
注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!