news 2026/10/3 20:12:00

双向链表实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
双向链表实现

今天学习双向为了方便理解,放个图:

head带头链表⾥的头结点,实际为“哨兵位”,哨兵位结点不存储任何有效元素,只是站在这⾥“放哨的”

双向链表:解决单链表的单向痛点

一、为什么需要双向链表

单链表只有一个next指针,只能从头到尾单向遍历。如果想找某个结点的前驱结点,或者想从尾部往前倒序访问,单链表就只能从头开始遍历,时间复杂度直接变成 O (N),在频繁需要双向操作的场景里效率很低。

双向链表就是在单链表的基础上,给每个结点多加了一个指向前驱的指针,让链表可以双向行走,补上了单链表的短板。

二、双向链表的结构

1. 结点组成

每个结点包含三部分:

  • 数据域:存储具体的业务数据
  • next指针:指向后一个结点
  • prev指针:指向前一个结点

对应的 C 语言结构体定义:

typedef int LTDataType; typedef struct ListNode { struct ListNode* next; // 指向后一个结点 struct ListNode* prev; // 指向前一个结点 LTDataType data; // 存储的数据 } LTNode;

2. 实际最常用:带头双向循环链表

工程里几乎不会用裸的双向链表,最常用的是带头双向循环链表。这里有两个核心概念:

  • 带头(哨兵位):在链表最前面加一个不存有效数据的 “哨兵结点”。它的存在是为了简化边界操作,不用单独处理头插、头删时的空指针问题。
  • 循环:链表尾结点的next指向哨兵位,哨兵位的prev指向尾结点,整个链表形成一个闭环。

这种结构看起来定义更复杂,但真正写代码的时候会发现,所有位置的插入、删除逻辑都完全统一了,不用区分头部、尾部、中间情况,反而实现起来更简洁。

三、双向链表的核心操作实现

下面以带头双向循环链表为例,梳理核心接口的实现思路。

1. 初始化

初始化就是创建一个哨兵位结点,让它的next和prev都指向自己,形成一个空的循环链表。

// 创造结点 listnode* creatnode(type x) { listnode* newnode = (listnode*)malloc(sizeof(listnode)); if (newnode == NULL) { perror("newnode"); exit(1); } newnode->date=x; newnode->next = newnode->prev = newnode; return newnode; }

2. 判空

判断链表有没有有效数据,只需要看哨兵位的next是不是还指向自己。

bool LTEmpty(LTNode* phead) { return phead->next == phead; }

3. 尾部插入

因为是循环结构,尾结点天然就是哨兵位的prev,所以直接在哨兵和原尾结点之间插入新结点即可,不需要遍历找尾。

void backinsert(listnode* phead, type x) { assert(phead); listnode* newnode = creatnode(x); newnode->next = phead; newnode->prev = phead->prev; phead->prev->next = newnode; phead->prev = newnode; }

头插逻辑同理,在哨兵位和第一个有效结点之间插入即可,时间复杂度都是 O (1)。

4. 指定位置之后插入

双向链表的插入核心是修改四个指针:新结点的prev和next,以及前后两个结点的next和prev。

// 在pos结点之后插入x void posback(listnode* pos, type x) { assert(pos); listnode* newnode = creatnode(x); newnode->next = pos->next; newnode->prev = pos; pos->next->prev = newnode; pos->next = newnode; }

有了这个通用插入函数,头插、尾插都可以直接复用:头插就是在哨兵位之后插,尾插就是在哨兵位的prev之后插。

5. 删除指定结点

删除pos结点,只需要把它前后两个结点直接连起来,然后释放pos即可。

void poserase(listnode* pos) { assert(pos); pos->next->prev = pos->prev; pos->prev->next = pos->next; free(pos); pos = NULL; }

同样,头删、尾删都可以复用这个函数,不用单独写边界逻辑。

6. 查找与遍历

查找从哨兵位的下一个结点开始遍历,直到回到哨兵位即为遍历结束。

listnode* find(listnode* phead, type x) { assert(phead); listnode* pcur = phead->next; while (pcur != phead) { if (pcur->date == x) { return pcur; } pcur = pcur->next; } return NULL; }

7. 销毁链表

遍历释放所有有效结点,最后把哨兵位也释放掉。

void destroy(listnode** phead) { listnode* pcur = (*phead)->next; while (pcur != *phead) { listnode* next = pcur->next; free(pcur); pcur = next; } free(*phead); *phead = NULL; }

四、双向链表的特点总结

  1. 优势
    • 支持双向遍历,找前驱和后继结点都很方便
    • 带头循环结构下,头尾的插入、删除都是 O (1),无需遍历
    • 任意位置的插入删除逻辑统一,边界情况少,代码更简洁
  2. 不足
    • 每个结点多了一个prev指针,相比单链表内存开销更大
    • 仍然不支持随机访问,按下标查找元素依然需要遍历,时间复杂度 O (N)
  3. 适用场景
    • 需要频繁在任意位置插入、删除元素
    • 需要双向遍历链表的业务场景
    • 对头部、尾部操作效率要求高的场

完整代码gittee:https://gitee.com/yang-mianmian-1/doubly-linked-list

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 20:07:50

PanDownload失效了?2026最新百度网盘满速下载直链脚本教程

平时在保存或获取资料的时候,许多朋友都会选择使用云端储存服务,但是偶尔也会遇到传输过程特别缓慢的情况,有时候等待半天进度条才前进一点点,让人感到非常困扰。其实传输表现不理想,并不一定都是服务提供方的原因&…

作者头像 李华
网站建设 2026/10/3 19:59:24

甘特图重温

一开始让Agent美化一下成这样子了,好像不太对劲,不是想要的效果。再来~~把自己的链接拿来用,发现控制台报一堆红,排查了一下,发现是没打包,效果如下和最终效果还是差蛮多的,还需完善~~

作者头像 李华