跳到主要内容数据结构:带头双向循环链表详解与实现 | 极客日志C算法
数据结构:带头双向循环链表详解与实现
介绍 C 语言中带头双向循环链表的结构与实现。通过哨兵位简化边界处理,利用双向指针实现 O(1) 复杂度的头尾插入删除操作。涵盖节点定义、函数接口、核心逻辑及测试代码,帮助理解该数据结构的应用。
观心15 浏览 引言
在单链表中,我们深入探索了单链表,学会了如何通过指针将零散的内存节点串联成一条线。单链表的结构简单,但操作时我们常常需要'瞻前顾后'——因为每个节点只知道下一个节点的位置,却不知道上一个节点,导致很多操作(如尾部删除)需要遍历整个链表。
为了解决这个问题,C 语言提供了更强大的链表形式:双向链表。它不仅知道下一个节点是谁,还知道上一个节点是谁,就像火车车厢不仅有后门,还有前门,可以双向通行。
今天,我们将学习最常用的双向链表结构——带头双向循环链表。
1. 双向链表的结构
1.1 什么是带头双向循环链表?
让我们把名字拆开看:
- 带头:存在一个特殊的节点,称为'头节点'或'哨兵位'(sentinel)。这个节点不存储有效数据,它的作用是简化边界条件的处理。就像停车场门口的岗亭,它本身不是车位,但帮助我们管理车辆进出。
- 双向:每个节点包含两个指针——
prev 指向前一个节点,next 指向后一个节点。
- 循环:最后一个节点的
next 指向头节点,头节点的 prev 指向最后一个节点。整个链表形成一个环,从任何节点出发都能遍历整个链表。

