(c)有序列表

官官方信息技术老师·14 页·深入(追求细节与边界)·0 次浏览·38 天前
数据结构链表插入删除深入细节

有序列表:唯一化与查找

看完你能徒手画出插入与删除每一步指针变化并说清边界情况

按 空格/→ 演示下一步

问一句
1 / 14 页

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

数据结构链表插入删除深入细节

有序列表:唯一化与查找

看完你能徒手画出插入与删除每一步指针变化并说清边界情况

1第 1 页 · 有序列表:唯一化与查找

什么是有序列表

查字典时你敢直接翻到中间,是因为确定字按拼音排过序。有序列表就是这种「排好队」的存储:元素按大小依次摆放,查找因此有了质变。

有序性
元素按大小关系排列,相邻元素必满足前小后大
随机访问
支持 O(1) 跳到任意位置,为二分查找铺路
中位数定位
有序后 O(1) 取中间元素,无序时做不到
字典的拼音排序对应 →有序列表

字典按拼音排页码,对应列表按下标定位元素

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

有序 vs 无序

都叫列表,但一个对顺序有承诺、一个没有,这决定了你能做什么操作。

有序列表
  • 顺序即信息:位置有含义
  • 可二分查找:O(log n)
  • 插入删除需保持序,成本高
无序列表
  • 顺序无意义:仅是存放次序
  • 只能线性扫描:O(n)
  • 插入删除随意,成本更低
当需要按序快速查找时选有序;当只做频繁增删、顺序无关时选无序。
3第 3 页 · 有序 vs 无序

唯一化·构思

上一页说过,有序列表的元素按大小排好了队。排队有个副产品:值相同的节点只能紧挨着坐在一起。于是「去掉重复」这件在无序表里很麻烦的事,在这里变得出奇地简单。

重复必相邻
排序后所有相等元素被挤进连续区段,不会散落各处
一次遍历
指针逐个比较相邻节点,当前与后继相等则摘除后继
边界情况
空表直接返回;连续三个以上相同值需循环摘除同一位置
图书馆还书对应 →唯一化遍历

书按类别排好后,同名书必并排,巡一遍书架即可清点

T(n)=O(n),S(n)=O(1)T(n)=O(n),\quad S(n)=O(1)
4第 4 页 · 唯一化·构思

唯一化的推进过程

一次线性扫描,p 逐个前进,只和前驱比较,遇重复即摘除。

1
p 从第二个出发
首个元素必然不重复,它是基准,无需检查
2
p 与前驱比较
有序性保证重复必相邻,只比前驱即可
3
值相等则摘除
前驱绕过 p 指向后继,释放 p 的内存
4
不等则 p 前进
p 成为新的前驱,继续扫描直到表尾
5第 5 页 · 唯一化的推进过程

唯一化·实现

python

用 Python 把“比较、判断、摘除”三步写成真实可运行的 dedup 循环。

代码高亮加载中…

有序性保证重复元素必相邻,所以只需比较 nums[i] 与后一个;发现相等就 pop 摘除且 i 不动,因为新邻居可能仍重复。

6第 6 页 · 唯一化·实现

唯一化的复杂度与边界

主路径是双指针唯一化:q 是读位置,慢指针是写位置;下方两条是特殊边界分支。

图解渲染中…
c1:写位≤q:写指针从不越过读指针,这是 O(n) 且原地安全的关键d1:遍历结束后只保留前 k 个写位元素,后面的直接丢弃a3:空表无元素可比较,循环不执行,天然正确a4:全重复时写位只在首位推进一次,k 恒为 1
7第 7 页 · 唯一化的复杂度与边界

有序列表的查找接口

前面唯一化已经把重复元素清理干净,列表变得规整。那怎么在这样一串有序数字里快速找到目标?查字典时你不会从第一页翻起,而是直接翻到中间——这就是 search 接口的起点。

search 语义
返回不小于 e 的秩最小的元素,可插入新元素且整体仍有序
约定返回位置
找到则返回命中秩;否则返回首个大于 e 的位置
为何不用 locate
无需判等,仅靠大小比较即可定位,逻辑更统一
查字典翻中间对应 →search 约定

字典翻到的页码对应 search 返回的插入位置

8第 8 页 · 有序列表的查找接口

二分查找的推进

二分查找每轮只做一件事:看中点,砍一半,直到目标无处可藏。

1
取中点
在当前区间 [lo, hi] 取中间元素,作为唯一被比较对象
2
比中点
目标与中点比大小:相等则命中,本轮结束
3
砍一半
目标偏大则丢弃左半,偏小则丢弃右半,中点一并排除
4
缩区间
新区间是旧区间的一半;lo 越过 hi 时确认不存在
9第 9 页 · 二分查找的推进

查找的边界情形

沿箭头读:每次比较走一条分支,最后看区间是否变空来区分命中、失败与边界返回。

图解渲染中…
g1:失败时 hi 停在最后一个不大于目标的节点上,无则无意义f1:区间空即 lo>hi,循环结束的时刻
10第 10 页 · 查找的边界情形

有序查找 vs 无序查找

都叫"查找",成本却差一个数量级:区别不在算法写法,而在列表结构承诺了什么。

无序查找
  • 顺序扫描,逐个比对
  • O(n):n 变大成本线性涨
  • 数据乱放,任何元素都可能命中
有序查找(二分)
  • 取中点折半,排除一半
  • O(log n):翻倍只多查一次
  • 有序承诺:比中点即知在哪半边
正确理解:二分快不是因为写得巧,而是有序性提供了可利用的结构信息。
11第 11 页 · 有序查找 vs 无序查找

自测:唯一化与查找

点击作答

对有序列表做二分查找,为什么每比较一次就能排除一半元素?

12第 12 页 · 自测:唯一化与查找

要点收束

  • ✓有序性是前提:查找快,依赖元素已排序
  • ✓唯一化靠相邻比较,一遍扫描即可完成
  • ✓有序把查找从逐个比对变为二分折半
  • ✓有序性、唯一化、查找互为因果的闭环
延伸主题:二分插入与平衡结构跳表:有序链表加速去重的稳定版:保留首次出现
13第 13 页 · 要点收束

课后思考

先自己想几分钟,再对照参考答案;这些题没有唯一解。

1唯一化为什么必须先有序才高效?如果列表本来无序,代价花在了哪里?

参考答案有序后重复元素必相邻,一遍扫描即可完成;无序时要么两两比较(O(n²)),要么先排序再扫——代价转移到了排序那一步,整体仍是 O(n log n)。

2二分查找每次排除一半,如果改成每次排除三分之一,界会怎样变化?

参考答案每次剩原来的比例 k,迭代次数是 log 底 k 的对数,仍是 O(log n)。底数只是常数因子,渐近界不变——对数界对分割比例不敏感。

3有序带来的好处,换个数据结构(比如链表)还成立吗?缺了什么前提?

参考答案链表即使有序,也无法 O(1) 访问中位元素,二分退化。有序高效的前提是随机访问;有序性本身不自动带来效率,还要配上合适的存取方式。

14第 14 页 · 课后思考