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