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

数据结构入门 (四):双向通行的“高速公路” —— 深入双向循环链表

目录

  • 引言:打破“单行道”的束缚
  • 一、双向链表的核心节点操作
    • 1.插入操作
    • 2.删除操作
  • 二、构建双向循环链表
  • 三、双向循环链表C语言实现
    • 1.定义结构体与接口
    • 2.初始化链表
    • 3.核心辅助函数
    • 4.头插法
    • 5.尾插法
    • 6.展示链表
    • 7.删除链表中的节点
    • 8.销毁链表
    • 9.测试函数
  • 四、总结:工程领域的“链表之王”

引言:打破“单行道”的束缚

在之前的探索中,我们已经掌握了单向链表。但它有一个无法回避的痛点:它是一条“单行道”。一旦我们从一个节点走向下一个,那么就再也无法轻松地回头。这时候,如果我们需要查找某个节点的前驱,我们只能从头开始,重新遍历一遍。

这种“一去不返”的特性,让单向链表在许多需要灵活操作的工程场景中显得力不从心。我们不禁要问:能不能为这条路开辟一条逆行车道,让数据节点可以来去自如呢?

答案是肯定的!只需在每个节点中,除了next指针外,再添加一个指向前驱节点的prev指针。这样,我们就构建了一条双向链表。

实际应用中,我们多使用双向链表,它即可往前又可往后的特点,在进行操作时非常方便,比如在删除元素的时候,它不需要像单向链表一样找前置节点,这是因为它的节点结构包含两个指针域,在这个节点上就可以找到指向上一个节点的指针。

!图1

图1 双向链表的一个节点结构

但普通的双向链表在进行尾插操作时,依然会遇到与单向链表一样的问题:从头一步步走到尾再进行插入。所以我们效仿单向链表的解决方法,引入循环,将双向链表的头尾相连,构建出它的终极形态,也是我们这篇文章的重点——双向循环列表。它几乎是工程应用中最常用、最强大的链表形态。

一、双向链表的核心节点操作

双向链表的节点中同时包含两个指针,因此每次操作的时候,都要注意处理好这两个指针。

1.插入操作

想象一下,我们要在两个节点(节点1和节点3)之间,插入一个新的节点(new_node)。如图2。

在这里插入图片描述

图2 待插入的链表和新节点

对于插入操作,为了方便,我们首先给每个节点都放一个指针,分别为prevnew_nodenext

!图3

图3 为三个节点都赋予指针

整个过程需要4个步骤来修改指针,将new_node完美地接入到链表中。一个安全且不易出错的顺序是:先处理新节点本身,再修改原有链表

  1. new_node->prev = prev; // 步骤①:新节点的prev指向前驱节点。
  2. new_node->next = next; // 步骤②:新节点的next指向后继节点。
  3. prev->next = new_node; // 步骤③:前驱节点的next指向新节点。
  4. next->prev = new_node; // 步骤④:后继节点的prev指向新节点。
    在这里插入图片描述
图4 新节点的prev指向前驱节点

在这里插入图片描述

图5 新节点的next指向后继节点

在这里插入图片描述

图6 前驱节点的next指向新节点

!图4,5,6,7

图7 后继节点的prev指向新节点

这个操作是所有插入(头插、尾插、中插)的核心。我们可以将它封装成一个函数便于调用:

static void addDNode(DNode* new_node, DNode *prev, DNode* next)
{next->prev = new_node;new_node->next = next;new_node->prev = prev;prev->next = new_node;
}

2.删除操作

对于删除一个节点(节点2)的操作则相对简单一些,只需要让它的前驱(节点1)和后继(节点3)跳过节点2,直接相连即可。

这个过程只需要2个步骤:

  1. prev->next = next; // 步骤①:前驱的next指向后继。
  2. next->prev = prev; // 步骤②:后继的prev指向前驱。
    !图8
图8 删除节点2

我们同样将其封装成函数:

static void delDNode(DNode* prev, DNode* next)
{next->prev = prev;prev->next = next;
}

二、构建双向循环链表

掌握了对节点的核心操作后,我们来构建一个带头节点的双向循环链表。它的空链表形态是这样的:头结点headernextprev指针都指向它自己,形成一个最小的闭环。
双向循环链表的带头节点的空链表如下图所示。

!图9

图9 带头节点的双向循环空链表

在这个结构下,我们将用到之前封装的addDNode函数:

  • 头插:在 header 和 header->next (原第一个节点) 之间插入新节点。
insertHead(new_node, header, header->next)

在这里插入图片描述

图10 带头节点的双向循环空链表头插示意图
  • 尾插:在 header->prev (原最后一个节点) 和 header 之间插入新节点。
insertTail(new_node, header->prev, header)

!图11

图10 带头节点的双向循环链表尾插示意图

无论是头插还是尾插,我们都调用了同一个核心函数,区别只是传递不同的“前驱”和“后继”。这正是带头结点的循环结构带来的优雅与统一。

三、双向循环链表C语言实现

1.定义结构体与接口

#include <stdio.h>
#include <stdlib.h>typedef int Element;
typedef struct _node {Element val;struct _node *next;struct _node *prev;
} DNode, DList; // 我们起了两个别名防止理解错误/* 使用一个带头节点的双向循环链表,头节点让用户来管理,提供初始化接口 */
void initDList(DList *header);
void releaseDList(DList *header);// 实现头插、尾插
void insertDListHeader(DList *header, Element val);
void insertDListRear(DList *header, Element val);
// 显示链表
void showDList(const DList *header);
// 删除一个元素
void deleteDList(DList *header, Element e);

