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

算法中的链表结构

准备工作:静态实现链表的原因

在算法竞赛中,静态实现链表(用数组模拟)比使用动态链表(如C++的std::list或手动new/delete创建节点)更常见,核心原因是效率、稳定性和可控性的综合考量,具体如下:

1. 避免动态内存分配的开销

动态链表的节点需要通过new(C++)或malloc(C)动态分配内存,删除时需要deletefree。这类操作的问题在于:

  • 时间开销大:动态内存分配涉及操作系统的内存管理(如查找空闲块、修改内存链表等),单次操作的时间复杂度看似是O(1),但实际常数极大(可能比数组下标访问慢几个数量级)。在竞赛中,当数据量达到1e5甚至1e6级别时,频繁的动态分配会直接导致超时。
  • 内存碎片:频繁的分配和释放会导致内存碎片,进一步降低内存管理效率,极端情况下可能触发意外的内存分配失败。

而静态链表通过预先定义大数组(如const int N = 1e5 + 10)存储节点,访问和修改仅通过数组下标(本质是指针的简化,直接访问内存地址),时间开销接近O(1)且常数极小,完全规避了动态分配的问题。

2. 更强的可控性和稳定性

  • 避免空指针错误:动态链表依赖指针(如Node*),若操作不当(如访问已删除的节点、未初始化的指针)会导致空指针异常(NULL访问),在竞赛中这类错误难以调试且可能直接导致程序崩溃。
    静态链表用数组下标(如0作为哨兵/空节点标志),通过预先初始化数组(如下标0固定为“空”),可天然避免空指针问题,逻辑更稳定。

  • 内存上限可控:竞赛题目通常会明确数据范围(如“节点数不超过1e5”),静态链表可通过定义N为略大于数据范围的值(如1e5 + 10),确保内存足够且不浪费。而动态链表若分配过量可能导致内存超限(MLE),分配不足则会运行时错误,可控性差。

3. 适配竞赛的特殊操作需求

算法竞赛中,链表常被用于实现更复杂的数据结构(如邻接表、单调队列、并查集的扩展等),或需要快速访问前驱/后继、批量操作节点的场景。静态链表的数组下标特性使其:

  • 支持O(1)时间的随机访问(通过下标直接定位节点),而动态链表需要从头遍历,效率极低。
  • 便于调试和输出中间状态(直接打印数组下标即可追踪节点关系),动态链表的指针地址无实际意义,难以调试。

4. 代码简洁,减少冗余

静态链表的实现仅需几个数组(如e[]存储值、pre[]/ne[]存储前驱/后继下标)和简单的索引管理(id记录当前可用节点),代码量极少,且逻辑清晰。例如:

int e[N], ne[N], id;  // 单链表核心定义
void init() { id = 0; }
void insert(int p, int x) { e[++id] = x; ne[id] = ne[p]; ne[p] = id; }

而动态链表需要定义节点结构体、处理指针操作,代码冗长且易出错,在时间紧张的竞赛中会浪费宝贵时间。

总结

算法竞赛的核心目标是在限制时间和内存内通过所有测试用例,静态链表通过“空间换时间”和“预先分配内存”,完美适配了竞赛对效率、稳定性和简洁性的需求。动态链表的灵活性在竞赛场景中优势有限,反而因开销和风险成为劣势,因此静态实现成为主流选择。

一.单链表

1.概念

在这里插入图片描述静态链表的实现原理

  • 实现方式:静态实现,用数组模拟链表,避免动态分配内存的复杂操作
  • 核心组成
    • 两个数组:elem(数据域,存节点数据)、next(指针域,存下一个节点下标 )
    • 两个变量:h(标记头结点下标,图中“哨兵”对应头结点,简化操作 )、id(标记新节点存储位置,记录可用空间 )
  • 逻辑与存储:通过next数组构建逻辑链表结构(逻辑上是链式关系),实际存储在数组里(物理上是连续/离散数组空间 ),用下标模拟指针关联节点,实现链表的插入、删除等操作 ,比如插入新节点时,更新next数组下标,就能改变节点的逻辑连接 。

2.示例代码

