news 2026/5/11 20:42:26

双向链表:高效遍历与插入删除

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
双向链表:高效遍历与插入删除

引言

在前面的文章中,我们详细讲解了单向链表。单向链表虽然结构简单,但存在一个天然缺陷:只能单向遍历,无法从后往前访问。这在某些场景下(如需要双向查找、在任意位置前后插入删除)会造成不便。

双向链表(Doubly Linked List)通过为每个节点增加一个prev指针,实现了向前和向后两个方向的遍历能力。本文将系统讲解双向循环链表的原理与实现,并对比单向链表与双向链表的设计差异。

第一部分:双向循环链表的设计原理

一、数据结构定义

// 节点结构体 typedef struct DuList { int val; // 数据域 struct DuList* prev; // 前驱指针 struct DuList* next; // 后继指针 } DuList; // 链表管理结构体(带头结点) typedef struct DuLinkList { struct DuList* head; // 头结点指针 size_t cursize; // 元素个数 } DuLinkList;

二、关键设计:循环结构

空链表的状态

有元素的状态

三、判空条件

// 双向循环链表的判空 bool IsEmpty(const DuLinkList* dps) { return dps->head->next == dps->head; // 或者 // return dps->head->prev == dps->head; // 或者 // return dps->cursize == 0; }
链表类型空表判断
单向不带头结点head == NULL
单向带头结点head->next == NULL
双向循环带头结点head->next == head

第二部分:初始化与节点管理

一、创建节点

DuList* Buynode(ElemType val) { DuList* p = (DuList*)malloc(sizeof(DuList)); if (p == NULL) return NULL; p->prev = NULL; p->next = NULL; p->val = val; return p; }

二、初始化链表

