当前位置: 首页 > news >正文

S4双向链表

2.3双向链表

2.3.1 双向链表定义

双向链表(Doubly Linked List)是一种更复杂的链式数据结构,它的每个节点都包含两个指针,分别指向直接前驱节点和直接后继节点。这种双向连接的特性使得链表可以双向遍历,解决了单链表只能单向遍历的限制。

核心结构对比

特性对比

单向链表

双向链表

指针数量

1个(next)

2个(prev + next)

遍历方向

只能从头到尾

双向遍历(前向+后向)

前驱访问

O(n)时间复杂度

O(1)时间复杂度

空间开销

较小

较大(多一个指针)

操作复杂度

相对简单

稍复杂(需维护两个指针)

2.3.2 双向链表的设计与实现

节点结构定义:

typedef int ELEM_TYPE;typedef struct ListNode {ELEM_TYPE val;ListNode* prior;ListNode* next;} ListNode;typedef struct LinkList { //头结点ListNode* head;int cursize;} LinkList;

代码解读

  • 每个节点包含三个部分:数据域、前驱指针(prior)、后继指针(next)
  •  Li  nkList 结构体封装头指针和大小信息,便于管理
  • 循环特性:尾节点的next指向头节点,头节点的prior指向尾节点
  • 带头节点:头节点不存储有效数据,作为哨兵节点简化操作

1. 初始化函数 ( In  itList )

void InitList(LinkList* plist) {assert(plist != NULL);ListNode* p = buynode();if (p == NULL) return;p->val = 0;p->prior = p; // 前驱指向自己p->next = p; // 后继指向自己plist->head = p;plist->cursize = 0;}

关键

  • 创建头节点并建立自环结构: p-  >prior = p; p->next = p; 
  • 空的双向循环链表就是头节点自己指向自己
  • 这种设计使得插入和删除操作的边界条件处理统一

2. 节点创建函数 ( bu  ynode )

ListNode* buynode(ELEM_TYPE val) {ListNode* p = (ListNode*)malloc(sizeof(ListNode));if (p == NULL) return NULL;p->val = val;p->prior = NULL;p->next = NULL;return p;}

3. 按位置查找节点 ( Fi  ndPos )

ListNode* FindPos(const LinkList* plist, int pos) {assert(plist != NULL);if (pos < 0 || pos > plist->cursize || Is_Empty(plist)) {printf("位置不符或链表为空\n");return NULL;}ListNode* p = plist->head;while (pos--) {p = p->next;}return p;}

关键

  • 位置约定:pos=0返回头节点,pos=1返回第一个数据节点
  • 循环遍历直到找到目标位置
  • 时间复杂度O(n)

4. 插入操作函数群

在指定节点后插入 ( In  sertNext )

bool InsertNext(LinkList* plist, ListNode* ptr, ELEM_TYPE val) {assert(plist != NULL);if (ptr == NULL) { return false; }ListNode* p = buynode();if (p == NULL) return false;p->val = val;// 关键指针操作p->prior = ptr;p->next = ptr->next;ptr->next = p;p->next->prior = p; // 原ptr->next节点的前驱指向新节点// 如果插入在尾节点后,需要更新头节点的前驱指向if (Is_Empty(plist) || (ptr == plist->head->prior)) {plist->head->prior = p;}plist->cursize++;return true;}

图解插入过程


插入前: A <--> C在A后插入B: A <--> B <--> C步骤:1. B->prior = A2. B->next = A->next (即C)3. A->next = B4. C->prior = B (即B->next->prior = B)

在指定节点前插入 ( In  sertPrev )

bool InsertPrev(LinkList* plist, ListNode* ptr, ELEM_TYPE val) {assert(plist != NULL);ListNode* newNode = buynode();newNode->val = val;newNode->next = ptr;newNode->prior = ptr->prior;ptr->prior = newNode;newNode->prior->next = newNode; // 原ptr->prior节点的后继指向新节点plist->cursize++;return true;}

关键:双向链表的优势体现,前插操作也是O(1)时间复杂度

头插法和尾插法

// 头插法:在头节点后插入bool Push_Front(LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);return InsertNext(plist, plist->head, val);}// 尾插法:在尾节点后插入bool Push_Back(LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);ListNode* tail = plist->head->prior; // 直接获取尾节点return InsertNext(plist, tail, val);}

关键

  • 头插法和尾插法的时间复杂度都是O(1)
  • 双向循环链表的尾节点可以通过 he  ad->prior 直接获得,无需遍历

5. 删除操作函数群

删除指定节点的后继节点 ( De  lNext )

bool DelNext(LinkList* plist, ListNode* ptr) {assert(plist != NULL);if (plist->cursize <= 0) return false;if (plist == NULL || ptr == NULL) return false;ListNode* p = ptr->next;ptr->next = p->next;p->next->prior = ptr;// 如果删除的是尾节点,需要更新头节点的前驱指向if (p->next == plist->head) {plist->head->prior = ptr;}free(p);p = NULL;plist->cursize--;return true;}

图解删除过程


删除前: A <--> B <--> C删除B: A <--> C步骤:1. A->next = B->next (即C)2. C->prior = B->prior (即A)3. free(B)

头删法和尾删法

// 头删法:删除头节点后的第一个节点bool Pop_Front(LinkList* plist) {return DelNext(plist, plist->head);}// 尾删法:删除尾节点bool Pop_Back(LinkList* plist) {assert(plist != NULL);// 删除尾节点等价于删除尾节点的前驱节点的后继return DelNext(plist, plist->head->prior->prior);}

关键

  • 头删法和尾删法的时间复杂度都是O(1)
  • 尾删法通过 he  ad->prior->prior 直接找到倒数第二个节点

6. 查找函数

按值查找 ( Fi  ndValue )

ListNode* FindValue(const LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);ListNode* p = plist->head->next;// 循环遍历,遇到头节点说明遍历完成while (p != plist->head) {if (p->val == val) return p;p = p->next;}return NULL;}

关键

  • 遍历的终止条件是 p   != plist->head (不是NULL)
  • 时间复杂度O(n)

7. 清空与销毁函数

清空链表 ( Cl  earList )

void ClearList(LinkList* plist) {assert(plist != NULL);ListNode* p = plist->head->next;while (p != plist->head) {ListNode* n = p;p = p->next;free(n);n = NULL;}// 恢复头节点的自环状态plist->head->prior = plist->head;plist->head->next = plist->head;plist->cursize = 0;}

销毁链表 ( De  stroyList )

void DestroyList(LinkList* plist) {assert(plist != NULL);ListNode* p = plist->head->next;// 先释放所有数据节点while (p != plist->head) {ListNode* n = p;p = p->next;free(n);n = NULL;}// 再释放头节点free(plist->head);plist->head = NULL;plist->cursize = 0;}

2.3.3 双向循环链表的优势总结

操作

时间复杂度

关键要点

初始化

O(1)

创建头节点并建立自环

插入操作

O(1)

需要维护两个方向的指针

删除操作

O(1)

需要维护两个方向的指针

按值查找

O(n)

需要遍历整个链表

头尾操作

O(1)

双向循环链表的优势体现

核心优势

  1. 双向遍历能力:支持前向和后向遍历,灵活性极高
  1. 操作效率高:插入、删除、头尾操作都是O(1)时间复杂度
  1. 边界统一:循环结构使得头尾操作逻辑统一,代码简洁
  1. 空间利用率:相对于性能提升,额外的指针开销是可接受的

适用场景

  • 需要频繁在链表两端进行插入删除的操作
  • 需要双向遍历的应用程序(如浏览器历史记录)
  • 实现双向队列(Deque)等高级数据结构
  • 需要循环缓冲区的场景
http://www.dtcms.com/a/507748.html

相关文章:

  • h5模板是什么网站开发好什么进行界面的优化
  • 淘宝做短视频网站自学网页设计的网站
  • 【含文档+PPT+源码】基于SpringBoot+Vue的校园电子设备租赁系统
  • 渗透测试快速启动工具箱
  • 在 go-zero 中优雅使用 Google Wire 实现依赖注入
  • Springboot3.5.6 + jdk21使用第三方xjar加密
  • [go 面试] 构建高效微服务通信:选择合适的通信方式
  • 推荐营销型网站建设做电影资源网站手机版
  • 厦门网站设计排行网站建设培训费用多少
  • 哪个网站做图文素材多项目投资网
  • Python3编程之面向对象
  • 【AI4S】从直觉到人工智能:药物发现中的小分子表征演变
  • docker compose 2.33版本安装
  • 在线解决window和linux对linux远程问题
  • 公司企业做网站好做吗深圳市网站备案
  • rabbitmq的多交换机(扇出为例)监听实现
  • 做网站那个好做淘宝联盟网站要多少钱
  • 4A 架构(业务架构、数据架构、应用架构、技术架构)在智慧电网中的实战:从边缘 AI 到云边协同的代码级拆解
  • django 做网站wordpress使用七牛防止降权
  • 定制建站方案当当网站建设的目标
  • 阿里云创建交换分区、设置内存监控预警和自动处理内存占用过大进程的脚本
  • 网站建设费用做什么科目思明自助建站软件
  • php网站开发实战教程app和网站开发语言的区别
  • 引领未来交易:达普韦伯全链路Swap交易所系统开发解决方案
  • 2.CUDA编程模型
  • YOLOV4
  • MES系统如何实现生产过程数据采集与管控?
  • 医保局网站建设dw网页设计作品简单
  • 网站如何更换空间wordpress镜像什么意思
  • 使用Yum安装Redis