零基础入门C语言之C语言实现数据结构之双向链表
在阅读本文之前,建议读者有限阅读专栏内前面部分的文章
目录
前言
一、双向链表的结构
二、双向链表的实现
总结
前言
本文主要介绍与C语言数据结构中双向链表部分有关的知识。
一、双向链表的结构

需要注意,这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了读者更好的理解就直接称为单链表的头节点。 带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”。 “哨兵位”存在的意义就是遍历循环链表避免死循环。
二、双向链表的实现
要实现双向链表,并且是带头双向循环链表,我们就需要首先先确定一下它的结构。
typedef int LTDataType;
typedef struct ListNode {LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;
然后我们来实现一下它的初始化,但与我们之前学的单链表不同的是,它的初始化是需要先申请一个哨兵位的,而哨兵位的两个指针我们需要想一想是否可以定义为空指针。答案是不可以的,因为如果这样,就无法满足我们循环的定义了。所以我们请读者思考一下,如何实现上述思路呢?我给出代码示例如下:
LTNode* LTBuyNode(LTDataType x) {LTNode* node = (LTNode*)malloc(sizeof(LTNode));if (node == NULL) {perror("malloc failed");exit(1);}else {node->data = x;node->next = node;node->prev = node;}return node;
}
void LTInit(LTNode** pphead) {*pphead = LTBuyNode(-1);
}
然后我们写代码来测试一下:
#include "List.h"void ListTest01() {LTNode* plist = NULL;LTInit(&plist);
}int main() {ListTest01();return 0;
}
我们进入调试界面来看看是否完成了初始化:

可以看到是成功了的。
然后我们该如何实现它的尾插呢,相对来说这是一个比较简单的函数,请读者直接思考代码,我给出示例:
void LTPushBack(LTNode* phead, LTDataType x) {assert(phead);LTNode* newnode = LTBuyNode(x);newnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}
同时为了方便观察,我们写出打印列表的代码,如下:
void LTPrint(LTNode* phead) {assert(phead);LTNode* p = phead->next;while (p!= phead) {printf("%d->", p->data);p = p->next;}printf("NULL\n");
}
我们插入5个数据并打印出来试试:

我们接下来再来尝试下头插如何实现,请读者先行思考,我们给出示例代码:
void LTPushFront(LTNode* phead, LTDataType x) {assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}
我们插入5个数据看看:

代码此时没有问题。
在此之后,我们来实现一下链表的头删和尾删,同样与前面的思路类似,请读者自行思考,我给出示例代码:
void LTPopBack(LTNode* phead) {assert(phead && phead->next != phead);LTNode* p = phead->prev;p->prev->next = phead;phead->prev = p->prev;free(p);p = NULL;
}void LTPopFront(LTNode* phead) {assert(phead && phead->next != phead);LTNode* p = phead->next;p->next->prev = phead;phead->next = p->next;free(p);p = NULL;
}
然后我们分别头删和尾删3个数据,来测试一下:

然后我们来实现一下在指定位置之后插入数据,但在此之前我们需要实现一下找到这个位置的查找方法:
LTNode* LTFind(LTNode* phead, LTDataType x) {LTNode* pcur = phead->next;while (pcur != phead) {if (pcur->data == x) {return pcur;}pcur = pcur->next;}return NULL;
}
那么实现这个方法之后,我们就可以来测试一下指定位置之后插入函数:
void LTInsert(LTNode* pos, LTDataType x) {assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}
可以看到实现结果如下:

然后与之相似的就是删除指定位置的节点,我们给出示例代码如下:
void LTErase(LTNode* pos) {assert(pos);pos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}
我们试着删除存储0这个节点,其结果如下:

然后就是我们最后的链表的销毁操作:
void LTDestroy(LTNode* phead) {assert(phead);LTNode* pcur = phead->next;while (pcur != phead) {LTNode* pnext = pcur->next;free(pcur);pcur = NULL;}free(phead);phead = NULL;
}
所以,相对来说双向链表方法的实现是要比单链表简单许多,至少不会出现很多的循环,这大大减少了我们运行的时间。二者优缺点对比如下:

总结
本文介绍了C语言中双向链表的实现方法,重点讲解了带头双向循环链表的结构特点和使用哨兵位节点的必要性。文章详细阐述了双向链表的初始化、插入(头插、尾插、指定位置插入)、删除(头删、尾删、指定位置删除)、查找以及销毁等操作的实现原理,并提供了完整的代码示例。通过调试测试验证了各函数的正确性,展示了双向链表相比单链表的操作优势,特别是在避免循环和减少时间复杂度方面的改进。