2.初始化链表

void initDList(DList* header)
{header->val = 0;header->next = header->prev = header;
}

3.核心辅助函数

// 核心插入函数 (static表示为内部使用)
static void addDNode(DNode* new_node, DNode *prev, DNode* next)
{new_node->prev = prev;new_node->next = next;prev->next = new_node;next->prev = new_node;
}// 核心删除函数 (static表示为内部使用)
static void delDNode(DNode* prev, DNode* next)
{prev->next = next;next->prev = prev;
}

4.头插法

void insertDListHeader(DList *header, Element val)
{DNode *new_node = malloc(sizeof(DNode));new_node->val = val;addDNode(new_node,header, header->next);++header->val;
}

5.尾插法

void insertDListRear(DList *header, Element val)
{DNode *new_node = malloc(sizeof(DNode));new_node->val = val;addDNode(new_node, header->prev, header);++header->val;
}

6.展示链表

void showDList(const DList *header)
{DNode *pos = header->next;printf("show:");while (pos != header){printf("%d\t", pos->val);pos = pos->next;}printf("\n");
}

7.删除链表中的节点

void deleteDList(DList *header, Element e)
{// 1.找到这个元素,就可以删除,不需要再找到前置节点DNode *pos = header->next;while (pos != header && pos->val != e){pos = pos->next;}// 2.找到没有?if (pos != header){delDNode(pos->prev, pos->next);pos->next = pos->prev = NULL;free(pos);--header->val;} else{printf("Not find %d element!\n",e);}
}

8.销毁链表

void releaseDList(DList* header)
{DNode *pos = header->next;DNode *tmp = NULL;while (pos != header){tmp = pos;delDNode(pos->prev, pos->next);pos = pos->next;free(tmp);--header->val;}
}

9.测试函数

DList stu_table; // 创建一个DList类型的全局变量stu_table
int main()
{initDList(&stu_table);for (int i = 0; i < 5; ++i){//insertDListHeader(&stu_table, i + 100);insertDListRear(&stu_table, i + 100);}insertDListHeader(&stu_table, 60);insertDListHeader(&stu_table, 80);showDList(&stu_table);printf("====================\n");deleteDList(&stu_table, 102);showDList(&stu_table);releaseDList(&stu_table);printf("num:%d\n",stu_table.val);return 0;
}

结果为:
!图12

四、总结:工程领域的“链表之王”

至此,我们已经掌握了双向循环链表。虽然每个节点都增加了一个指针,带来了额外的内存开销,但它换来的是无与伦比的操作便利性:

  • 双向遍历:既能从前到后,也能从后到前。
  • O(1)复杂度的两端操作:无论是头插、头删,还是尾插、尾删,都快如闪电。
  • O(1)复杂度的邻居节点查找:给定任意一个节点,都能立刻找到其前驱和后驱。

正是这些强大的特性,使得双向循环链表在实际工程中备受青睐,成为许多底层库和系统的实现基础,例如著名的 Linux 内核中的list_head就是一个经典的双向循环链表实现。

我们的线性结构探索之旅,到这里就告一段落了。从最简单的顺序表,到最灵活强大的双向循环链表,我们一步步见证了数据结构为了应对不同挑战而进行的精妙演化。接下来,我们将迈入一个全新的维度,探索更加复杂和强大结构,比如“栈”和“队列”。

http://www.dtcms.com/a/420205.html

相关文章:

  • 西安做网站公司玖佰网络如何在google上免费推广
  • 个人域名备案完成了 可以改网站内容吗wordpress个人展示网站6
  • MySQL中SELECT 语句如何 INTO OUTFILE 保存数据到文件
  • 防火墙技术、模型、发展趋势、局限性及安全体系相关分析
  • 上海网站建设公司推荐排名网站建设中英语
  • 电脑从做系统怎么找回以前登录的网站wordpress网站合并
  • 做网站排名多少钱有没有专门做儿童房的网站
  • 菁染料CY5-二硬脂酰基磷脂酰乙醇胺,CY5标记的二硬脂酰基磷脂酰乙醇胺三个核心组成部分
  • 酵母表面展示技术:真核蛋白工程的 “全能工具”,如何重塑生物医学研究?
  • 泸州市住房和城乡建设厅官方网站WordPress破解怎样主题修复
  • redis常见数据结构及其编码方式
  • 双八网站建设深圳定做网站
  • 来个网站奖励自己单页网站怎么制作教程
  • 网站建设费用做网站和app哪个简单
  • 什么网站可以做直播wordpress 多个域名
  • Windows版本PostgreSQL定时备份
  • 网站运营维护方案苏州知名网站建设建站公司
  • 空壳网站主体注销浙江省建设监理管理协会网站
  • Webpack5 第五节
  • 网站的建立设计app的软件
  • “优化编码(Z)” 带来更稳定?还是带来不稳定?- Task.Delay引发的思考
  • 医疗网站前置审批广州制作网站公司简介
  • 帮人做钓鱼网站以及维护建设工程招标专业网站
  • 河南基安建设集团有限公司网站江苏常州网站建设
  • 小视频网站源码不良网站进入窗口
  • 分割数组得到最小绝对差
  • openGauss数据库自动安装脚本
  • GSM8K:评估大模型数学推理能力的关键数据集
  • 网站建立公司百度站长平台h5网站
  • P1989 三元环计数