#include<iostream>
using namespace std;// 定义数组大小常量,1e3表示1000,用于存储链表节点
const int N = 1e3;// e[]: 存储节点的值,e是elem
// ne[]: 存储节点的next指针,即下一个节点的索引,ne是next
// h: 头节点的索引(哨兵节点)
// id: 用于分配新节点的索引,记录当前已使用的节点数量
int e[N], ne[N], h, id;
// mp[]: 映射表,用于快速查找值对应的节点索引(哈希表思想)
int mp[N];// 在链表头部插入一个新节点,值为x
void push_front(int x)
{id++;  // 分配新的节点索引(从1开始)e[id] = x;  // 存储节点的值mp[x] = id;  // 记录值x对应的节点索引,用于快速查找// 将新节点插入到哨兵节点和原头节点之间ne[id] = ne[h];  // 新节点的next指向原头节点ne[h] = id;      // 哨兵节点的next指向新节点,使新节点成为新的头节点
}// 在链表中指定位置插入新节点
// 参数p:插入位置的前驱节点编号
// 参数x:要插入的节点值
void insert(int p, int x) {// 生成新节点的唯一编号(假设id是全局变量,用于记录节点总数)id++;// 存储新节点的值e[id] = x;// 建立值到节点编号的映射(便于快速查找节点)mp[x] = id;// 将新节点的后继指针指向p节点原来的后继节点ne[id] = ne[p];// 将p节点的后继指针指向新节点,完成插入操作ne[p] = id;
}// 删除链表中指定节点的后继节点
// 参数p:要删除节点的前驱节点编号
void erase(int p)
{// 检查p节点是否有后继节点if (ne[p]){// 清除被删除节点的值到编号的映射mp[e[ne[p]]] = 0;// 将p节点的后继指针指向被删除节点的后继节点,完成删除操作ne[p] = ne[ne[p]];}
}// 查找值为x的节点的索引
int find(int x)
{/* 注释掉的是遍历查找方式,时间复杂度O(n)for (int i = ne[h]; i; i = ne[i]){if (i == x) return i;}return -1;*/// 使用映射表查找,时间复杂度O(1)return mp[x];
}// 打印链表中的所有节点索引
void print()
{// 从哨兵节点的下一个节点开始遍历,直到遇到0(链表结束标志)for (int i = ne[h]; i; i = ne[i])cout << i << " ";  // 输出当前节点的索引cout << endl;  // 换行
}
```cpp
int main() {// 初始化链表(哨兵节点h=0,无数据节点)h = 0;id = 0;cout << "初始化空链表,节点索引:";print();  // 空输出// 头插测试:插入10、20、30push_front(10);push_front(20);push_front(30);cout << "\n头插10、20、30后,节点索引:";print();  // 应输出3 2 1cout << "值30的索引:" << find(30) << "(预期3)\n";cout << "值20的索引:" << find(20) << "(预期2)\n";cout << "值10的索引:" << find(10) << "(预期1)\n";// 插入测试:在值20(索引2)后插入25insert(find(20), 25);cout << "\n在20后插入25,节点索引:";print();  // 应输出3 2 4 1cout << "值25的索引:" << find(25) << "(预期4)\n";// 删除测试:删除值25(索引4的前驱是2)erase(find(20));cout << "\n删除25后,节点索引:";print();  // 应输出3 2 1cout << "值25的索引(应清零):" << find(25) << "(预期0)\n";// 查找不存在值测试cout << "\n查找值50的索引:" << find(50) << "(预期0)\n";// 删除头节点测试(删除30,前驱是哨兵h=0)erase(h);cout << "\n删除头节点30后,节点索引:";print();  // 应输出2 1cout << "值30的索引(应清零):" << find(30) << "(预期0)\n";return 0;
}

二.双向链表

1.概念

在这里插入图片描述
以上图片展示的是静态双向链表的实现原理,属于数据结构中链表的一种底层实现方式。

  • 核心原理
    • 通过三个数组模拟双向链表的节点结构:elem数组存储节点数据,prev数组存储前驱节点的下标,next数组存储后继节点的下标。
    • 引入“哨兵节点”(下标为0),用于简化链表的空表、头插、尾插等操作的边界条件处理。
  • 逻辑与存储的对应
    • 逻辑上的节点(如A、B、C、D)在存储结构中通过prevnext数组的下标关联,形成双向的遍历链路。例如节点A的prev为0(哨兵节点),next为3(节点C的下标),从而实现逻辑上的顺序关联。
  • 用途
    这种静态实现方式常用于编程竞赛或对内存管理要求特殊的场景,无需动态分配内存,通过数组下标即可快速维护节点的前驱和后继关系,同时利用哨兵节点避免了许多边界条件的判断,提升代码的简洁性和鲁棒性。

2.示例代码

#pragma once
#include <iostream>
using namespace std;const int N = 1e5 + 10;  // 竞赛中通常预先定义足够大的数组大小template <class T>
struct DoublyLinkedList {T e[N];         // 存储节点值int pre[N], ne[N];  // 前驱、后继数组int idx, head;   // idx: 当前可用节点索引,head: 哨兵节点(固定为0)// 初始化(替代构造函数,竞赛中常用init函数)void init() {head = 0;ne[head] = pre[head] = head;  // 哨兵节点自循环idx = 0;  // 从1开始分配节点(0作为哨兵)}// 头插void push_front(const T& x) {e[++idx] = x;pre[idx] = head;ne[idx] = ne[head];pre[ne[idx]] = idx;ne[head] = idx;}// 在pos前插入void insert(int pos, const T& x) {e[++idx] = x;pre[idx] = pre[pos];ne[idx] = pos;ne[pre[pos]] = idx;pre[pos] = idx;}// 在pos后插入(复用insert,等价于insert(ne[pos], x))void insert_after(int pos, const T& x) {insert(ne[pos], x);}// 删除pos节点void erase(int pos) {ne[pre[pos]] = ne[pos];pre[ne[pos]] = pre[pos];}// 查找值为x的节点索引int find(const T& x) {for (int i = ne[head]; i != head; i = ne[i]) {if (e[i] == x) return i;}return -1;}// 打印链表void print() {for (int i = ne[head]; i != head; i = ne[i]) {cout << e[i] << " ";}cout << endl;}
};
// 测试函数
int main() {// 测试整数类型双向链表DoublyLinkedList<int> list;list.init();cout << "=== 初始化空链表 ===" << endl;cout << "初始链表(空):";list.print();  // 应输出空行// 测试头插list.push_front(3);list.push_front(2);list.push_front(1);cout << "\n=== 头插 1, 2, 3 后 ===" << endl;cout << "链表内容:";list.print();  // 应输出:1 2 3// 测试查找int pos = list.find(2);cout << "\n查找值为2的节点索引:" << pos << endl;  // 应输出2(插入顺序为1→2→3,索引依次为1,2,3)int invalid_pos = list.find(100);cout << "查找值为100的节点索引:" << invalid_pos << endl;  // 应输出-1// 测试在pos前插入(在2前面插入5)list.insert(pos, 5);cout << "\n=== 在2前面插入5后 ===" << endl;cout << "链表内容:";list.print();  // 应输出:1 5 2 3// 测试在pos后插入(在2后面插入6)list.insert_after(pos, 6);cout << "\n=== 在2后面插入6后 ===" << endl;cout << "链表内容:";list.print();  // 应输出:1 5 2 6 3// 测试删除节点(删除值为5的节点)int del_pos = list.find(5);list.erase(del_pos);cout << "\n=== 删除5后 ===" << endl;cout << "链表内容:";list.print();  // 应输出:1 2 6 3// 测试删除头节点(1)int head_node_pos = list.find(1);list.erase(head_node_pos);cout << "\n=== 删除1后 ===" << endl;cout << "链表内容:";list.print();  // 应输出:2 6 3// 测试删除尾节点(3)int tail_node_pos = list.find(3);list.erase(tail_node_pos);cout << "\n=== 删除3后 ===" << endl;cout << "链表内容:";list.print();  // 应输出:2 6return 0;
}
http://www.dtcms.com/a/515831.html

相关文章:

  • 【蓝队面试】Struts2漏洞原理与面试中常见的问题
  • 基于3D激光点云的障碍物检测与跟踪---(2)点云聚类
  • 测试 gRPC 调用
  • **发散创新:Web Components的深度探索与实践**随着Web技术的飞速发展,Web Components作为一
  • spark组件-spark sql
  • Copy Cell 解释
  • 列表使用练习题
  • 杭州悦数与复旦大学共建“先进金融图技术”校企联合研究中心”正式揭牌
  • 网站怎么做搜索栏蓝海网站建设
  • Win11系统更新导致博图v15.1授权报错
  • 项目案例作业3(AI辅助):使用DAO模式改造学生信息管理系统
  • 责任链模式:灵活处理请求的设计模式
  • 什么是邮件打开率?邮件营销打开率影响因素有哪些?
  • 未来的 AI 操作系统(七)——认知共生:AI 与人类的协作边界
  • 快速入门LangChain4j Ollama本地部署与阿里百炼请求大模型
  • 虫情测报灯:精准预警,守护农田安全
  • 如何设置电脑分辨率和显示缩放
  • 【GESP】C++四级真题 luogu-B4069 [GESP202412 四级] 字符排序
  • Solana 官宣中文名「索拉拉」,中文 Meme 叙事正成为链上新主流
  • 《巨神军师》在电脑上多开不同窗口不同IP的教程
  • led灯 东莞网站建设公司注册资金减少意味着什么
  • 如何正确理解flink 消费kafka时的watermark
  • 未来的 AI 操作系统(六)——从“大模型”到“小智能”:Agent生态的去中心化演化
  • [人形机器人]宇树G1拆解分析 - 主控部分
  • 建筑毕业设计代做网站建筑网格组织
  • 面向汽车硬件安全模块的后量子安全架构
  • 广州网站制作哪家公司好做视频用的网站有哪些
  • Petalinux高版本自动登录与开机自启动完全指南
  • 用 AI 编码代理重塑前后端交互测试的未来
  • PID算法基础知识