双向链表
双向链表
小白感觉双向链表和单向链表的区别并不大,就是地址的交接有点繁琐,需要清晰的逻辑,简单理解就是俩条平行线,无线延伸,但是俩个线不交叉,但都是在一张纸上开始延展,头结点就像这张纸,理解的可能有点抽象,但我感觉这就是个抽象的概念,根据如此特画出下图进行分析:
新链表的初始化
创建结构体存储结点数据
-
/***************************************************************** * * file name :DoubleLinkedList.c * authour :yq_dyx@163.com * date :2024/04/23 * function :设计一个函数,实现对双向链表的增删改查的功能。 * note :None * CopyRight (c) 2024 yq_dyx@163.com All Right Reseverd * ******************************************************************/ #include <stdio.h> #include <stdlib.h> #include <stdbool.h> typedef int DataType_t; //定义一个链表结构体,分别存储数据域和指针域 typedef struct doubleLinkedList { DataType_t data; // 数据域 struct doubleLinkedList *next; // 指针域 struct doubleLinkedList *prev; }doubLList_t;
创建空链表
创建一个空的链表和头结点,并进行初始化
/******************************************************************* * * name : doubleLinkedList_creat * function : 创建一个空链表并创建一个头结点,并进行初始化 * argument : * retval : None * author : yq_dyx@163.com * date : 2024/04/22 * note : None * * *****************************************************************/ //创建空链表,并创建一个头结点 doubLList_t * doubleLinkedList_creat() { doubLList_t *head = (doubLList_t *)calloc(1,sizeof(doubLList_t)); //calloc来申请不需要初始化更方便 if(NULL == head) { perror("calloc head is error\n"); // 分配内存失败 exit(1); } head->next = NULL; //头结点next初始化 head->prev = NULL; //头结点prev初始化 }
创建新结点
在添加结点时候必然要创建新的结点,再进行添加,代码如下:
/*******************************************************************
*
* name : doubleLinkedList_NewNode
* function : 创建一新的双向链表结点,并对新结点初始化
* argument : @data 新结点的数据域的数据
* retval : None
* author : yq_dyx@163.com
* date : 2024/04/23
* note : 此函数不单独使用,需要配合Insert函数使用
*
* *****************************************************************/
doubLList_t * doubleLinkedList_NewNode(DataType_t data)
{
doubLList_t *New =(doubLList_t *)malloc(sizeof(doubLList_t));
if(NULL == New)
{
perror("calloc NewNode is error\n"); // 分配内存失败
return NULL;
}
New->data = data; //新结点数据域初始化
New->next = NULL; //新结点指针域next初始化
New->prev = NULL; //新结点指针域prev初始化
}
插入操作
头插
当在头部进行插入新结点动作的时候,可以先画出下图,方便更好的理解
-
/******************************************************************* * * name : doubleLinkedList_HeadInsert * function : 结点在头部插入 * argument : @data 要插入的结点的数据域的数据 * @head 链表的头结点 * retval : None * author : yq_dyx@163.com * date : 2024/04/23 * note : 要判断此链表是否为空,为空则直接在头结点后面插入 * * *****************************************************************/ bool doubleLinkedList_HeadInsert(doubLList_t *head,DataType_t data) { doubLList_t *New = doubleLinkedList_NewNode(data); if(NULL == New) //判断新节点是否创建成功 { printf("calloc NewNode is failed\n"); return false; } if(NULL == head->next) //判断链表是否为空,则直接插入 { head->next = New; return true; } New->next = head->next; head->next->prev = New; head->next = New; return true; }
尾插
当选择在尾部插入的时候,我们先画出下图,这样更有利于我们理解
/*******************************************************************
*
* name : doubleLinkedList_TailInsert
* function : 结点在尾部插入
* argument : @data 要插入的结点的数据域的数据
* @head 链表的头结点
* retval : None
* author : yq_dyx@163.com
* date : 2024/04/23
* note : 要判断此链表是否为空,为空则直接在头结点后面插入,尾结点指向NULL
*
* *****************************************************************/
bool doubleLinkedList_TailInsert(doubLList_t *head,DataType_t data)
{
doubLList_t *phead = head; // 备份头结点
doubLList_t *New = doubleLinkedList_NewNode(data);
if(NULL == New) //判断新节点是否创建成功
{
printf("calloc NewNode is failed\n");
return false;
}
if(NULL == head->next) //判断链表是否为空,则直接插入
{
head->next = New;
return true;
}
while(phead->next)
{
phead = phead->next; // 遍历链表
}
phead->next = New;
New->prev = phead;
return true;
}
中间插入
当进行中间插入时需要考虑多种情况,比如就一个首结点,目标结点在头结点,目标结点在尾结点,我们都需要考虑,也画了下图进行简单分析有利于理解
-
/******************************************************************* * * name : doubleLinkedList_DestInsert * function : 结点在指定位置后插入 * argument : @data 指定结点的数据域的数据 * @value 要插入新的结点的数据域的数据 * @head 链表的头结点 * retval : None * author : yq_dyx@163.com * date : 2024/04/23 * note : None * * *****************************************************************/ bool doubleLinkedList_DestInsert(doubLList_t *head,DataType_t data,DataType_t value) { doubLList_t *phead = head; // 备份头结点 doubLList_t *p = head; // 备份头结点 doubLList_t *New = doubleLinkedList_NewNode(value); if(NULL == New) //判断新节点是否创建成功 { printf("calloc NewNode is failed\n"); return false; } if(NULL == head->next) //判断链表是否为空,则直接插入 { head->next = New; return true; } while(phead->next && phead->data != data) //再循环再次到尾巴结点时退出,说明没有该数据 { phead = phead->next; // 遍历链表,找到和data相同的节点时(phead)退出, } //头插 if(phead == NULL && phead->data != data) //当phead == NULL导致退出时,则没找该data,函数直接退出 { printf("not found this data\n"); return false; } //如果遍历链表发现目标结点,分为三种可能(头插,尾插,中间插),头插和中间插入相同 //头插和中间插入 if(phead->data == data) { New->next = phead->next; //将指定插入结点的直接后继的地址给新节点的next phead->next->prev = New; //将New的地址给指定插入结点的直接后继的prev phead->next = New; //将新节点的地址给指定插入结点的next New->prev = phead; //将指定插入结点的地址给新节点的prev } //尾插 else if(phead->next == NULL) { phead->next = New; New->prev = phead; } return true; }
删除操作
删除首节点
对于删除操作咱们先进行头部删除,尾部删除和中间指定删除
-
/******************************************************************* * * name : doubleLinkedList_HeadDelete * function : 删除首结点 * argument : @data 指定结点的数据域的数据 * @head 链表的头结点 * retval : None * author : yq_dyx@163.com * date : 2024/04/23 * note : None * * *****************************************************************/ bool doubleLinkedList_HeadDelete(doubLList_t *head) { if(NULL == head->next) //判断链表是否为空,为空直接退出 { printf("doubleLinkedList is empty\n"); return false; } doubLList_t *phead = head->next; // 备份首结点 head->next = phead->next; //将首结点的直接后继地址给头结点 phead->next->prev = head; //将头结点的地址给首结点的直接后继的prev phead->next = NULL; //对删除的结点释放,防止内存泄漏,以及段错误 free(phead); return true; }
删除尾节点
删除尾结点较为简单,操作不复杂,主要就是断开就好,就像下图所示:
/*******************************************************************
*
* name : doubleLinkedList_TailDelete
* function : 删除尾结点
* argument : @data 指定结点的数据域的数据
* retval : None
* author : yq_dyx@163.com
* date : 2024/04/23
* note : None
*
* *****************************************************************/
bool doubleLinkedList_TailDelete(doubLList_t *head)
{
int i = 0;
if(NULL == head->next) //判断链表是否为空,为空直接退出
{
printf("doubLinkedList is empty\n");
return false;
}
doubLList_t *phead = head; // 备份头结点
doubLList_t *p = head; // 备份头结点
while(phead->next)
{
phead = phead->next; // 遍历链表
}
phead->prev->next = NULL; //尾结点的直接前驱的next指向NULL,断开最后一个节点
phead->prev = NULL; //尾结点的prev指向NULL,与直接前驱断开
free(phead); //对删除的结点释放,防止内存泄漏,以及段错误
return true;
}
### 删除指定节点
<img src="111.assets/image-20240425103226652.png" alt="image-20240425103226652" style="zoom:150%;" />
中间删除指定结点
找到指定结点,但需要考虑多种情况,比如链表就一个首结点,链表为空,指定结点为首结点或者尾结点,简单图示如下:
-
/******************************************************************* * * name : doubleLinkedList_DestDelete * function : 删除指定位置的结点 * argument : @data 指定结点的数据域的数据 * @head 链表的头结点 * retval : None * author : yq_dyx@163.com * date : 2024/04/23 * note : None * * *****************************************************************/ bool doubleLinkedList_DestDelete(doubLList_t *head,DataType_t data) { if(NULL == head->next) //判断链表是否为空,为空直接退出 { printf("doubLinkedList is empty\n"); return false; } doubLList_t *phead = head->next; // 备份头结点 doubLList_t *p = head; // 备份头结点 while(phead->next && phead->data != data) //再循环再次到尾巴结点时退出,说明没有该数据 { phead = phead->next; // 遍历链表,找到和data相同的节点时(phead)退出, } if(phead->next == NULL && phead->data != data) //当phead == NULL导致退出时,又没找该data,函数直接退出 { printf("not found this data\n"); return false; } //头部删除 if(phead == head->next) //当目标结点为首结点 { head->next = phead->next; //更新头结点,让头结点连上头结点直接后继 if(phead->next != NULL) //当只有一个首结点的时候 { phead->next->prev = NULL; //让新的首结点的prev指向NULL phead->next = NULL; //让首结点的next直接指向NULL } free(phead); //释放删除的结点 } //尾部删除 else if(phead->next == NULL) //当phead == NULL导致退出时,找到该data { phead->prev->next = NULL; //尾结点的直接前驱的next指向NULL phead->prev = NULL; //尾结点的prev指向NULL free(phead); //释放删除的结点 } else //当删除中间结点的时候 { phead->prev->next = phead->next; //将指定的结点的next赋值给指定结点的直接前驱的next phead->next->prev = phead->prev; //对删除的结点释放,防止内存泄漏,以及段错误 phead->next = NULL; //将指定删除结点的next指向NULL; phead->prev = NULL; //将指定删除结点的prev指向NULL; free(phead); } return true; }
打印操作
对于整个链表进行遍历,当然也有不同情况要考虑,链表为空等等,切尾结点也需要打印
-
/******************************************************************* * * name : doubleLinkedList_Print * function : 遍历双向链表,并打印各结点的数据域 * argument : @data 指定结点的数据域的数据 * retval : None * author : yq_dyx@163.com * date : 2024/04/23 * note : None * * *****************************************************************/ bool doubleLinkedList_Print(doubLList_t *head) { doubLList_t *phead = head; // 备份头结点 if(NULL == head->next) //判断链表是否为空,为空直接退出 { printf("doubLinkedList is empty\n"); return false; } while(phead->next) { phead = phead->next; // 遍历链表 printf(" %d",phead->data); // 打印链表 } printf("\n"); return true }
主函数测试
int main()
{
doubLList_t * Head = doubleLinkedList_creat();
doubleLinkedList_HeadInsert(Head,8);
/*
doubleLinkedList_HeadInsert(Head,5);
doubleLinkedList_HeadInsert(Head,3);
doubleLinkedList_HeadInsert(Head,7);
doubleLinkedList_Print(Head); //7 3 5 8
doubleLinkedList_TailInsert(Head,42);
doubleLinkedList_TailInsert(Head,99);
doubleLinkedList_TailInsert(Head,87);
doubleLinkedList_TailInsert(Head,63);
doubleLinkedList_Print(Head); //7 3 5 8 42 99 87 63
doubleLinkedList_DestInsert(Head,42,11);
doubleLinkedList_DestInsert(Head,3,33);
doubleLinkedList_DestInsert(Head,87,61);
doubleLinkedList_Print(Head); //7 3 33 5 8 42 11 99 87 61 63
doubleLinkedList_DestDelete(Head,42);
doubleLinkedList_Print(Head);//
doubleLinkedList_HeadDelete(Head);
doubleLinkedList_Print(Head);
doubleLinkedList_TailDelete(Head);
doubleLinkedList_Print(Head);// 3 33 5 8 11 99 87 61
doubleLinkedList_DestDelete(Head,3);*/
doubleLinkedList_DestDelete(Head,8);
doubleLinkedList_Print(Head);// 33 5 8 11 99 87
return 0;
}
已经经过测试验证并成功,测试结果如代码的测试结果一致,如图:
虽然和上次的单向循环链表结果相同,主要是我测试的数据一样,嘿嘿嘿,偷了个懒,还有不少作业要做呢,希望以上的代码能对同是初学者的你有所帮助