数据结构:双向链表(1)
前言
本篇文章将讲解双向链表概念与结构,实现双向链表,顺序表与链表的分析,链表算法题等知识的相关内容,其中双向链表概念与结构,实现双向链表,顺序表与链表的分析为本章节知识的内容,单链表的题,会在不久后更新。
一、双向链表概念与结构
双向链表概念
双向链表是一种链式存储的数据结构,每个节点包含两个指针:一个指向前驱节点(prior),一个指向后继节点(next),同时包含数据域(data)存储数据。这种结构允许双向遍历(从头到尾或从尾到头),并支持更灵活的插入、删除操作,但相比单链表会增加一定的空间开销(额外的指针域)。
图示:(不带头双向循环链表)

双向链表不仅能找到当前节点的下一个节点还可以找到当前节点的上一个节点,使用起来是很方便的。
因为刚刚讲解过的单链表为单向不带头不循环链表,目前还没有讲过带头形式的,所以本双链表为
带头双向循环链表。
带头双向循环链表
带头双向循环链表是一种兼具“头节点”“双向指针”“循环结构”三大特性的链表,是应用最广泛的双向链表类型。其结构稳定、边界处理简单,支持高效的插入、删除和双向遍历操作。
对于上面头结点讲解:
- 带头链中的头节点,是不存储任何有效数据,只用来站岗放哨,我们可称之为"哨兵位"
- 在单链表的学习中,我们有时候也会把第一个节点表述为头节点,其实这个称呼是不严谨的:
- 按照定义来说,严谨的定义:头节点是链表中第一个节点,但不存储有效数据(部分场景可存储链表长度等元信息),其核心价值是简化边界操作(如插入/删除首节点时无需特殊判断)。
双向链表结构
根据前文知识讲解,以及前面单向不带头不循环链表的知识,我们来实现下双向链表结构:
typedef int type;
typedef struct ListNode
{type data;//前驱指针,指向前一个指针struct ListNode* prev;//后继指针,指向后一个指针struct ListNode* next;
}ListNode;二、实现双向链表
1.双向链表的初始化
我们在双向链表中头节点(可叫哨兵位)是需要初始化一下的,数据域可以存任意的数据,前驱指针和后继指针都指向自己即可。
函数形式:
void LTInit(ListNode** 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* ph = (ListNode*)malloc(sizeof(ListNode));为头节点分配内存空间(哨兵位节点)。 if (ph == NULL) { perror("malloc fail!"); exit(1); }检查内存分配是否成功,失败则报错并终止程序。 *h = ph;将传入的二级指针 h指向新创建的头节点(即外部头指针指向哨兵位节点)。(*h)->data = -1;给哨兵位节点的 data赋值-1(通常无实际意义,仅作标记)。(*h)->next = *h;头节点的 next指针指向自身(形成循环)。(*h)->prev = *h;头节点的 prev指针指向自身(双向循环)。初始图示:
void test()
{ListNode* h;LTInit(&h);}int main()
{test();
}
2.双向链表的尾插
双向链表尾插是指在链表的 尾部(最后一个有效节点之后) 插入新节点。对于 带头节点的双向循环链表,尾插可直接通过头节点的 prev 指针定位尾节点,无需遍历链表,时间复杂度为 O(1)。
void LTPushBack(ListNode* h, type x)
不过,该函数为实现尾插入,需要插入一个新节点,但传入的参数为type,需要先将type类型转化为ListNode*类型,所以,应有下面函数:
创建节点
ListNode* LTcreat(type x)
ListNode* LTcreat(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 = LTcreat(x);p->next = h;p->prev = h->prev;h->prev->next = p;h->prev = p;
}讲解:
LTcreat函数:创建新节点(含哨兵位初始化)
- 功能:创建一个新节点,初始化
data为x,并让next和prev指针自指(形成循环)。- 用途:
- 可用于 初始化链表的哨兵位头节点(此时
x通常为无意义值,如-1);- 也可用于 创建普通有效节点(此时
x为实际数据)。
LTPushBack函数:尾插操作
- 核心逻辑:通过
LTcreat(x)创建新节点p,并插入到链表尾部(头节点h的prev位置)。- 步骤拆解:
ListNode* p = LTcreat(x);→ 创建新节点p(p->next和p->prev初始指向自身)。p->next = h;→ 新节点p的next指向头节点h(保持循环)。p->prev = h->prev;→ 新节点p的prev指向原尾节点(h->prev是原尾节点)。h->prev->next = p;→ 原尾节点的next指向新节点p。h->prev = p;→ 头节点h的prev指向新节点p(更新尾节点为p)。newNode->prev = h->prev; // 新节点 prev 指向原尾节点newNode->next = h; // 新节点 next 指向头节点h->prev->next = newNode; // 原尾节点 next 指向新节点h->prev = newNode; // 头节点 prev 更新为新节点(新尾节点)
图示:


3.双向链表的头插
头插是双向链表中最常用的操作之一,指将新节点插入到 头节点之后、第一个有效节点之前 的位置。适用于需频繁在头部添加数据的场景。
根据上面所说,我们可以知晓一点:插入到 头节点之后、第一个有效节点之前 的位置,所以实现上要考虑考虑:
void LTPushFront(ListNode* h, type x)
ListNode* LTcreat(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 LTPushFront(ListNode* h, type x)
{ListNode* p = LTcreat(x);p->next = h->next;p->prev = h;h->next->prev = p;h->next = p;
}细讲一下:
ListNode* LTcreat(type x) {// 1. 动态内存分配(关键:检查 malloc 失败场景)ListNode* ph = (ListNode*)malloc(sizeof(ListNode));if (ph == NULL) { // 工程化必备:处理内存分配失败perror("malloc fail!"); // 打印错误原因(如内存不足)exit(1); // 终止程序(或返回 NULL,视场景而定)}// 2. 初始化节点数据与指针ph->data = x; // 存储有效数据ph->next = ph; // 初始 next 指向自身(循环结构基础)ph->prev = ph; // 初始 prev 指向自身(循环结构基础)return ph; // 返回新节点地址 }向 带头节点的双向循环链表 头部插入新节点(头插)。
前提:链表已初始化(头节点h存在,且h->next和h->prev初始指向自身,即空链表状态)。完整头插流程(以空链表插入第一个节点为例)
假设头节点
h已通过LTcreat(-1)创建(-1为哨兵位无效数据),插入第一个有效节点x=10:
- 创建新节点:
ListNode* p = LTcreat(10);
LTcreat分配内存并初始化p->data=10,p->next=p,p->prev=p。- 插入新节点:
p->next = h->next; // 步骤1:p->next = h(因空链表 h->next = h) p->prev = h; // 步骤2:p->prev = h(头节点为 p 的前驱) h->next->prev = p; // 步骤3:h->prev = p(原 h->next 是 h,故 h->prev 指向 p) h->next = p; // 步骤4:h->next = p(头节点 next 指向 p,p 成为第一个有效节点)- 结果:链表变为
h <-> p(双向循环,p为唯一有效节点)。
图示:

4.双向链表的尾删
双向链表的尾删(删除链表最后一个有效节点)是链表操作的高频场景,其核心是 安全释放尾节点内存并修复前驱节点的指针关系。实现尾删之前,我们需要先实现一个判空的函数,如果链表为空则不能继续删除了:
尾删函数形式:
void LTPopBack(ListNode* h)
双向链表的判空
bool LTEmpty(ListNode* phead)
bool LTEmpty(ListNode* phead)
{assert(phead);return phead->next == phead;
}讲解:
bool LTEmpty(ListNode* phead) {assert(phead); // 确保传入的头节点指针非空(避免对 NULL 解引用)return phead->next == phead; // 直接通过指针关系判断 }
- 空链表:头节点的
next指针指向自身(phead->next == phead),此时链表无任何有效节点。- 非空链表:头节点的
next指向第一个有效节点(phead->next != phead)。
接下来回归正题:
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);
}讲解:
步骤1:判空——避免对空链表操作(核心安全检查!)
if (LTEmpty(h)) { return; } // 调用 LTEmpty 判断链表是否为空
- LTEmpty 逻辑:
return phead->next == phead(头节点的next指向自身,说明无有效节点)。- 作用:若链表为空(如
h <-> h),直接返回,避免后续p->prev访问空指针导致 程序崩溃。步骤2:定位尾节点——无需遍历,O(1) 效率!
ListNode* p = h->prev; // p 指向尾节点(C)
- 双向循环链表特性:头节点的
prev指针 直接指向尾节点(无需从h->next开始遍历),时间复杂度 O(1)(单向链表需 O(n),这是双向链表的核心优势)。步骤3:修复指针——确保链表循环关系不中断
h->prev = p->prev; // 步骤3.1:头节点的 prev 指向尾节点的前驱(B) p->prev->next = h; // 步骤3.2:尾节点前驱(B)的 next 指向头节点(h)
- 修复后链表结构:
h <-> A <-> B <-> h(尾节点 C 已从逻辑上“脱离”链表)。- 关键对比:若仅修改
h->prev = p->prev而不修改p->prev->next,会导致 B 的 next 仍指向 C,链表出现“断裂”(B <-> C <-> h <-> A <-> B),形成错误的循环子链。步骤4:释放内存——避免内存泄漏(必须!)
free(p); // 释放尾节点 C 的内存空间
尾删图示:


5.双向链表的头删
双向链表的头删指删除链表的第一个有效节点(即头节点后的第一个节点)。带头节点的链表可避免对空链表的特殊处理,实现更简洁。
根据前文所讲,我们可知其实现图示:


函数形式:
void LTPopFront(ListNode* h);
void LTPopFront(ListNode* h)
{if (LTEmpty(h) ){printf("链表为空,无法头删\n");return;}ListNode* p = h->next;h->next = p->next;p->next->prev = h;free(p);
}讲解:
核心代码逐行解析
1. 判空处理(避免操作无效链表)
if (h == NULL || h->next == h) {printf("链表为空,无法头删\n");return; }
h == NULL:检查头节点指针是否为空(链表未初始化)。h->next == h:检查链表是否为空(头节点的next指向自身,说明无有效节点)。- 处理逻辑:若满足任一条件,打印错误信息并退出函数,避免后续非法操作。
2. 记录待删除节点
ListNode* p = h->next; // p 指向第一个有效节点(待删除节点)
- 头节点
h的next指针指向链表的第一个有效节点,用p临时保存该节点地址,便于后续释放内存。3. 更新链表指针关系(断链与重连)
h->next = p->next; // 步骤1:头节点的next指向p的下一个节点(跳过p) p->next->prev = h; // 步骤2:p的下一个节点的prev指向头节点(反向指针同步)
- 步骤1:头节点
h不再指向p,而是直接指向p的下一个节点(p->next),完成“前向断链”。- 步骤2:
p的下一个节点(p->next)的prev指针从指向p改为指向头节点h,完成“反向断链”。- 效果:通过双向指针的更新,
p节点从链表中完全脱离。4. 释放内存(避免内存泄漏)
free(p); // 释放p节点的内存空间
- 手动释放
p指向的节点内存(C语言需显式管理内存,否则会导致内存泄漏)。
6.双向链表的销毁
双向链表的销毁需遍历所有节点并逐个释放内存,避免内存泄漏,顺序为:先销毁除了头结点(哨兵位)之外的所有节点,在最后释放头结点空间。
函数形式:
void LTDestory(ListNode* h);
void LTDestory(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;
}讲解:
按道理来说是要传入二级指针的,但是前面其它接口都用的一级,这里和初始化部分统一比较好点,我们可以在测试文件中最后销毁完手动将头节点置为空。
借助现有实现测试:
本代码通过三个文件来实现的:
首先:
1.h
#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* LTcreat(type x);
void LTPushFront(ListNode* h, type x);
void LTPopBack(ListNode* h);
void LTPopFront(ListNode* h);
void LTDestory(ListNode* h);
void print(ListNode* h);1.cpp
#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* LTcreat(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 = LTcreat(x);p->next = h;p->prev = h->prev;h->prev->next = p;h->prev = p;
}
void LTPushFront(ListNode* h, type x)
{ListNode* p = LTcreat(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 LTDestory(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");
}main.cpp
#include"1.h"
void test()
{ListNode* h;LTInit(&h);LTPushBack(h, 10); //10 LTPushBack(h, 15); //10 15 LTPushBack(h, 111); //10 15 111print(h);LTPushFront(h, 2); //2 10 15 111LTPushFront(h, 12); //12 2 10 15 111print(h);LTPopBack(h); // 12 2 10 15print(h);LTPopFront(h); //2 10 15print(h);LTDestory(h);
}int main()
{test();
}结果:

总结
以上就是今天要讲的内容,本篇文章涉及的知识点为:双向链表概念与结构,实现双向链表(双向链表的初始化,双向链表的尾插,双向链表的头插,双向链表的尾删,双向链表的头删)等知识的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,接下来的内容我会很快更新。


