【初阶数据结构05】——双向链表专题

【初阶数据结构05】——双向链表专题

文章目录

引言

1. 双向链表的结构

1.1 什么是带头双向循环链表?

1.2 为什么需要哨兵位?

2. 双向链表的实现

2.1 节点类型定义

2.2 函数声明

2.3 函数实现

2.3.1 创建新节点(内部辅助函数)

2.3.2 初始化

2.3.3 打印链表

2.3.4 尾插

2.3.5 尾删

2.3.6 头插(在第一个有效节点之前插入)

2.3.7 头删(删除第一个有效节点)

2.3.8 查找

2.3.9 在指定位置之后插入

2.3.10 删除指定位置的节点

2.3.11 销毁链表

2.4 测试代码示例

3. 完整代码展示

3.1 List.h

3.2 List.c

3.3 test.c

4. 总结

下期预告


引言

单链表专题中,我们深入探索了单链表,学会了如何通过指针将零散的内存节点串联成一条线。单链表的结构简单,但操作时我们常常需要“瞻前顾后”——因为每个节点只知道下一个节点的位置,却不知道上一个节点,导致很多操作(如尾部删除)需要遍历整个链表。

为了解决这个问题,C语言提供了更强大的链表形式:双向链表。它不仅知道下一个节点是谁,还知道上一个节点是谁,就像火车车厢不仅有后门,还有前门,可以双向通行。

今天,我们将学习最常用的双向链表结构——带头双向循环链表

1. 双向链表的结构

1.1 什么是带头双向循环链表?

让我们把名字拆开看:

  • 带头:存在一个特殊的节点,称为“头节点”或“哨兵位”(sentinel)。这个节点不存储有效数据,它的作用是简化边界条件的处理。就像停车场门口的岗亭,它本身不是车位,但帮助我们管理车辆进出。
  • 双向:每个节点包含两个指针——prev 指向前一个节点,next 指向后一个节点。
  • 循环:最后一个节点的 next 指向头节点,头节点的 prev 指向最后一个节点。整个链表形成一个环,从任何节点出发都能遍历整个链表。

1.2 为什么需要哨兵位?

在单链表中,当我们插入或删除第一个节点时,需要修改头指针(一级指针的二级指针问题)。而有了哨兵位后,头指针永远指向这个哨兵节点,所有有效节点都在它之后。这样,插入和删除操作就不需要改变头指针的值,代码逻辑更加统一,边界条件也大大简化。

此外,循环的特性使得我们可以从哨兵位开始,既能正向遍历(next),也能反向遍历(prev),非常灵活。

2. 双向链表的实现

我们将使用 C 语言实现一个带头双向循环链表,提供常见的操作接口。

2.1 节点类型定义

typedef int ListdataType; typedef struct ListNode { ListdataType data; struct ListNode* next; struct ListNode* prev; }Listnode;

2.2 函数声明

Listnode* BuyNode(ListdataType x); Listnode* InitList(); //链表初始化,其实就是让链表只含有一个头节点 void PrintList(Listnode* head);//打印链表 void Listpushback(Listnode* head, ListdataType x);//尾插数据 void Listpushfront(Listnode* head, ListdataType x);//头插数据,在第一个有效数据之前插入 void Listpopback(Listnode* head);//尾删 void Listpopfront(Listnode* head);//头删 Listnode* Listfind(Listnode* head, ListdataType x);//在链表中查找数据,返回第一个匹配的节点 void Listinsert(Listnode* pos, ListdataType x);//在指定位置之后插入数据 void Listdelete(Listnode* pos);//删除指定位置的数据 void Listdestroy(Listnode* head);//销毁链表

2.3 函数实现

2.3.1 创建新节点(内部辅助函数)
Listnode* BuyNode(ListdataType x)//因为后面会有尾插、头插,所以写一个函数来创建节点 { Listnode* node = (Listnode*)malloc(sizeof(Listnode)); if (node == NULL) { perror("malloc error"); exit(1); } node->data = x; node->prev = node->next = node; return node; }
2.3.2 初始化
Listnode* InitList() { Listnode* head = BuyNode(-1); return head; } 
2.3.3 打印链表
void PrintList(Listnode* head) { Listnode* pcur = head->next; while (pcur != head) { printf("%d->", pcur->data); pcur = pcur->next; } printf("\n"); }
2.3.4 尾插
void Listpushback(Listnode* head, ListdataType x) { assert(head); Listnode* newnode = BuyNode(x); Listnode* pcur = head->prev; newnode->prev = pcur; newnode->next = head; pcur->next = newnode; head->prev = newnode; }

