#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 构造一个单链表
// 单链表的结点由两个部分构成,一个是数据域,一个是指针域 单链表中数据域中的数据类型是同一类型,指针域中存放的是下一个结点的地址
// 先将单链表中的数据定为int型,用户可根据实际情况进行修改
typedef int DataType_t;
// 创建单链表结点的结构体,每个结点包含两个部分
typedef struct LinkedList
{
DataType_t data; // 单链表中结点的数据(结点的数据域)
struct LinkedList *next; // (结点的指针域)由于结点中的第二部分存的是下一个结点的地址,所以用到指针变量,地址下的值的类型是结点类型,也就是结构体的类型; (说白了,存放的是直接后继的地址);
// 不能在结构体中出现结构体本身,但是可以出现指向结构体本身的指针
} LList_t;
// 1.创建一个空链表,空链表中应该有一个头节点,对链表进行初始化
LList_t *LList_Create() // 这里不需要传入链表的数量,进行申请内存,因为单链表不需要提前申请一大块内存,咱们随用随往里面放就ok;
{
// 头结点唯一的目的就是为了访问首结点
// 空链表中只有一个头结点,并且头节点中什么都没有,所以要给头节点申请内存
// 1.创建一个头节点,并未头节点申请内存(只有一个头节点,所以申请一块内存,申请一个结构体大小的内存,因为一个结构体就是一个结点)
LList_t *Head = (LList_t *)calloc(1, sizeof(LList_t)); //(这个堆内存下面存的就是这个结构体,这个结构体就是一个结点,地址下的值就是结构体类型)
// 此时Head就是头指针
// 记住,申请堆内存之后一定要进行判断申请的堆内存是否为空,要是空,则退出此函数,因为不做判断的话,若为空,则访问结构体里面的数据,则会出现段错误;
if (Head == NULL)
{
perror("calloc memory for Head is failed");
exit(-1); // 如果申请空间失败,则直接退出
}
// 下面是申请内存成功的代码
// 2.对头节点进行初始化,头节点是不存储有效数据的,所以不能给data赋值;(calloc已经默认data赋值为0了)
// Head是结构体指针,可以访问结构体里面的元素
Head->next = NULL; // 此时没有有效结点的地址
// 3.由于Head是是一个局部变量,函数调用完,就会释放,要是没有这个地址,则不能访问这块内存,所以将头节点的地址返回;
return Head; // 由于返回的是指针,也可以叫地址,所以函数的返回值类型应该为LList_t *(指针类型);
}
// 插入的是结点,结点也需要申请内存
// 2.创建一个新节点,并为新结点申请堆内存,以及对新节点的数据域和指针域进行初始化(初始化说白了是进行赋值动作);
LList_t *LList_NewNode(DataType_t data) // 此时需要传参,传的是要往新结点里面存的元素(传数据域的值进来),这里的data和结构体里面的data不冲突
{
// 2.1.创建一个新节点,并未新节点申请内存(每次只创建一个节点,所以申请一块内存,申请一个结构体大小的内存,因为一个结构体就是一个结点)
LList_t *New = (LList_t *)calloc(1, sizeof(LList_t)); //(这个堆内存下面存的就是这个结构体,这个结构体就是一个结点,地址下的值就是结构体类型)
// 记住,申请堆内存之后一定要进行判断申请的堆内存是否为空,要是空,则退出此函数,因为不做判断的话,若为空,则访问结构体里面的数据,则会出现段错误;
if (New == NULL)
{
perror("calloc memory for New is failed");
return NULL; // 如果申请空间失败,则直接退出 也可以写exit(-1);
}
// 2.2 对新结点的数据和指针域进行初始化
New->data = data; // 结点中数据域的值存入传进来的值
New->next = NULL; // 存放的是当前新结点下一个结点的地址,这里是NULL,是因为只是创建了一个新节点,并没有往链表里面放
return New; // 返回的是地址,和返回头节点的方式相似;
}
// 如果链表是空的(只有一个头结点),插入的时候,让头结点的指针域指向新节点的地址就欧克了(此时的新节点就是首结点)
// 链表是非空的(既有头结点,也有其他结点),分三种情况,头插,中间插,尾插(单链表尾插最麻烦,因为不能直接知道尾结点的地址(只知道头节点的地址),要从头开始访问,找到尾结点,然后将新节点插入到尾结点的后面);
// 头插是最简单的,因为此时要插入的结点即将成为首结点(咱们只知道头结点的地址);
// 如果链表为空,此时头插,尾插,中间插都一样;
// 重要一点,不论插入删除,先连接,后断开;
// 3.头插(插入新结点失败或者成功是此函数的返回值)
bool LList_HeadInsert(LList_t *Head, DataType_t data) // 此时需要传入参数,不管是头插,尾插,中间插,都要把头指针传进来 还要把新结点传进来,但此时传的是新结点的数据域里的值,一会再插入函数中,直接调用新节点的函数,就可以把新节点传进来了(注意只有做插入的时候,才需要新节点,删除的时候不需要,肯定不会创建一个新结点,再将它删除)
{
// 3.1 创建一个新节点,并为新结点申请堆内存,以及对新节点的数据域和指针域进行初始化(初始化说白了是进行赋值动作);
LList_t *New = LList_NewNode(data); // 这个函数会返回新节点的地址,用一个指针变量去接收 (这个New和2.1中的New不冲突,因为作用的不是同一个作用域),这个New就是新节点的地址
// 假设2.1中申请内存失败了,则返回NULL,从而导致创建一个结点失败了
// 判断指针变量的地址是否为空
if (New = NULL)
{
perror("can not insert new node"); ////假设2.1中申请内存失败了,则返回NULL,从而导致创建一个结点失败了,所以报这个错误的话,也会报2.1中的错误提示
return false;
}
// 3.2 首先判断链表是否为空,若为空,直接插入即可
if (Head->next == NULL) //(头节点的指针域要是为NULL,则没有插入任何元素进来)
{
// 记住写链表赋值的时候,先写等号,再写等号的右边,再写等号的左边,这样分析程序不会出错;
Head->next = New; // 将新结点的地址赋值给头结点的指针域;
return true;
}
// 3.3 如果链表不为空(既有头节点,也有其他结点),则把链表插入到链表的头部;
// 不论插入删除, 先连接, 后断开
// 此时新结点就作为了首结点;
New->next = Head->next; // 把链表首结点的地址放在了新节点的指针域里面 (Head->next是首结点的地址)
Head->next = New; // 将新节点的地址赋值给头结点的指针域;
return true;
}
// 4.尾插
bool LList_TailInsert(LList_t *Head, DataType_t data)
{
// 4.1记录Head的地址,防止头指针丢失,若不备份,遍历一次链表后,链表的起点丢失,后续无法再访问链表,当遍历结束后,Head仍然指向头结点
LList_t *Phead = Head;
// 3.1 创建一个新节点,并为新结点申请堆内存,以及对新节点的数据域和指针域进行初始化(初始化说白了是进行赋值动作);
LList_t *New = LList_NewNode(data); // 这个函数会返回新节点的地址,用一个指针变量去接收 (这个New和2.1中的New不冲突,因为作用的不是同一个作用域),这个New就是新节点的地址
// 假设2.1中申请内存失败了,则返回NULL,从而导致创建一个结点失败了
// 判断指针变量的地址是否为空
if (New = NULL)
{
perror("can not insert new node"); ////假设2.1中申请内存失败了,则返回NULL,从而导致创建一个结点失败了,所以报这个错误的话,也会报2.1中的错误提示
return false;
}
// 3.2 首先判断链表是否为空,若为空,直接插入即可
if (Head->next == NULL) //(头节点的指针域要是为NULL,则没有插入任何元素进来)
{
// 记住写链表赋值的时候,先写等号,再写等号的右边,再写等号的左边,这样分析程序不会出错;
Head->next = New; // 将新结点的地址赋值给头结点的指针域;
return true;
}
// 3.3 如果链表不为空(既有头节点,也有其他结点),则把链表插入到链表的尾部;
// 不论插入删除, 先连接, 后断开
// 此时新结点就作为了首结点;
while (Phead->next)
{
Phead = Phead->next; // 移动头指针的指向,指向到尾结点,当找到尾结点,自动退出循环,因为尾结点的next为NULL;
}
Phead->next = New; // 创建新节点的时候,已经将New中的next指向NULL了,所以此时不需要将New中的next指向NULL了
return true;
}
// 5.中间插(指定位置插入)
bool LList_DestInsert(LList_t *Head, DataType_t destval, DataType_t data) // dest是目标值变量
{
// 4.1记录Head的地址,防止头指针丢失
LList_t *Phead = Head->next; // 将头指针指向首结点
// 3.1 创建一个新节点,并为新结点申请堆内存,以及对新节点的数据域和指针域进行初始化(初始化说白了是进行赋值动作);
LList_t *New = LList_NewNode(data); // 这个函数会返回新节点的地址,用一个指针变量去接收 (这个New和2.1中的New不冲突,因为作用的不是同一个作用域),这个New就是新节点的地址
// 假设2.1中申请内存失败了,则返回NULL,从而导致创建一个结点失败了
// 判断指针变量的地址是否为空
if (New = NULL)
{
perror("can not insert new node"); ////假设2.1中申请内存失败了,则返回NULL,从而导致创建一个结点失败了,所以报这个错误的话,也会报2.1中的错误提示
return false;
}
// 3.2 首先判断链表是否为空,若为空,直接插入即可
if (Head->next == NULL) //(头节点的指针域要是为NULL,则没有插入任何元素进来)
{
// 记住写链表赋值的时候,先写等号,再写等号的右边,再写等号的左边,这样分析程序不会出错;
Head->next = New; // 将新结点的地址赋值给头结点的指针域;
return true;
}
// 5.4 遍历链表,为了找到目标结点,比较结点的数据域,若数据域相同,则找到了目标结点
// while(Phead->next) //这个while循环,只是为了找到目标结点
// {
// Phead = Phead->next; //每一次遍历,移动头指针,指向下一个结点
// if(Phead->data == destval) //找到了目标结点
// {
// break; // 找到了,则跳出循环 但是循环条件Phead->next=NULL的情况是找到为尾结点自动退出循环,但是没找到的情况,此时Phead就指向了尾结点
// }
// }
// // 如果Phead的next为NULL,说明没找到,但是找的元素刚好是尾结点呢,会出现代码不对的情况,所以改懂5.4的代码
while (Phead != NULL && destval != Phead->data)
// Phead != NULL:确保当前指针 Phead 指向的是一个有效节点(非空),避免访问非法内存(如 Phead->data 时发生段错误)。如果 Phead == NULL,说明已经遍历完链表但未找到目标值。
// destval != Phead->data:检查当前节点的数据 Phead->data 是否不等于目标值 destval。如果相等,循环终止,说明找到了目标节点。
// 两者要同时满足才进入循环
{
Phead = Phead->next; // 每一次遍历,移动头指针(刚开始头指针是指向头节点的),指向下一个结点(Phead就是下一个结点的地址)
}
if (Phead == NULL) // 说明找了一圈没找到目标值,则退出
{
return false;
}
// 5.5如果指向了下面的代码,说明找到了目标代码,则把新结点加入到目标结点的后面(此时的Phead指向了目标节点)
New->next = Phead->next; // 先链接
Phead->next = New; // 后断开,再链接
return true;
}
// 7.遍历单链表,将每一个结点的数据域输出
void LList_Print(LList_t *Head)
{
// 对链表的头节点地址进行备份,防止丢失,因为下面的判断条件里面会移动头指针
LList_t *Phead = Head;
// 先判断头结点的地址是不是空(NULL),若不是空,进行操作
while (Phead->next) // 总有一次Phead ->next会为NULL,当遍历到尾结点的时候;
{
// 把头结点的直接后继作为新的头节点
Phead = Phead->next; // 由于头结点没有有效数据,所以让头指针指向首结点,紧接着输出首节点的值,进行循环判断,再移动头指针
// 输出当前结点的数据域
printf("data = %d\n", Phead->data);
}
}
// 8.删除
// 链表是空的,只有一个头结点,头节点数据域里面没东西,直接走人
// 链表非空(既有头结点也有其他结点),要考虑删的东西在哪里,并不需要考虑创建新节点,分为头删(删除首结点,此时首结点后面的结点就成为了首结点),中间删和尾删;
// 头删(删除首结点) 1.要知道首节点和首节点直接后继的地址,要头节点的next指向首节点的直接后继
// 尾删 1.遍历链表,找到尾结点和尾结点的直接前驱 2.将尾节点的直接前驱的next赋值为NULL(此时尾节点的直接前驱节点为新的尾节点) 3,释放旧尾节点 (这三步做完,尾节点的直接前驱就成为了新的尾节点)
// 指定删除 1.遍历链表,找到待删除的节点和待删除的节点的直接前驱 2.将待删除的节点的直接前驱的next赋值为待删除的节点的直接后继的地址(先连接,因为要是你先断开的话,也就是将待删除结点的指针域置为NULL,此时就没办法链接了,因为待删除的节点的直接前驱找不到待删除的节点的直接后继的地址了); 3.将待删除结点的next指向NULL 4.释放待删除的结点
// 8.1头删除
bool LList_HeadDel(LList_t *Head) // 将头指针的地址传来
{
// 1.对链表的头节点地址进行备份,防止丢失,因为下面的判断条件里面会移动头指针
LList_t *Phead = Head;
// 2.判断链表是否为空,如果为空,则直接退出(因为没有首结点,则不需要做删除动作);
if (Head->next == NULL)
{
return false;
}
// 3.链表非空,则直接删除首结点
// (注意但凡看到链表插入和删除操作一定要先链接,再断开);
Head->next = Phead->next->next; // Phead->next->next为首节点直接后继的节点地址 Head->next是头节点指针域 所以此时首节点的直接后继节点作为了新的首节点 (这一步是链接) ,注意这里不能写为Phead->next 因为这样写会写下一行代码会找不到旧的首结点,此时最开头的一行代码 (对链表的头节点地址进行备份)也会失效;
// 上面这一行代码是先链接,由于先链接了,则找不到首节点的地址了(说白了,头节点和首节点的连接处断开了),所以这个函数中第一步先将Head进行了备份,就是为了找到首节点(重点),下一行代码才能操作首节点;
Phead->next->next = NULL; // 将旧的首节点的next置为NULL (这一步是断开)(自己拿图分析)//Phead还是原来的头结点
// 完全断开之后做free;
free(Phead->next);
return true;
}
// 8.2 尾删(删除链表最后一个节点)
bool LList_TailDel(LList_t *Head)
{
// 1.判断链表是否为空,若为空,直接退出函数
if (Head->next == NULL)
{
return false;
}
// 2.创建两个指针,一个指向当前节点,一个指向前驱节点
LList_t *Current = Head->next; // 当前节点(从首节点开始) (Current此时指向首节点)
LList_t *Prev = Head; // 前驱节点(初始为头节点) (Prev此时指向头节点)
// 3.遍历链表找到尾节点及其前驱节点(循环的方法去做)
while (Current->next != NULL)
{
Prev = Current; // 前驱节点指向当前节点
Current = Current->next; // 当前节点指向下一个节点
}
// 4.此时Current指向尾节点,Prev指向尾节点的前驱节点
Prev->next = NULL; // 将前驱节点的next置为NULL (此时Prev为新的尾节点,next值为空,则断开了链接);
free(Current); // 释放旧尾节点内存
return true;
}
// 8.3 指定删除(根据数据值删除节点)
bool LList_DestDel(LList_t *Head, DataType_t destval)
{
// 1.判断链表是否为空,没东西可删,直接退出
if (Head->next == NULL)
{
return false;
}
// 2.创建两个指针,一个指向当前节点,一个指向前驱节点
// 使用两个指针(Current和Prev)来跟踪当前节点及其前驱节点
LList_t *Current = Head->next; // 当前节点(从首节点开始)(Current指向首节点)
LList_t *Prev = Head; // 前驱节点(初始为头节点)(Prev指向头节点)
// 3.遍历链表查找目标节点
while (Current != NULL && Current->data != destval) // Current->data != destval: 检查当前节点的数据是否不等于目标值 destval,如果相等,循环终止,说明找到了目标节点。 Current != NULL:确保当前节点是有效节点(不是空指针),避免访问非法内存(防止 Current->data 出现段错误),并且如果 Current == NULL,说明已经遍历完链表但未找到目标值。
// 两者要同时满足,才进入循环
{
Prev = Current; // 前驱节点指向当前节点
Current = Current->next; // 当前节点指向下一个节点
}
// 4.如果没找到目标节点,直接退出此函数
if (Current == NULL)
{
return false;
}
// 5.能执行下面的代码,说明找到目标节点了,进行删除操作
Prev->next = Current->next; // 前驱节点指向当前节点的下一个节点(先链接)
Current->next = NULL; // 断开当前节点的连接(再断开)
free(Current); // 释放当前节点内存
return true;
}
// 9.链表销毁函数 LList_Destroy,用于释放所有节点内存(包括头节点)
void LList_Destroy(LList_t *Head)
{
LList_t *Current = Head;
while (Current)
{
LList_t *Temp = Current;
Current = Current->next;
free(Temp);
}
}
int main()
{
// 1. 创建一个空链表(只有头节点)
LList_t *List = LList_Create();
if (List == NULL)
{
printf("Failed to create list!\n");
return -1;
}
// 2. 头插法插入节点(插入顺序:3 -> 2 -> 1)
LList_HeadInsert(List, 1);
LList_HeadInsert(List, 2);
LList_HeadInsert(List, 3);
printf("After head insert (3->2->1):\n");
LList_Print(List); // 输出:3, 2, 1
// 3. 尾插法插入节点(插入顺序:4 -> 5)
LList_TailInsert(List, 4);
LList_TailInsert(List, 5);
printf("\nAfter tail insert (4->5):\n");
LList_Print(List); // 输出:3, 2, 1, 4, 5
// 4. 指定位置插入(在值为 2 的节点后插入 99)
LList_DestInsert(List, 2, 99);
printf("\nAfter inserting 99 after 2:\n");
LList_Print(List); // 输出:3, 2, 99, 1, 4, 5
// 5. 头删(删除首节点 3)
LList_HeadDel(List);
printf("\nAfter head deletion (remove 3):\n");
LList_Print(List); // 输出:2, 99, 1, 4, 5
// 6. 尾删(删除尾节点 5)
LList_TailDel(List);
printf("\nAfter tail deletion (remove 5):\n");
LList_Print(List); // 输出:2, 99, 1, 4
// 7. 指定删除(删除值为 99 的节点)
LList_DestDel(List, 99);
printf("\nAfter deleting node with value 99:\n");
LList_Print(List); // 输出:2, 1, 4
// 8. 尝试删除不存在的节点(返回 false)
if (!LList_DestDel(List, 100))
{
printf("\nFailed to delete node 100 (not found).\n");
}
//9. 释放链表内存(可选:需补充链表销毁函数)
LList_Destroy(List);
return 0;
}