1.2 为什么需要哨兵位?
在单链表中,当我们插入或删除第一个节点时,需要修改头指针(一级指针的二级指针问题)。而有了哨兵位后,头指针永远指向这个哨兵节点,所有有效节点都在它之后。这样,插入和删除操作就不需要改变头指针的值,代码逻辑更加统一,边界条件也大大简化。
此外,循环的特性使得我们可以从哨兵位开始,既能正向遍历(next),也能反向遍历(prev),非常灵活。
2. 双向链表的实现
我们将使用 C 语言实现一个带头双向循环链表,提供常见的操作接口。
2.1 节点类型定义
typedef int ListdataType;
typedef struct ListNode {
ListdataType data;
struct ListNode* next;
struct ListNode* prev;
}Listnode;
2.2 函数声明
Listnode* BuyNode(ListdataType x);
Listnode* InitList();
;
;
;
;
;
Listnode* ;
;
;
;
void
PrintList
(Listnode* head)
void
Listpushback
(Listnode* head, ListdataType x)
void
Listpushfront
(Listnode* head, ListdataType x)
void
Listpopback
(Listnode* head)
void
Listpopfront
(Listnode* head)
Listfind
(Listnode* head, ListdataType x)
void
Listinsert
(Listnode* pos, ListdataType x)
void
Listdelete
(Listnode* pos)
void
Listdestroy
(Listnode* head)
2.3 函数实现
2.3.1 创建新节点(内部辅助函数)
Listnode* BuyNode(ListdataType x)
{
Listnode* node = (Listnode*)malloc(sizeof(Listnode));
if (node == NULL) {
perror("malloc error");
exit(1);
}
node->data = x;
node->prev = node->next = node;
return node;
}
2.3.2 初始化
Listnode* InitList()
{
Listnode* head = BuyNode(-1);
return head;
}
2.3.3 打印链表
void PrintList(Listnode* head)
{
Listnode* pcur = head->next;
while (pcur != head) {
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
2.3.4 尾插
void Listpushback(Listnode* head, ListdataType x)
{
assert(head);
Listnode* newnode = BuyNode(x);
Listnode* pcur = head->prev;
newnode->prev = pcur;
newnode->next = head;
pcur->next = newnode;
head->prev = newnode;
}
分析:利用循环特性,我们不需要遍历就能直接找到尾节点(phead->prev),因此尾插操作的时间复杂度为 O(1)。
2.3.5 尾删
void Listpopback(Listnode* head)
{
assert(head && head->next != head);
Listnode* del = head->prev;
del->prev->next = head;
head->prev = del->prev;
free(del);
del = NULL;
}
2.3.6 头插(在第一个有效节点之前插入)
void Listpushfront(Listnode* head, ListdataType x)
{
assert(head);
Listnode* newnode = BuyNode(x);
Listnode* pcur = head->next;
newnode->prev = head;
newnode->next = pcur;
pcur->prev = newnode;
head->next = newnode;
}
2.3.7 头删(删除第一个有效节点)
void Listpopfront(Listnode* head)
{
assert(head && head->next != head);
Listnode* del = head->next;
del->next->prev = head;
head->next = del->next;
}
2.3.8 查找
Listnode* Listfind(Listnode* head, ListdataType x)
{
Listnode* pcur = head->next;
while (pcur != head) {
if (pcur->data == x) {
printf("找到了\n");
return pcur;
}
pcur = pcur->next;
}
printf("链表中没有找到%d\n", x);
return NULL;
}
2.3.9 在指定位置之后插入
void Listinsert(Listnode* pos, ListdataType x)
{
assert(pos);
Listnode* newnode = BuyNode(x);
newnode->next = pos->next;
pos->next->prev = newnode;
newnode->prev = pos;
pos->next = newnode;
}
注意:这个操作在 pos 之后插入,时间复杂度 O(1)。如果我们想在 pos 之前插入,也可以利用双向链表的特性很快实现(只需稍微调整逻辑),但这里我们只实现一种。
2.3.10 删除指定位置的节点
void Listdelete(Listnode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
2.3.11 销毁链表
void Listdestroy(Listnode* head)
{
assert(head);
Listnode* pcur = head->next;
while (pcur != head) {
Listnode* del = pcur;
pcur = pcur->next;
free(del);
del = NULL;
}
free(pcur);
pcur = head = NULL;
}
2.4 测试代码示例
void Test01()
{
Listnode* head = NULL;
head = InitList();
Listnode* node1 = BuyNode(1);
Listnode* node2 = BuyNode(2);
Listnode* node3 = BuyNode(3);
Listnode* node4 = BuyNode(4);
head->next=node1; node1->prev=head;
node1->next=node2; node2->prev=node1;
node2->next=node3; node3->prev=node2;
node3->next=node4; node4->prev=node3;
node4->next=head;
PrintList(head);
}
void Test02()
{
Listnode* head=InitList();
Listpushback(head, 1);
Listpushback(head, 2);
Listpushback(head, 3);
Listpushfront(head, 4);
PrintList(head);
Listpopback(head); PrintList(head);
Listpopfront(head); PrintList(head);
Listpopfront(head); PrintList(head);
Listpopfront(head); PrintList(head);
Listpopfront(head); PrintList(head);
}
void Test03()
{
Listnode* head = InitList();
Listpushback(head, 1);
Listpushback(head, 2);
Listpushback(head, 3);
Listpushback(head, 4);
Listinsert(Listfind(head, 4), 10);
PrintList(head);
Listdestroy(head);
head = NULL;
}
int main()
{
Test03();
return 0;
}
3. 完整代码展示
3.1 List.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int ListdataType;
typedef struct ListNode {
ListdataType data;
struct ListNode* next;
struct ListNode* prev;
}Listnode;
Listnode* BuyNode(ListdataType x);
Listnode* InitList();
void PrintList(Listnode* head);
void Listpushback(Listnode* head, ListdataType x);
void Listpushfront(Listnode* head, ListdataType x);
void Listpopback(Listnode* head);
void Listpopfront(Listnode* head);
Listnode* Listfind(Listnode* head, ListdataType x);
void Listinsert(Listnode* pos, ListdataType x);
void Listdelete(Listnode* pos);
void Listdestroy(Listnode* head);
3.2 List.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
Listnode* BuyNode(ListdataType x)
{
Listnode* node = (Listnode*)malloc(sizeof(Listnode));
if (node == NULL) {
perror("malloc error");
exit(1);
}
node->data = x;
node->prev = node->next = node;
return node;
}
void PrintList(Listnode* head)
{
Listnode* pcur = head->next;
while (pcur != head) {
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
Listnode* InitList()
{
Listnode* head = BuyNode(-1);
return head;
}
void Listpushback(Listnode* head, ListdataType x)
{
assert(head);
Listnode* newnode = BuyNode(x);
Listnode* pcur = head->prev;
newnode->prev = pcur;
newnode->next = head;
pcur->next = newnode;
head->prev = newnode;
}
void Listpushfront(Listnode* head, ListdataType x)
{
assert(head);
Listnode* newnode = BuyNode(x);
Listnode* pcur = head->next;
newnode->prev = head;
newnode->next = pcur;
pcur->prev = newnode;
head->next = newnode;
}
void Listpopback(Listnode* head)
{
assert(head && head->next != head);
Listnode* del = head->prev;
del->prev->next = head;
head->prev = del->prev;
free(del);
del = NULL;
}
void Listpopfront(Listnode* head)
{
assert(head && head->next != head);
Listnode* del = head->next;
del->next->prev = head;
head->next = del->next;
}
Listnode* Listfind(Listnode* head, ListdataType x)
{
Listnode* pcur = head->next;
while (pcur != head) {
if (pcur->data == x) {
printf("找到了\n");
return pcur;
}
pcur = pcur->next;
}
printf("链表中没有找到%d\n", x);
return NULL;
}
void Listinsert(Listnode* pos, ListdataType x)
{
assert(pos);
Listnode* newnode = BuyNode(x);
newnode->next = pos->next;
pos->next->prev = newnode;
newnode->prev = pos;
pos->next = newnode;
}
void Listdelete(Listnode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
void Listdestroy(Listnode* head)
{
assert(head);
Listnode* pcur = head->next;
while (pcur != head) {
Listnode* del = pcur;
pcur = pcur->next;
free(del);
del = NULL;
}
free(pcur);
pcur = head = NULL;
}
3.3 test.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
void Test01()
{
Listnode* head = NULL;
head = InitList();
Listnode* node1 = BuyNode(1);
Listnode* node2 = BuyNode(2);
Listnode* node3 = BuyNode(3);
Listnode* node4 = BuyNode(4);
head->next=node1; node1->prev=head;
node1->next=node2; node2->prev=node1;
node2->next=node3; node3->prev=node2;
node3->next=node4; node4->prev=node3;
node4->next=head;
PrintList(head);
}
void Test02()
{
Listnode* head=InitList();
Listpushback(head, 1);
Listpushback(head, 2);
Listpushback(head, 3);
Listpushfront(head, 4);
PrintList(head);
Listpopback(head); PrintList(head);
Listpopfront(head); PrintList(head);
Listpopfront(head); PrintList(head);
Listpopfront(head); PrintList(head);
Listpopfront(head); PrintList(head);
}
void Test03()
{
Listnode* head = InitList();
Listpushback(head, 1);
Listpushback(head, 2);
Listpushback(head, 3);
Listpushback(head, 4);
Listinsert(Listfind(head, 4), 10);
PrintList(head);
Listdestroy(head);
head = NULL;
}
int main()
{
Test03();
return 0;
}
4. 总结
双向链表(带头双向循环链表)是链表家族中功能最强大的成员。它通过引入哨兵位和双向指针,使得几乎所有操作(头插、头删、尾插、尾删、指定位置插入删除)都能在 O(1) 时间内完成,代码逻辑也更加简洁统一。
然而,没有完美的数据结构。双向链表牺牲了随机访问的能力,且每个节点需要额外存储两个指针,内存开销比单链表更大。在实际开发中,我们需要根据具体需求权衡利弊,选择最合适的数据结构。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online