分析:利用循环特性,我们不需要遍历就能直接找到尾节点(phead->prev),因此尾插操作的时间复杂度为 O(1)。

2.3.5 尾删
void Listpopback(Listnode* head) { assert(head && head->next != head); Listnode* del = head->prev; del->prev->next = head; head->prev = del->prev; free(del); del = NULL; }
2.3.6 头插(在第一个有效节点之前插入)
void Listpushfront(Listnode* head, ListdataType x) { assert(head); Listnode* newnode = BuyNode(x); Listnode* pcur = head->next; newnode->prev = head; newnode->next = pcur; pcur->prev = newnode; head->next = newnode; }
2.3.7 头删(删除第一个有效节点)
void Listpopfront(Listnode* head) { assert(head && head->next != head); Listnode* del = head->next; del->next->prev = head; head->next = del->next; }
2.3.8 查找
Listnode* Listfind(Listnode* head, ListdataType x) { Listnode* pcur = head->next; while (pcur != head) { if (pcur->data == x) { printf("找到了\n"); return pcur; } pcur = pcur->next; } printf("链表中没有找到%d\n", x); return NULL; } 
2.3.9 在指定位置之后插入
void Listinsert(Listnode* pos, ListdataType x) { assert(pos); Listnode* newnode = BuyNode(x); newnode->next = pos->next; pos->next->prev = newnode; newnode->prev = pos; pos->next = newnode; }

注意:这个操作在 pos 之后插入,时间复杂度 O(1)。如果我们想在 pos 之前插入,也可以利用双向链表的特性很快实现(只需稍微调整逻辑),但这里我们只实现一种。

2.3.10 删除指定位置的节点
void Listdelete(Listnode* pos) { assert(pos); pos->next->prev = pos->prev; pos->prev->next = pos->next; free(pos); pos = NULL; }
2.3.11 销毁链表
void Listdestroy(Listnode* head) { assert(head); Listnode* pcur = head->next; while (pcur != head) { Listnode* del = pcur; pcur = pcur->next; free(del); del = NULL; } free(pcur); pcur = head = NULL; }

2.4 测试代码示例

void Test01()//测试初始化、打印、创建节点函数功能 { Listnode* head = NULL; head = InitList(); Listnode* node1 = BuyNode(1); Listnode* node2 = BuyNode(2); Listnode* node3 = BuyNode(3); Listnode* node4 = BuyNode(4); head->next=node1; node1->prev=head; node1->next=node2; node2->prev=node1; node2->next=node3; node3->prev=node2; node3->next=node4; node4->prev=node3; node4->next=head; PrintList(head); } void Test02()//测试插入节点、删除节点函数功能 { Listnode* head=InitList(); Listpushback(head, 1); Listpushback(head, 2); Listpushback(head, 3); Listpushfront(head, 4); PrintList(head); Listpopback(head); PrintList(head); Listpopfront(head); PrintList(head); Listpopfront(head); PrintList(head); Listpopfront(head); PrintList(head); Listpopfront(head); PrintList(head); } void Test03() { Listnode* head = InitList(); Listpushback(head, 1); Listpushback(head, 2); Listpushback(head, 3); Listpushback(head, 4); Listinsert(Listfind(head, 4), 10); PrintList(head); Listdestroy(head); head = NULL; } int main() { //Test01(); //Test02(); Test03(); return 0; }

3. 完整代码展示

3.1 List.h

#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int ListdataType; typedef struct ListNode { ListdataType data; struct ListNode* next; struct ListNode* prev; }Listnode; Listnode* BuyNode(ListdataType x); Listnode* InitList(); //链表初始化,其实就是让链表只含有一个头节点 void PrintList(Listnode* head);//打印链表 void Listpushback(Listnode* head, ListdataType x);//尾插数据 void Listpushfront(Listnode* head, ListdataType x);//头插数据,在第一个有效数据之前插入 void Listpopback(Listnode* head);//尾删 void Listpopfront(Listnode* head);//头删 Listnode* Listfind(Listnode* head, ListdataType x);//在链表中查找数据,返回第一个匹配的节点 void Listinsert(Listnode* pos, ListdataType x);//在指定位置之后插入数据 void Listdelete(Listnode* pos);//删除指定位置的数据 void Listdestroy(Listnode* head);//销毁链表

