链 表 类 型 全 面 总 结:单 向、双 向、循 环 链 表 的 特 性 与 选 型 指 南
链 表 类 型 全 面 总 结:单 向、双 向、循 环 链 表 的 特 性 与 选 型 指 南
- 带 头 双 向 循 环 链 表
- 链 表 总 结
- 链 表 种 类
- 双 向 循 环 链 表 的 操 作 实 现
- 代 码 全 貌 与 功 能 介 绍
- 带 头 双 向 循 环 链 表 的 功 能 说 明
- 代 码 效 果 展 示
- 代 码 总 结
- 数 据 结 构 定 义
- 函 数 说 明
- 辅 助 功 能 与 测 试
- 总 结
💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 数据 结 构。
💡个 人 主 页:@笑口常开xpr 的 个 人 主 页
📚系 列 专 栏:硬 核 数 据 结 构 与 算 法
✨代 码 趣 语:巧 妙 的 数 据 结 构 搭 配 简 单 的 代 码,远 比 反 过 来 效 果 好 得 多。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee
在 数 据 结 构 的 世 界 里,每 一 种 设 计 都 可 能 孕 育 出 惊 人 的 效 率 变 革。你 是 否 深 思 过,一 组 精 心 组 织 的 数 据 究 竟 能 创 造 怎 样 的 奇 迹?每 一 次 挖 掘 底 层 原 理,都 是 与 计 算 机 智 慧 的 巅 峰 对 话;每 一 次 剖 析 存 储 模 式,都 在 破 解 数 据 世 界 的 终 极 密 码。准 备 好 迎 接 这 场 盛 宴 了 吗?让 我 们 一 同 探 寻 带 头 双 向 循 环 链 表 的 无 尽 奥 秘,见 证 它 如 何 重 塑 数 字 时 代 的 运 行 法 则!
带 头 双 向 循 环 链 表
基 本 概 念
带 头
带 有 哨 兵 位 的 头 结 点
双 向
双 向 表 示 链 表 可 以 在 可 以 在 两 个 方 向 上 遍 历(向 前 或 向 后)。
循 环
链 表 中 某 个 结 点 的 指 针 指 向 链 表 中 前 面 的 节 点 时,就 形 成 了 环。
特 点
节 点(哨 兵 节 点)
不 存 储 有 效 数 据,仅 作 为 链 表 的 入 口 和 边 界 标 志。
头 节 点 的 prev 指 向 尾 节 点,next 指 向 首 节 点,形 成 环 形 结 构。
作 用:简 化 空 链 表 处 理,避 免 插 入/删 除 时 的 边 界 判 断。
双 向 指 针
每 个 节 点 包 含 两 个 指 针:
prev:指 向 前 驱 节 点(前 一 个 节 点)。
next:指 向 后 继 节 点(后 一 个 节 点)。
优 势:支 持 双 向 遍 历(从 前 到 后 或 从 后 到 前),插 入/ 删 除 时 可 快 速 定 位 前 后 节 点。
循 环 特 性
尾 节 点 的 next 指 向 头 节 点,头 节 点 的 prev 指 向 尾 节 点,形 成 闭 环。
作 用:首 尾 操 作(如 尾 插、头 删)效 率 高,无 需 单 独 处 理 首 尾 指 针。
链 表 总 结
链 表 种 类
链 表 的 结 构 可 根 据 是 否 带 有 哨 兵 位(头 节 点)、是 否 循 环、单 向 或 双 向 三 个 维 度 划 分,共 衍 生 出 8 种 组 合 形 式。
链 表 结 构 的 三 维 度 分 类
哨 兵 位(头 节 点)
有 哨 兵 位:包 含 不 存 储 数 据 的 哑 节 点,简 化 边 界 操 作。
无 哨 兵 位:直 接 通 过 首 节 点 操 作,需 手 动 处 理 边 界 条 件。
循 环 特 性
循 环:首 尾 节 点 相 连(尾 节 点 next 指 向 头 节 点,头 节 点 prev 指 向 尾 节 点)。
非 循 环:尾 节 点 next 为 NULL(单 向)或 无 环(双 向)。
指 针 方 向
单 向:仅 含 指 向 下 一 节 点 的 next 指 针。
双 向:含 指 向 前 驱 的 prev 和 后 继 的 next 指 针。
链 表 结 构 全 解 析
序号 | 哨兵位 | 循环 | 方向 | 典型结构名称 | 核心特点 |
---|---|---|---|---|---|
1 | 无 | 非循环 | 单向 | 单向非循环链表 | 最基础结构,首节点直接指向后继,尾节点next=NULL |
2 | 无 | 循环 | 单向 | 单向循环链表 | 尾节点next指向首节点,形成环形,无哨兵位。 |
3 | 有 | 非循环 | 单向 | 带哨兵位单向链表 | 头节点next指向首节点,尾节点next=NULL,头节点作为边界标志。 |
4 | 有 | 循环 | 单向 | 带哨兵位单向循环链表 | 头节点next指向首节点,尾节点next指向头节点,形成单向环。 |
5 | 无 | 非循环 | 双向 | 双向非循环链表 | 每个节点含prev和next,头节点无前驱,尾节点无后继。 |
6 | 无 | 循环 | 双向 | 双向循环链表 | 头节点prev=NULL(或指向尾节点),尾节点next=NULL(或指向头节点),双向环。 |
7 | 有 | 非循环 | 双向 | 带哨兵位双向链表 | 头节点prev=NULL,next指向首节点,尾节点prev指向倒数第二节点。 |
8 | 有 | 循环 | 双向 | 带头双向循环链表 | 头节点prev指向尾节点,next指向首节点,所有节点形成双向环,操作最灵活。 |
核 心 逻 辑
哨 兵 位:通 过 头 节 点 简 化 边 界 条 件,避 免 空 链 表 特 判。
循 环 特 性:循 环 结 构 使 首 尾 操 作 直 接 通 过 头 节 点 完 成。
双 向 指 针:支 持 O(1) 时 间 复 杂 度 的 前 驱 访 问,适 用 于 需 要 频 繁 前 后 移 动 的 场 景。
双 向 循 环 链 表 的 操 作 实 现
代 码 全 貌 与 功 能 介 绍
整 个 带 头 双 向 循 环 链 表 由 三 个 主 要 文 件 构 成:List.h、List.c 和 test.c。这 种 多 文 件 的 架 构 设 计,有 助 于 将 不 同 功 能 模 块 分 离,提 高 代 码 的 可 读 性、可 维 护 性 与 可 扩 展 性。
List.h
List.h 包 含 了 带 头 双 向 循 环 链 表 所 需 的 头 文 件 引 用、常 量 定 义 以 及 函 数 声 明。
test.c
test.c 是 带 头 双 向 循 环 链 表 的 主 逻 辑 文 件,负 责 处 理 用 户 输 入 和 代 码 流 程 的 控 制。
List.c
List.c 则 实 现 了 带 头 双 向 循 环 链 表 的 具 体 功 能 函 数。
下 面 展 示
完 整 代 码
。
读 者 可 以 将 这 段 代 码 复 制 到 自 己 的 编 译 器 中 运 行。
List.h
#pragma once//双向带头循环链表
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>typedef int LTDataType;
typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;//初始化---设置哨兵位的头节点
LTNode* LTInit();//销毁
void LTDestory(LTNode* phead);//尾插
void LTPushBack(LTNode* phead, LTDataType x);//头插
void LTPushFront(LTNode* phead, LTDataType x);//尾删
void LTPopBack(LTNode* phead);//头删
void LTPopFront(LTNode* phead);//创建节点
LTNode* BuyListNode(LTDataType x);//输出数据
void LTPrint(LTNode* phead);//检查是否只有哨兵位的头节点
bool LTEmpty(LTNode* phead);//pos位置之前插入一个节点
void LTInsertFront(LTNode* pos, LTDataType x);//pos位置之后插入一个节点
void LTInsertAfter(LTNode* pos, LTDataType x);//pos位置的前面删除一个节点
void LTEraseFront(LTNode* phead, LTNode* pos);//pos位置删除一个节点
void LTErase(LTNode* phead, LTNode* pos);//pos位置的后面删除一个节点
void LTEraseBack(LTNode* phead, LTNode* pos);//查找
LTNode* LTFind(LTNode* phead, LTDataType x);//加载链表
void LTLoad(LTNode* phead);//保存链表
void LTSave(LTNode* phead);//函数指针
void LTPushMiddle(void(*pf)(LTNode* phead, LTDataType x),LTNode* phead);void LTMiddlePos(int(*pf)(LTNode* phead, LTDataType x), LTNode* phead, LTDataType num);void LTMiddleErase(int(*pf)(LTNode* phead, LTNode* pos), LTNode* phead, LTDataType num);
test.c
#define _CRT_SECURE_NO_WARNINGS 1
//尾的next指向哨兵位的头
//哨兵位的prev指向尾
#include "List.h"
void menu()
{printf("*******************************************************\n");printf("***** 0. LTExit 1. LTPushBack ******\n");printf("***** 2. LTPopBack 3. LTPushFront ******\n");printf("***** 4. LTPopFront 5. LTInsertFront ******\n");printf("***** 6. LTInsertAfter 7. LTEraseFront ******\n");printf("***** 8. LTErase 9. LTEraseBack ******\n");printf("***** 10. LTPrint 11. LTFind ******\n");printf("*******************************************************\n");
}
void test()
{int input = 0;LTNode* plist = LTInit();do{menu();printf("请输入你想要进行的操作:>");scanf("%d", &input);switch (input){case 0:LTSave(plist);LTDestory(plist);break;case 1:LTPushMiddle(LTPushBack, plist);break;case 2:LTPopBack(plist);LTPrint(plist);break;case 3:LTPushMiddle(LTPushFront, plist);break;case 4:LTPopFront(plist);LTPrint(plist);break;case 5:LTMiddlePos(LTInsertFront, plist, 5);break;case 6:LTMiddlePos(LTInsertAfter, plist, 6);break;case 7:LTMiddleErase(LTEraseFront, plist, 7);break;case 8:LTMiddleErase(LTErase, plist, 8);break;case 9:LTMiddleErase(LTEraseBack, plist, 9);break;case 10:LTPrint(plist);break;case 11:LTMiddlePos(LTFind, plist, 11);break;default:printf("输入错误,请重新输入\n");break;}} while (input);
}
int main()
{test();//TestList();return 0;
}
List.c
#define _CRT_SECURE_NO_WARNINGS 1 #include "List.h"LTNode* LTInit()
{LTNode* phead = BuyListNode(-1);phead->next = phead;phead->prev = phead;LTLoad(phead);return phead;
}
void LTDestory(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* curnext = cur->next;free(cur);cur = curnext;}free(phead);
}
LTNode* LTFind(LTNode* phead,LTDataType x)
{LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}
LTNode* BuyListNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("newnode");return NULL;}newnode->next = NULL;newnode->prev = NULL;newnode->data = x;return newnode;
}
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);//一定不为空,空可能是传错了//方法1LTNode* newnode = BuyListNode(x);LTNode* tail = phead->prev;//phead tail newnodetail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;//方法2//newnode phead tail//LTInsertFront(phead, x);
}
bool LTEmpty(LTNode* phead)
{assert(phead);//方法1//if (phead->next == phead)//{// return true;//}//else//{// return false;//}//方法2return phead->next != phead;
}
void LTPopBack(LTNode* phead)
{assert(phead);assert(LTEmpty(phead));//phead head tail//方法1//LTNode* tail = phead->prev;//LTNode* tailprev = tail->prev;//tailprev->next = phead;//phead->prev = tailprev;//free(tail);//tail = NULL;//方法2LTErase(phead, phead->prev);
}
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);//一定不为空,空可能是传错了LTNode* newnode = BuyListNode(x);//phead newnode head//方法1---可以随便换顺序LTNode* head = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = head;head->prev = newnode;//方法2---不能随便换顺序//newnode->next = phead->next;//phead->next->prev = newnode;//phead->next = newnode;//newnode->prev = phead;//方法3//LTInsertFront(phead->next, x);
}//头删
void LTPopFront(LTNode* phead)
{assert(phead);assert(LTEmpty(phead));//phead head headnext//方法1//LTNode* head = phead->next;//phead->next = head->next;//head->prev = phead;//free(head);//head = NULL;//方法2LTErase(phead, phead->next);
}
void LTPrint(LTNode* phead)
{assert(phead);printf("<=head=>");LTNode* cur = phead->next;while (cur != phead){printf("%d<=>", cur->data);cur = cur->next;}printf("\n");
}void LTInsertFront(LTNode* pos, LTDataType x)
{assert(pos);LTNode* prev = pos->prev;LTNode* newnode = BuyListNode(x);//prev newnode posprev->next = newnode;newnode->prev = prev;newnode->next = pos;pos->prev = newnode;
}void LTInsertAfter(LTNode* pos, LTDataType x)
{assert(pos);//pos newnode posnextLTNode* posnext = pos->next;LTNode* newnode = BuyListNode(x);pos->next = newnode;newnode->prev = pos;newnode->next = posnext;posnext->prev = newnode;
}void LTEraseFront(LTNode* phead, LTNode* pos)
{assert(pos);assert(pos->prev && pos != phead);// posprev posLTNode* posprev = pos->prev;posprev->prev->next = pos;pos->prev = posprev->prev;free(posprev);posprev = NULL;
}void LTErase(LTNode* phead, LTNode* pos)
{assert(pos);assert(pos != phead);// posprev pos LTNode* posprev = pos->prev;posprev->next = pos->next;pos->next->prev = posprev;free(pos);pos = NULL;
}void LTEraseBack(LTNode* phead, LTNode* pos)
{assert(pos);assert(pos->next && pos->next != phead);//pos posnextLTNode* posnext = pos->next;posnext->next->prev = pos;pos->next = posnext->next;free(posnext);posnext = NULL;
}void LTLoad(LTNode* phead)
{FILE* pf = fopen("List.txt", "r");if (pf == NULL){perror("pf");return;}int num = 0;while (fscanf(pf, "%d", &num) == 1){LTPushBack(phead, num);}fclose(pf);pf = NULL;
}
void LTSave(LTNode* phead)
{FILE* pf = fopen("List.txt", "w");if (pf == NULL){perror("pf");return;}LTNode* cur = phead->next;while (cur != phead){fprintf(pf, "%d ", cur->data);cur = cur->next;}fclose(pf);pf = NULL;printf("保存双向循环链表成功\n");
}void LTPushMiddle(void(*pf)(LTNode* phead, LTDataType x), LTNode* phead)
{LTDataType x = 0;printf("请输入你想要插入的数字:>\n");scanf("%d", &x);pf(phead, x);LTPrint(phead);
}void LTMiddlePos(int(*pf)(LTNode* phead, LTDataType x), LTNode* phead,LTDataType num)
{LTDataType x = 0;LTDataType y = 0;if (num == 11){printf("请输入你想要查找哪个数字:>");scanf("%d", &x);LTNode* ret = LTFind(phead, x);if (ret == NULL){printf("找不到\n");}else{printf("%d\n", ret->data);return;}}LTPrint(phead);if (num == 5){printf("请输入你想要插入哪个数字的前面:>");scanf("%d", &x);printf("请输入你想要插入的数字:>");scanf("%d", &y);LTNode* ret = LTFind(phead, x);pf(ret, y);}else{printf("请输入你想要插入哪个数字的后面:>");scanf("%d", &x);printf("请输入你想要插入的数字:>");scanf("%d", &y);LTNode* ret = LTFind(phead, x);pf(ret, y);}LTPrint(phead);
}void LTMiddleErase(int(*pf)(LTNode* phead, LTNode* pos), LTNode* phead, LTDataType num)
{int x = 0;LTPrint(phead);if (num == 7){printf("请输入你想要删除哪个数字前面的数字:>");scanf("%d", &x);LTNode* ret = LTFind(phead, x);pf(phead, ret);}else if (num == 8){printf("请输入你想要删除哪个数字:>");scanf("%d", &x);LTNode* ret = LTFind(phead, x);pf(phead, ret);}else{printf("请输入你想要删除哪个数字后面的数字:>");scanf("%d", &x);LTNode* ret = LTFind(phead, x);pf(phead, ret);}LTPrint(phead);
}
带 头 双 向 循 环 链 表 的 功 能 说 明
代 码 效 果 展 示
菜 单 展 示
每 次 循 环 开 始 时 会 显 示 菜 单,内 容 包 括:
LTExit:退 出 程 序 并 将 双 向 循 环 链 表 元 素 保 存 到 文 件 中
LTPushBack:尾 插
LTPopBack:尾 删
LTPushFront:头 插
LTPopFront:头 删
LTInsertFront:从 一 个 数 的 前 面 插 入
LTInsertAfter:从 一 个 数 的 后 面 插 入
LTErasFront:删 除 指 定 数 前 面 的 数 字
LTErase:擦 除 某 个 指 定 的 数
LTEraseAfter:删 除 指 定 数 后 面 的 数 字
LTPrint:输 出 链 表
LTFind:查 找 链 表 元 素
退 出 双 向 循 环 链 表(LTExit)
输 入 0 后,程 序 会 将 双 向 循 环 链 表 中 的 信 息 保 存 到 文 件 “List.txt” 中。释 放 内 存 并 销 毁 双 向 循 环 链 表, 然 后 退 出 程 序。
尾 插(LTPushBack)
输 入 1 后,程 序 会 提 示 用 户 输 入 插 入 的 数 字。输 入 完 成 后,数 字 被 添 加 到 双 向 循 环 链 表 的 末 尾,然 后 输 出 修 改 后 的 双 向 循 环 链 表。
尾 删(LTPopBack)
输 入 2 后,双 向 循 环 链 表 的 末 尾 的 数 字 会 被 删 除,然 后 输 出 修 改 后 的 双 向 循 环 链 表。
头 插(LTPushFront)
输 入 3 后,程 序 会 提 示 用 户 输 入 插 入 的 数 字。输 入 完 成 后,数 字 被 添 加 到 双 向 循 环 链 表 的 第 一 个 位 置,然 后 输 出 修 改 后 的 双 向 循 环 链 表。
头 删(LTPopFront)
输 入 4 后, 双 向 循 环 链 表 的 第 一 个 位 置 的 数 字 会 被 删 除 并 输 出 删 除 后 的 双 向 循 环 链 表。
从 某 一 个 数 字 的 前 面 插 入(LTInsertFront)
输 入 5 后,程 序 首 先 输 出 双 向 循 环 链 表,然 后 会 提 示 用 户 输 入 插 入 的 数 字 和 要 插 入 数 字 的 位 置。输 入 完 成 后,数 字 被 添 加 到 双 向 循 环 链 表 的 要 输 入 数 字 的 前 面 并 输 出 修 改 后 的 双 向 循 环 链 表。
从 某 一 个 数 字 的 后 面 插 入(LTInsertAfter)
输 入 6 后,程 序 首 先 输 出 双 向 循 环 链 表,然 后 会 提 示 用 户 输 入 插 入 的 数 字 和 要 插 入 数 字 的 位 置。输 入 完 成 后,数 字 被 添 加 到 双 向 循 环 链 表 的 要 输 入 数 字 的 后 面 并 输 出 修 改 后 的 双 向 循 环 链 表。
删 除 指 定 数 字 前 面 的 数 字(LTEraseFront)
输 入 7 后,程 序 首 先 输 出 双 向 循 环 链 表,然 后 提 示 用 户 输 入 要 删 除 的 数 字。输 入 完 成 后,要 删 除 数 字 前 面 的 数 字 会 被 删 除,然 后 输 出 修 改 后 的 双 向 循 环 链 表。
删 除 指 定 的 数 字(LTErase)
输 入 8 后,程 序 会 提 示 用 户 输 入 想 要 删 除 的 数 字。输 入 完 成 后,想 要 删 除 的 数 字 会 被 清 除,然 后 输 出 修 改 后 的 双 向 循 环 链 表。
删 除 指 定 数 字 后 面 的 数 字(LTEraseBack)
输 入 9 后,程 序 首 先 输 出 双 向 循 环 链 表,然 后 提 示 用 户 输 入 要 删 除 的 数 字。输 入 完 成 后,要 删 除 数 字 后 面 的 数 字 会 被 删 除,然 后 输 出 修 改 后 的 双 向 循 环 链 表。
输 出 双 向 循 环 链 表(LTPrint)
输 入 10 后,程 序 会 在 屏 幕 上 显 示 目 前 已 经 保 存 的 双 向 循 环 链 表。
输 出 双 向 循 环 链 表(LTFind)
输 入 11 后,程 序 提 示 用 户 想 要 查 找 的 数 字,如 果 数 字 在 链 表 中,则 输 出 数 字 在 链 表 中 的 位 置。反 之,输 出 找 不 到。
代 码 总 结
数 据 结 构 定 义
typedef int LTDataType;
typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
} LTNode;
双 向 链 表 节 点:包 含 数 据 域 data、前 驱 指 针 prev、后 继 指 针 next。
哨 兵 位 头 节 点:初 始 化 时 创 建,data 为 -1,next 和 prev 指 向 自 身,便 于 统 一 处 理 空 链 表 和 非 空 链 表 的 操 作。
函 数 说 明
函数名 | 功能描述 |
---|---|
LTInit | 初始化链表,创建哨兵位头节点,并加载数据。 |
LTDestroy | 销毁链表,释放所有节点内存。 |
LTPushBack/LTPushFront | 尾插 / 头插节点,支持通过LTInsertFront复用实现头插。 |
LTPopBack/LTPopFront | 尾删 / 头删节点,通过LTErase复用实现删除逻辑。 |
LTInsertFront/LTInsertAfter | 在指定节点pos前 / 后插入新节点。 |
LTErase/LTEraseFront/LTEraseBack | 删除指定节点pos、pos的前驱或后继节点。 |
LTFind | 查找值为x的节点,返回节点指针或NULL。 |
LTPrint | 遍历链表并输出节点数据。 |
LTLoad/LTSave | 从文件加载数据到链表 / 将链表数据保存到文件(文本文件List.txt)。 |
辅 助 功 能 与 测 试
菜 单 驱 动 测 试:通 过 menu 函 数 提 供 操 作 选 项,调 用 各 功 能 函 数 实 现 交 互 测 试。
函 数 指 针 复 用:如 LTPushMiddle 通 过 函 数 指 针 动 态 调 用 LTPushBack 或 LTPushFront,提 高 代 码 灵 活 性。
文 件 操 作:LTLoad 和 LTSave 实 现 链 表 数 据 的 持 久 化 存 储,便 于 程 序 重 启 后 恢 复 数 据。
总 结
至 此,关 于 双 向 循 环 链 表 的 探 索 暂 告 一 段 落,但 你 的 编 程 征 程 才 刚 刚 启 航。编 写 代 码 是 与 计 算 机 逻 辑 深 度 对 话,过 程 中 虽 会 在 结 构 设 计、算 法 实 现 的 困 境 里 挣 扎,但 这 些 磨 砺 加 深 了 对 代 码 逻 辑 和 数 据 组 织 的 理 解。愿 你 合 上 电 脑 后,灵 感 不 断,在 数 据 结 构 的 世 界 里 持 续 深 耕,书 写 属 于 自 己 的 编 程 传 奇,下 一 次 开 启,定 有 全 新 的 精 彩 等 待。小 编 期 待 重 逢,盼 下 次 阅 读 时 见 证 你 们 更 大 的 进 步,共 赴 代 码 之 约!