跳到主要内容
数据结构实战:带头双向循环链表详解与实现 | 极客日志
C 算法
数据结构实战:带头双向循环链表详解与实现 综述由AI生成 双向链表通过前驱和后继指针实现双向遍历,带头节点简化边界处理。涵盖初始化、插入删除、查找销毁等核心操作,对比顺序表差异,并结合移除元素与反转链表算法题进行实战演练,提供完整 C 语言代码实现。重点解析了指针操作细节及内存管理注意事项。
漫步 发布于 2026/3/15 更新于 2026/5/3 5 浏览数据结构实战:带头双向循环链表详解与实现
1. 概念与结构
双向链表是一种链式存储的数据结构,每个节点包含两个指针:一个指向前驱节点(prev),一个指向后继节点(next),同时包含数据域(data)存储数据。这种结构允许双向遍历(从头到尾或从尾到头),并支持更灵活的插入、删除操作,但相比单链表会增加一定的空间开销。
图示:不带头双向循环链表
双向链表不仅能找到当前节点的下一个节点,还可以找到上一个节点,使用非常灵活。为了简化边界处理(如头插、尾删时不需要特殊判断空指针),我们通常采用带头双向循环链表 。其核心特性是头节点不存有效数据(哨兵位),且首尾相连形成闭环。
结构定义
typedef int type;
typedef struct ListNode {
type data;
struct ListNode * prev ;
struct ListNode * next ;
} ListNode;
2. 核心操作实现
初始化
初始化时需要创建头节点,并将其 next 和 prev 都指向自己,形成自循环。
void LTInit (ListNode** h) {
ListNode* ph = (ListNode*)malloc (sizeof (ListNode));
if (ph == NULL ) {
perror("malloc fail!" );
exit (1 );
}
*h = ph;
(*h)->data = -1 ;
(*h)->next = *h;
(*h)->prev = *h;
}
这里要注意,传入的是二级指针,因为我们需要修改外部传入的头节点指针本身。初始化后,链表状态就是 h <-> h。
辅助函数:创建节点 在插入操作中,我们需要频繁创建新节点。创建一个独立的辅助函数能让主逻辑更清晰。
ListNode* LTCreate (type x) {
ListNode* ph = (ListNode*)malloc (sizeof (ListNode));
if (ph == NULL ) {
perror("malloc fail!" );
exit (1 );
}
ph->data = x;
ph->next = ph;
ph->prev = ph;
return ph;
}
尾插与头插 对于带头双向循环链表,尾插可以直接通过头节点的 prev 指针定位到尾部,无需遍历,时间复杂度为 O(1)。
void LTPushBack (ListNode* h, type x) {
ListNode* p = LTCreate(x);
p->next = h;
p->prev = h->prev;
h->prev->next = p;
h->prev = p;
}
void LTPushFront (ListNode* h, type x) {
ListNode* p = LTCreate(x);
p->next = h->next;
p->prev = h;
h->next->prev = p;
h->next = p;
}
删除操作 bool LTEmpty (ListNode* phead) {
assert(phead);
return phead->next == phead;
}
由于是循环链表,尾节点即 h->prev。删除时需修复前驱节点的指针。
void LTPopBack (ListNode* h) {
if (LTEmpty(h)) return ;
ListNode* p = h->prev;
h->prev = p->prev;
p->prev->next = h;
free (p);
}
void LTPopFront (ListNode* h) {
if (LTEmpty(h)) {
printf ("链表为空,无法头删\n" );
return ;
}
ListNode* p = h->next;
h->next = p->next;
p->next->prev = h;
free (p);
}
销毁 void LTDestroy (ListNode* h) {
if (LTEmpty(h)) {
free (h);
return ;
}
ListNode* p = h->next;
while (p != h) {
ListNode* pr = p;
p = p->next;
free (pr);
}
free (h);
h = NULL ;
}
3. 进阶操作
查找 ListNode* LTFind (ListNode* h, type x) {
if (LTEmpty(h)) return NULL ;
ListNode* p = h->next;
while (p != h) {
if (p->data == x) return p;
p = p->next;
}
return NULL ;
}
指定位置插入 分为在节点 pos 之后插入和在 pos 之前插入。
void LTInsert (ListNode* pos, type x) {
assert(pos);
ListNode* p = pos->next;
ListNode* newnode = LTCreate(x);
pos->next = newnode;
newnode->prev = pos;
newnode->next = p;
p->prev = newnode;
}
void LTInsertFront (ListNode* h, ListNode* pos, type x) {
if (LTEmpty(h)) return ;
ListNode* p = h;
while (p->next != h) {
if (p->next == pos) break ;
p = p->next;
}
if (p->next == h) return ;
ListNode* newnode = LTCreate(x);
ListNode* pr = p->next;
newnode->next = pr;
newnode->prev = p;
p->next = newnode;
pr->prev = newnode;
}
指定位置删除 void LTErase (ListNode* pos) {
assert(pos);
ListNode* p = pos->prev;
p->next = pos->next;
pos->next->prev = p;
free (pos);
pos = NULL ;
}
4. 顺序表与链表对比 特性 顺序表 链表 存储方式 连续内存 离散内存 + 指针 随机访问 O(1),支持下标 O(n),需遍历 插入删除 需移动元素,O(n) 仅需改指针,O(1) 空间占用 固定大小或扩容 动态分配,有指针开销
结论 :顺序表适合频繁随机访问、数据量固定的场景;链表适合频繁插入删除、数据量动态变化的场景。
5. 算法题实战
移除链表元素 思路 :遍历链表,构建一个新链表只保留不等于 val 的节点,或者原地修改指针。这里演示构建新链表的方法,逻辑更直观。
struct ListNode* removeElements (struct ListNode* head, int val) {
struct ListNode *h = NULL , *pr = NULL ;
struct ListNode *p = head;
while (p) {
if (p->val != val) {
if (h == NULL ) {
h = p;
pr = p;
} else {
pr->next = p;
pr = p;
}
}
p = p->next;
}
if (pr) pr->next = NULL ;
return h;
}
反转链表 思路 :迭代法,使用三个指针 prev, curr, next 逐次反转指向。
struct ListNode* reverseList (struct ListNode* head) {
struct ListNode *s1 = NULL ;
struct ListNode *s2 = head;
struct ListNode *s3 = NULL ;
while (s2) {
s3 = s2->next;
s2->next = s1;
s1 = s2;
s2 = s3;
}
return s1;
}
6. 完整代码参考 #include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int type;
typedef struct ListNode {
type data;
struct ListNode * prev ;
struct ListNode * next ;
} ListNode;
void LTInit (ListNode** h) ;
void LTPushBack (ListNode* h, type x) ;
ListNode* LTCreate (type x) ;
void LTPushFront (ListNode* h, type x) ;
void LTPopBack (ListNode* h) ;
void LTPopFront (ListNode* h) ;
void LTDestroy (ListNode* h) ;
void print (ListNode* h) ;
ListNode* LTFind (ListNode* h, type x) ;
void LTInsert (ListNode* pos, type x) ;
void LTInsertFront (ListNode* h, ListNode* pos, type x) ;
void LTErase (ListNode* pos) ;
#include "1.h"
void LTInit (ListNode** h) {
ListNode* ph = (ListNode*)malloc (sizeof (ListNode));
if (ph == NULL ) { perror("malloc fail!" ); exit (1 ); }
*h = ph;
(*h)->data = -1 ;
(*h)->next = *h;
(*h)->prev = *h;
}
ListNode* LTCreate (type x) {
ListNode* ph = (ListNode*)malloc (sizeof (ListNode));
if (ph == NULL ) { perror("malloc fail!" ); exit (1 ); }
ph->data = x;
ph->next = ph;
ph->prev = ph;
return ph;
}
void LTPushBack (ListNode* h, type x) {
ListNode* p = LTCreate(x);
p->next = h;
p->prev = h->prev;
h->prev->next = p;
h->prev = p;
}
void LTPushFront (ListNode* h, type x) {
ListNode* p = LTCreate(x);
p->next = h->next;
p->prev = h;
h->next->prev = p;
h->next = p;
}
bool LTEmpty (ListNode* phead) {
assert(phead);
return phead->next == phead;
}
void LTPopBack (ListNode* h) {
if (LTEmpty(h)) return ;
ListNode* p = h->prev;
h->prev = p->prev;
p->prev->next = h;
free (p);
}
void LTPopFront (ListNode* h) {
if (LTEmpty(h)) { printf ("链表为空,无法头删\n" ); return ; }
ListNode* p = h->next;
h->next = p->next;
p->next->prev = h;
free (p);
}
void LTDestroy (ListNode* h) {
if (LTEmpty(h)) { free (h); return ; }
ListNode* p = h->next;
while (p != h) {
ListNode* pr = p;
p = p->next;
free (pr);
}
free (h);
h = NULL ;
}
void print (ListNode* h) {
if (LTEmpty(h)) return ;
ListNode* p = h->next;
while (p != h) {
printf ("%d " , p->data);
p = p->next;
}
printf ("\n" );
}
ListNode* LTFind (ListNode* h, type x) {
if (LTEmpty(h)) return NULL ;
ListNode* p = h->next;
while (p != h) {
if (p->data == x) return p;
p = p->next;
}
return NULL ;
}
void LTInsert (ListNode* pos, type x) {
assert(pos);
ListNode* p = pos->next;
ListNode* newnode = LTCreate(x);
pos->next = newnode;
newnode->prev = pos;
newnode->next = p;
p->prev = newnode;
}
void LTInsertFront (ListNode* h, ListNode* pos, type x) {
if (LTEmpty(h)) return ;
ListNode* p = h;
while (p->next != h) {
if (p->next == pos) break ;
p = p->next;
}
if (p->next == h) return ;
ListNode* newnode = LTCreate(x);
ListNode* pr = p->next;
newnode->next = pr;
newnode->prev = p;
p->next = newnode;
pr->prev = newnode;
}
void LTErase (ListNode* pos) {
assert(pos);
ListNode* p = pos->prev;
p->next = pos->next;
pos->next->prev = p;
free (pos);
pos = NULL ;
}
#include "1.h"
void test () {
ListNode* h;
LTInit(&h);
LTPushBack(h, 10 );
LTPushBack(h, 15 );
LTPushBack(h, 111 );
print(h);
LTPushFront(h, 2 );
LTPushFront(h, 12 );
print(h);
LTPopBack(h);
print(h);
LTPopFront(h);
print(h);
ListNode* p = LTFind(h, 10 );
LTInsert(p, 100 );
LTInsert(p, 200 );
LTErase(p);
print(h);
LTDestroy(h);
}
int main () {
test();
return 0 ;
}
相关免费在线工具 加密/解密文本 使用加密算法(如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