void InitDuLinkList(DuLinkList* dps) { assert(dps != NULL); // 创建头结点(存储的值可任意,一般存 0) DuList* head = Buynode(0); if (head == NULL) return; // 循环指向自己 head->next = head; head->prev = head; dps->cursize = 0; dps->head = head; }

初始化后的状态

第三部分:插入操作

一、核心插入逻辑:在指定节点前插入

双向链表的插入有一个通用操作:在指定节点ptr前插入新节点。掌握了这个操作,头插、尾插都可以通过它来实现。

bool InsertElem(DuLinkList* dps, DuList* ptr, ElemType val) { assert(dps != NULL && ptr != NULL); // 1. 创建新节点 DuList* newnode = Buynode(val); if (newnode == NULL) return false; // 2. 找到 ptr 的前驱节点 DuList* prev = ptr->prev; // 3. 连接新节点的前后指针 newnode->next = ptr; newnode->prev = prev; // 4. 更新前后节点对新节点的引用 ptr->prev = newnode; prev->next = newnode; dps->cursize++; return true; }

插入过程图解

二、头插法

// 在头结点之后插入(成为第一个实际元素) bool PushFront(DuLinkList* dps, ElemType val) { return InsertElem(dps, dps->head->next, val); }

逻辑:在head->next前插入,也就是插入到第一个实际元素之前。

三、尾插法

// 在头结点之前插入(成为最后一个实际元素) bool PushBack(DuLinkList* dps, ElemType val) { return InsertElem(dps, dps->head, val); }

逻辑:在head前插入。因为头结点是循环的,在头结点前插入就相当于在链表的末尾插入。

四、插入操作的对比总结

操作调用方式插入位置
头插InsertElem(dps, head->next, val)第一个实际元素之前
尾插InsertElem(dps, head, val)头结点之前(即末尾)
任意位置InsertElem(dps, ptr, val)指定节点之前

设计优点:所有插入操作都复用同一个函数,无需为头插尾插编写重复代码。

第四部分:删除操作

一、删除指定节点(核心)

双向链表删除节点的优势在于:不需要找前驱,直接通过ptr->prevptr->next就能完成。

bool EraseNode(DuLinkList* dps, DuList* ptr) { assert(dps != NULL && ptr != NULL); // 不能删除头结点 if (ptr == dps->head) return false; // 1. 获取前后节点 DuList* prev = ptr->prev; DuList* next = ptr->next; // 2. 绕过 ptr,连接前后节点 prev->next = next; next->prev = prev; // 3. 释放 ptr free(ptr); ptr = NULL; dps->cursize--; return true; }

删除过程图解

二、头删

bool PopFront(DuLinkList* dps) { assert(dps != NULL); if (dps->head->next == dps->head) return false; // 空表 return EraseNode(dps, dps->head->next); }

三、尾删

bool PopBack(DuLinkList* dps) { assert(dps != NULL); if (dps->head->prev == dps->head) return false; // 空表 return EraseNode(dps, dps->head->prev); }

四、删除操作对比

链表类型删除指定节点需要找前驱时间复杂度
单向链表需要前驱指针✅ 需要O(n)
双向链表只需节点本身❌ 不需要O(1)

第五部分:遍历操作

一、正向遍历(从头到尾)

void PrintDuList(const DuLinkList* dps) { assert(dps != NULL); // 从第一个实际节点开始,遍历直到回到头结点 for (DuList* p = dps->head->next; p != dps->head; p = p->next) { printf("%d ", p->val); } printf("\n"); }

遍历路径

二、反向遍历(从尾到头)

void PrintReversely(const DuLinkList* dps) { assert(dps != NULL); // 从最后一个实际节点开始,反向遍历直到回到头结点 for (DuList* p = dps->head->prev; p != dps->head; p = p->prev) { printf("%d ", p->val); } printf("\n"); }

遍历路径

这是双向链表相比单向链表的独特优势——可以反向遍历


第六部分:双向链表 vs 单向链表对比

一、核心对比表

对比项单向链表双向链表
节点结构data + nextdata + prev + next
内存开销每节点 1 个指针每节点 2 个指针
正向遍历✅ O(n)✅ O(n)
反向遍历❌ 不支持✅ O(n)
在指定节点后插入O(1)O(1)
在指定节点前插入O(n)(需找前驱)O(1)(直接 prev)
删除指定节点O(n)(需找前驱)O(1)(直接 prev)
实现复杂度简单稍复杂

二、何时选择双向链表

第八部分:完整实现

头文件

// Dulist.h #pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> typedef int ElemType; typedef struct DuList { ElemType val; struct DuList* prev; struct DuList* next; } DuList; typedef struct DuLinkList { DuList* head; size_t cursize; } DuLinkList; // 函数声明 void InitDuLinkList(DuLinkList* dps); bool InsertElem(DuLinkList* dps, DuList* ptr, ElemType val); void PrintDuList(const DuLinkList* dps); void PrintReversely(const DuLinkList* dps); bool EraseNode(DuLinkList* dps, DuList* ptr); bool PushFront(DuLinkList* dps, ElemType val); bool PushBack(DuLinkList* dps, ElemType val); bool PopFront(DuLinkList* dps); bool PopBack(DuLinkList* dps); bool IsEmpty(const DuLinkList* dps); size_t GetSize(const DuLinkList* dps); void Clear(DuLinkList* dps); void Destroy(DuLinkList* dps);

c文件

// Dulist.c #include "Dulist.h" // 创建节点 DuList* Buynode(ElemType val) { DuList* p = (DuList*)malloc(sizeof(DuList)); if (p == NULL) return NULL; p->prev = NULL; p->next = NULL; p->val = val; return p; } // 初始化 void InitDuLinkList(DuLinkList* dps) { assert(dps != NULL); DuList* head = Buynode(0); if (head == NULL) return; head->next = head; head->prev = head; dps->cursize = 0; dps->head = head; } // 判空 bool IsEmpty(const DuLinkList* dps) { assert(dps != NULL); return dps->head->next == dps->head; } // 获取大小 size_t GetSize(const DuLinkList* dps) { assert(dps != NULL); return dps->cursize; } // 核心:在指定节点前插入 bool InsertElem(DuLinkList* dps, DuList* ptr, ElemType val) { assert(dps != NULL && ptr != NULL); DuList* newnode = Buynode(val); if (newnode == NULL) return false; // 直接通过 ptr->prev 获取前驱节点(核心优势) DuList* prev = ptr->prev; newnode->next = ptr; newnode->prev = prev; ptr->prev = newnode; prev->next = newnode; dps->cursize++; return true; } // 头插 bool PushFront(DuLinkList* dps, ElemType val) { return InsertElem(dps, dps->head->next, val); } // 尾插 bool PushBack(DuLinkList* dps, ElemType val) { return InsertElem(dps, dps->head, val); } // 删除指定节点 bool EraseNode(DuLinkList* dps, DuList* ptr) { assert(dps != NULL && ptr != NULL); if (ptr == dps->head) return false; // 不能删除头结点 DuList* prev = ptr->prev; DuList* next = ptr->next; prev->next = next; next->prev = prev; free(ptr); dps->cursize--; return true; } // 头删 bool PopFront(DuLinkList* dps) { assert(dps != NULL); if (IsEmpty(dps)) return false; return EraseNode(dps, dps->head->next); } // 尾删 bool PopBack(DuLinkList* dps) { assert(dps != NULL); if (IsEmpty(dps)) return false; return EraseNode(dps, dps->head->prev); } // 正向打印 void PrintDuList(const DuLinkList* dps) { assert(dps != NULL); for (DuList* p = dps->head->next; p != dps->head; p = p->next) { printf("%d ", p->val); } printf("\n"); } // 反向打印 void PrintReversely(const DuLinkList* dps) { assert(dps != NULL); for (DuList* p = dps->head->prev; p != dps->head; p = p->prev) { printf("%d ", p->val); } printf("\n"); } // 清空(保留头结点) void Clear(DuLinkList* dps) { assert(dps != NULL); while (!IsEmpty(dps)) { EraseNode(dps, dps->head->next); } } // 销毁(释放所有节点,包括头结点) void Destroy(DuLinkList* dps) { Clear(dps); free(dps->head); dps->head = NULL; dps->cursize = 0; }

总结

一、双向链表的核心原理

二、双向链表的两大优势

优势说明
O(1) 删除不需要找前驱,直接通过ptr->prev完成
双向遍历既可以正向也可以反向遍历

三、关键记忆点

操作实现方式时间复杂度
初始化创建头结点,prev/next 指向自己O(1)
头插InsertElem(dps, head->next, val)O(1)
尾插InsertElem(dps, head, val)O(1)
删除EraseNode(dps, ptr)O(1)
头删EraseNode(dps, head->next)O(1)
尾删EraseNode(dps, head->prev)O(1)
判空head->next == headO(1)
遍历for(p=head->next; p!=head; p=p->next)O(n)

四、一句话记忆

双向循环链表让头结点的 next 和 prev 都指向自己形成闭环,插入时通过调整前后节点的 prev/next 指针完成,删除时直接通过ptr->prev获取前驱节点,实现了 O(1) 的删除操作。

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

实战CANopen --- [2] Python-CANopen库核心功能与应用解析

1. Python-CANopen库基础与环境搭建 如果你正在寻找一个能快速上手CANopen协议开发的Python工具&#xff0c;christiansandberg开发的canopen库绝对是首选。这个开源项目不仅完整实现了CANopen协议栈&#xff0c;还提供了非常友好的Python API接口。我在工业自动化项目中使用这…

作者头像 李华
网站建设 2026/5/11 20:37:13

5步终极指南:免费绕过iOS 15-16激活锁的完整方案

5步终极指南&#xff1a;免费绕过iOS 15-16激活锁的完整方案 【免费下载链接】applera1n icloud bypass for ios 15-16 项目地址: https://gitcode.com/gh_mirrors/ap/applera1n 忘记Apple ID密码或购买二手设备后遇到iCloud激活锁&#xff1f;iOS激活锁绕过不再遥不可及…

作者头像 李华
网站建设 2026/5/11 20:34:08

阴阳师御魂自动挂机终极指南:5分钟快速实现智能护肝

阴阳师御魂自动挂机终极指南&#xff1a;5分钟快速实现智能护肝 【免费下载链接】yysScript 阴阳师脚本 支持御魂副本 双开 项目地址: https://gitcode.com/gh_mirrors/yy/yysScript 还在为阴阳师御魂副本的重复刷取感到疲惫吗&#xff1f;yysScript智能护肝脚本为你提供…

作者头像 李华
网站建设 2026/5/11 20:34:03

Windows任务栏透明美化终极指南:从新手到专家的完整使用手册

Windows任务栏透明美化终极指南&#xff1a;从新手到专家的完整使用手册 【免费下载链接】TranslucentTB A lightweight utility that makes the Windows taskbar translucent/transparent. 项目地址: https://gitcode.com/gh_mirrors/tr/TranslucentTB 想要让Windows桌…

作者头像 李华