(b)无序列表

官官方信息技术老师·14 页·深入(追求细节与边界)·0 次浏览·38 天前
数据结构动态存储图解深入细节

无序列表

看完你能亲手画出插入、删除、查找、遍历与扩容的每一步内存变化

按 空格/→ 演示下一步

问一句
1 / 14 页

全部页面点击任意一页,跳回舞台从这页播放

数据结构动态存储图解深入细节

无序列表

看完你能亲手画出插入、删除、查找、遍历与扩容的每一步内存变化

1第 1 页 · 无序列表

什么是无序列表

前一页看到列表由节点链接而成。现在有个关键问题:每个节点只知道下一个是谁,不记得自己是第几个——这和向量能直接按下标取元素完全不同。这一页就讨论由此带来的结构差异。

节点单向链接
节点之间靠 next 指针相连,形成一条线性链
节点不知位次
节点本身不存储编号,整体顺序只在遍历中体现
循秩不可直取
想访问第 r 个元素,必须从头沿链走 r 步,无法跳跃
循位访问
列表的自然访问方式是沿位置逐个移动,而非按秩跳转
寻宝游戏对应 →无序列表

每张线索卡只指向下一张,想知道第几张只能一张张翻,卡上不印编号

2第 2 页 · 什么是无序列表

循秩访问

上页说无序列表是节点手拉手串起来的,那已知第 5 个元素在哪?你没法像数组那样一步跳过去——只能从第一个节点出发,顺着引用一个一个数过去。这条逐个移动的路线,就叫循秩访问。

秩是逻辑位置
列表本身不存位置号,秩 i 只是「第 i 个」这个逻辑序号
从首节点出发
拿着头引用,从第一个节点开始移动
沿引用逐个走
每步读当前节点的 next,跳到下一个,共走 i 步
代价 O(i)
访问第 i 个元素需要 i 步,越靠后越慢
摸黑走火车车厢对应 →循秩访问

从第 1 节车厢开始逐节往后穿行,对应沿 next 引用逐个移动

3第 3 页 · 循秩访问

循秩 vs 循位

「列表也能按下标访问」是常见误解——同样是第 k 项,取法完全不同。

列表·循位访问
  • 只能从头沿引用逐个挪过去
  • 取第 k 项需 k 次跳转,O(k)
  • 插入删除只需改指针,O(1)
向量·循秩访问
  • 物理地址连续,秩即偏移量
  • 取第 k 项一次算出地址,O(1)
  • 插入删除需整体搬移,O(n)
读多写少、按下标取用选向量;频繁插删、位置动态变化选列表。
4第 4 页 · 循秩 vs 循位

查找的过程

在无序列表里找一个元素,只能从某个节点出发,沿引用一步步比对。

1
从头节点出发
拿到第一个节点的引用,作为查找起点
2
沿引用前进
读当前节点的 next,走到下一个节点
3
逐个比对
检查当前节点的数据是否等于目标值
4
命中即返回
相等就停下,返回当前位置作为结果
5
到尾未命中
next 为空说明查完全表,返回查找失败
5第 5 页 · 查找的过程

查找的实现

python

一段真实的 Python 线性查找:逐个比对,找不到就按约定返回 -1。

代码高亮加载中…

循环不变式:[0,lo) 已排除、[lo,hi) 待查。区间空即失败,统一返回 -1,调用方用返回值判断命中与否。

6第 6 页 · 查找的实现

插入节点

在链表两个节点之间插入新节点,关键是先接后断,指针重接顺序不能错。

1
定位前驱
从头沿 next 走到插入位置的前一个节点 p,新节点将挂在 p 之后
2
新节点接后继
先执行 s->next = p->next,让新节点抓住 p 原来的后继
3
前驱接新节点
再执行 p->next = s,把新节点挂入链中,插入完成
7第 7 页 · 插入节点

删除节点

摘除节点分四步:先定位,再重接,后清理,最后归还空间。

1
找到目标
从头部沿 next 逐个遍历,直到定位待删节点及其前驱
2
前驱越过后继
前驱的 next 改指目标的后继,链表绕开目标
3
断开目标引用
把目标的 next 置空,避免悬挂引用和内存泄漏
4
归还内存
释放目标节点空间,链表长度减一
8第 8 页 · 删除节点

复制列表

从原列表尾部出发,每看一个节点就头插进新列表,循环到 p 为空。

图解渲染中…
b1:头插:新节点插到新列表头部,顺序自动反转d1:循环终止条件:p 走过头节点即结束
9第 9 页 · 复制列表

析构与清空

cpp

看看无序列表如何逐个删掉节点,把内存干净归还给系统。

代码高亮加载中…

析构 = clear 循环逐个 remove 数据节点 + 最后删两个哨兵;每次 remove 摘链后 delete 归还内存,复杂度 O(n)。

10第 10 页 · 析构与清空

唯一化

唯一化靠每个新节点向前查找,一旦遇到相同元素就当场删除,保证全表唯一。

1
从首节点起
依次访问每个节点,把它当作待检查对象
2
向前查找
在它之前的所有节点里查找相同元素
3
命中即删
找到重复值就摘除当前节点,指针后移
4
未命中继续
没找到就保留节点,处理下一个节点
5
扫描完毕
所有节点检查完,列表中不存在重复元素
11第 11 页 · 唯一化

自测一下

点击作答

在无序列表的第 k 个位置找到节点并删除它,删除本身是 O(1),但整个操作为什么是 O(k)?

12第 12 页 · 自测一下

要点收束

  • ✓列表的本质:逻辑相邻由指针维系,物理位置无关
  • ✓循秩 O(n) 是一切操作复杂度的根源
  • ✓查找/插入/删除:先定位 O(n),动作本身 O(1)
  • ✓复制与析构是镜像:一个逐点 new,一个逐点 delete
  • ✓唯一化 = 排序场景下双指针,无序场景下查找去重
延伸主题:有序列表:唯一化为何能降到 O(n)选择排序与插入排序的列表实现从列表到链表变体:双向与循环
13第 13 页 · 要点收束

课后思考

先自己想,别急着看参考答案——这些问题值得多琢磨一会。

1唯一化用排序能更快,但为什么列表偏要用双重循环的 O(n²) 做法?

参考答案顺着「循位访问」想:列表不支持循秩,排序需要随机访问才划算;在列表上排序反而更慢。提示:数据结构的选择决定了算法的形态。

2如果插入操作远多于查找,列表和数组该怎么选?换成频繁按位置读取呢?

参考答案插入多时列表占优:找到位置后 O(1) 挂接,数组要整体搬移。频繁按秩读取则反过来,数组 O(1) 循秩完胜列表的 O(n) 走链。

3析构函数里为什么不能直接 delete head,而要逐个节点释放?边界在哪?

参考答案delete head 只释放头节点本身,后续节点全部泄漏。逐个走链释放是因为节点在堆上离散分布、彼此独立。边界:空列表要能直接安全返回。

14第 14 页 · 课后思考