3.2 List.c

#define _CRT_SECURE_NO_WARNINGS #include"List.h" Listnode* BuyNode(ListdataType x)//因为后面会有尾插、头插,所以写一个函数来创建节点 { Listnode* node = (Listnode*)malloc(sizeof(Listnode)); if (node == NULL) { perror("malloc error"); exit(1); } node->data = x; node->prev = node->next = node; return node; } void PrintList(Listnode* head) { Listnode* pcur = head->next; while (pcur != head) { printf("%d->", pcur->data); pcur = pcur->next; } printf("\n"); } Listnode* InitList() { Listnode* head = BuyNode(-1); return head; } void Listpushback(Listnode* head, ListdataType x) { assert(head); Listnode* newnode = BuyNode(x); Listnode* pcur = head->prev; newnode->prev = pcur; newnode->next = head; pcur->next = newnode; head->prev = newnode; } void Listpushfront(Listnode* head, ListdataType x) { assert(head); Listnode* newnode = BuyNode(x); Listnode* pcur = head->next; newnode->prev = head; newnode->next = pcur; pcur->prev = newnode; head->next = newnode; } void Listpopback(Listnode* head) { assert(head && head->next != head); Listnode* del = head->prev; del->prev->next = head; head->prev = del->prev; free(del); del = NULL; } void Listpopfront(Listnode* head) { assert(head && head->next != head); Listnode* del = head->next; del->next->prev = head; head->next = del->next; } Listnode* Listfind(Listnode* head, ListdataType x) { Listnode* pcur = head->next; while (pcur != head) { if (pcur->data == x) { printf("找到了\n"); return pcur; } pcur = pcur->next; } printf("链表中没有找到%d\n", x); return NULL; } void Listinsert(Listnode* pos, ListdataType x) { assert(pos); Listnode* newnode = BuyNode(x); newnode->next = pos->next; pos->next->prev = newnode; newnode->prev = pos; pos->next = newnode; } void Listdelete(Listnode* pos) { assert(pos); pos->next->prev = pos->prev; pos->prev->next = pos->next; free(pos); pos = NULL; } void Listdestroy(Listnode* head) { assert(head); Listnode* pcur = head->next; while (pcur != head) { Listnode* del = pcur; pcur = pcur->next; free(del); del = NULL; } free(pcur); pcur = head = NULL; }

3.3 test.c

#define _CRT_SECURE_NO_WARNINGS #include"List.h" void Test01()//测试初始化、打印、创建节点函数功能 { Listnode* head = NULL; head = InitList(); Listnode* node1 = BuyNode(1); Listnode* node2 = BuyNode(2); Listnode* node3 = BuyNode(3); Listnode* node4 = BuyNode(4); head->next=node1; node1->prev=head; node1->next=node2; node2->prev=node1; node2->next=node3; node3->prev=node2; node3->next=node4; node4->prev=node3; node4->next=head; PrintList(head); } void Test02()//测试插入节点、删除节点函数功能 { Listnode* head=InitList(); Listpushback(head, 1); Listpushback(head, 2); Listpushback(head, 3); Listpushfront(head, 4); PrintList(head); Listpopback(head); PrintList(head); Listpopfront(head); PrintList(head); Listpopfront(head); PrintList(head); Listpopfront(head); PrintList(head); Listpopfront(head); PrintList(head); } void Test03() { Listnode* head = InitList(); Listpushback(head, 1); Listpushback(head, 2); Listpushback(head, 3); Listpushback(head, 4); Listinsert(Listfind(head, 4), 10); PrintList(head); Listdestroy(head); head = NULL; } int main() { //Test01(); //Test02(); Test03(); return 0; }

4. 总结

双向链表(带头双向循环链表)是链表家族中功能最强大的成员。它通过引入哨兵位和双向指针,使得几乎所有操作(头插、头删、尾插、尾删、指定位置插入删除)都能在 O(1) 时间内完成,代码逻辑也更加简洁统一。

然而,没有完美的数据结构。双向链表牺牲了随机访问的能力,且每个节点需要额外存储两个指针,内存开销比单链表更大。在实际开发中,我们需要根据具体需求权衡利弊,选择最合适的数据结构。

下期预告

掌握了单链表和双向链表后,我们已经能够灵活地组织线性数据。接下来,我们将进入另一个重要的线性结构——栈和队列。它们是对线性表的一种特殊限制,广泛应用于函数调用、表达式求值、消息队列等场景。让我们继续探索数据结构的奇妙世界!

Read more

Re:从零开始的 C++ 进阶篇(三)彻底搞懂 C++ 多态:虚函数、虚表与动态绑定的底层原理

Re:从零开始的 C++ 进阶篇(三)彻底搞懂 C++ 多态:虚函数、虚表与动态绑定的底层原理

◆ 博主名称: 晓此方-ZEEKLOG博客大家好,欢迎来到晓此方的博客。⭐️C++系列个人专栏: 主题曲:C++程序设计⭐️ 踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰 0.1概要&序論 这里是此方,好久不见。 多态是 C++ 中最核心而且是最难理解的机制之一。它不仅是语法层面的特性,更牵涉到 C++ 的对象模型、对象内存布局以及多态机制的底层实现原理。本文将从底层原理出发,系统全面解析多态的真实运作机制。这里是「此方」。让我们现在开始吧! 一,多态的概念 通俗来说,多态就是多种形态。多态分为编译时多态(静态多态) 和 运行时多态(动态多态),这里我们重点讲运行时多态。 1.1编译时多态(静态多态) 编译时多态主要就是我们前面讲的 函数重载和函数模板。 它们通过传递不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。之所以叫编译时多态,是因为实参传递给形参的参数匹配是在编译时完成的,

By Ne0inhk
【STL】stack/queue 底层模拟实现与典型算法场景实践

【STL】stack/queue 底层模拟实现与典型算法场景实践

前言 STL 中 stack 与 queue 本质是容器适配器,基于基础容器封装实现特定操作逻辑。本文先介绍容器适配器及二者核心概念,再手动模拟实现,最后通过几道算法题展示其应用,助力夯实 STL 设计思想与数据结构基础。 目录  ------------容器适配器------------ 1、什么是容器适配器? 2、为啥容器配置器不支持迭代器  ---------------stack--------------- 1、stack介绍 2、stack模拟实现 问题:为啥 stack 不用提供默认成员函数? ---------------queue-------------- 1、queue介绍 2、queue模拟实现 --------------算法题-------------- 1、最小栈 2、栈的压入、弹出序列 3、逆波兰表达式求值 4、用栈实现队列 5、用队列实现栈  ------------容器适配器------------ 1、什么是容器适配器? 适配器可以理解为“

By Ne0inhk
【C++写详细总结①】从for循环到算法初步

【C++写详细总结①】从for循环到算法初步

前言 本文通过小编自身学习的进程从而总结出本文,也希望大家可以好好学习,帮助到自己 这个是萌新考场救场代码,与本文一起食用更佳 for循环计数器 for(定义计数变量;定义结束条件;每次循环所做的动作) 示例 for(int i=1;i<=10;i++) //首先定义“i”变量作为计数数组,赋初值为“1”//然后每次循环判断条件是否成立,不成立则退出//最后每循环执行条件,此示例为每循环“i”增加1 而计数器就是在for循环有了一定执行范围的基础上创建了一个数组,进行++计数 示例 #include<iostream>// 万年不变的框架usingnamespace std;intmain(){int n; cin>>n;//输入数值表示从1~n中有几个数字int

By Ne0inhk
【C++笔记】模板初阶

【C++笔记】模板初阶

前言:         C++模板是C++中实现泛型编程的核心工具,允许程序员编写与类型无关的代码,从而提高代码的复用性和灵活性。模板在编译时进行实例化,根据实际使用的类型生成具体的代码,因此不会带来运行时开销。          一、模板基础          1.1 为什么需要模板?          在编写函数或类时,如果希望它们能处理多种数据类型(如int、double、string),传统方法是使用函数重载,但这样会产生大量重复代码或失去类型信息。 模板允许将类型作为参数,编译器根据调用时传入的具体类型生成对应的代码。          场景:需要编写一个求两个数最大值的函数,支持 int、double 和 string(按字典序)。          ①传统方法:函数重载 #include <iostream> #include <string> using namespace std; // 为 int 重载 int max(int

By Ne0inhk