快速选择:修订间差异
AIContentBot(留言 | 贡献) 新增快速选择与KMP:名次划分、前缀回退、可运行Python及过程图 |
AIContentBot(留言 | 贡献) 修复 Python 代码语法高亮、行号与算法分步动画 |
||
| 第16行: | 第16行: | ||
下面代码把目标名次改成从 0 开始的全数组下标 <math>k-1</math>。它在一份副本上原地做三路划分,因此即使左右边界不断移动,目标的'''全局'''下标也不必每次重算。循环保持:所求元素仍在 <math>[lo,hi)</math> 中。 | 下面代码把目标名次改成从 0 开始的全数组下标 <math>k-1</math>。它在一份副本上原地做三路划分,因此即使左右边界不断移动,目标的'''全局'''下标也不必每次重算。循环保持:所求元素仍在 <math>[lo,hi)</math> 中。 | ||
<math-experiment type="algorithm" demo="quick-select" /> | |||
== Python 实现 == | == Python 实现 == | ||
输入是非空、元素之间可比较的序列和 <math>1\le k\le n</math>;函数返回一个值,不改动传入的序列。为了与图逐步对应,基准固定为当前区间首项。 | 输入是非空、元素之间可比较的序列和 <math>1\le k\le n</math>;函数返回一个值,不改动传入的序列。为了与图逐步对应,基准固定为当前区间首项。 | ||
< | <div class="math-code-example"> | ||
<div class="math-code-language">Python 3</div> | |||
<pre class="math-code-source" data-language="python"> | |||
def quick_select(items, k): | def quick_select(items, k): | ||
if not 1 | if not 1 <= k <= len(items): | ||
raise ValueError("k must be between 1 and len(items)") | raise ValueError("k must be between 1 and len(items)") | ||
a = list(items) | a = list(items) | ||
| 第32行: | 第36行: | ||
lt = i = lo | lt = i = lo | ||
gt = hi | gt = hi | ||
while i | while i < gt: | ||
if a[i] | if a[i] < pivot: | ||
a[lt], a[i] = a[i], a[lt] | a[lt], a[i] = a[i], a[lt] | ||
lt += 1 | lt += 1 | ||
i += 1 | i += 1 | ||
elif a[i] | elif a[i] > pivot: | ||
gt -= 1 | gt -= 1 | ||
a[i], a[gt] = a[gt], a[i] | a[i], a[gt] = a[gt], a[i] | ||
| 第43行: | 第47行: | ||
i += 1 | i += 1 | ||
if target | if target < lt: | ||
hi = lt | hi = lt | ||
elif target | elif target >= gt: | ||
lo = gt | lo = gt | ||
else: | else: | ||
| 第58行: | 第62行: | ||
assert values == [6, 3, 6, 2, 8, 3, 5] | assert values == [6, 3, 6, 2, 8, 3, 5] | ||
</pre> | </pre> | ||
</div> | |||
== 代价与排序的区别 == | == 代价与排序的区别 == | ||
2026年9月24日 (四) 02:27的最新版本
快速选择(quickselect)在未排序的序列中找第 小的值。它借用快速排序的划分步骤,却只继续处理包含目标名次的那一段;不需要把其余元素排成有序。本文的名次从 1 开始:最小值是第 1 小,重复值分别占据各自的名次。
名次落在哪一段
沿用排序词条的七个数 ,要找第 4 小。以首项 6 为基准,三路划分可能得到 左段四个数都小于 6,因而占第 1 至第 4 名;两个 6 占第 5、6 名;8 占第 7 名。目标落在左段,右两段此后都不用再看。左段以 3 为基准再划分为 ;扣除一个小于 3 的数和两个等于 3 的数后,第 4 小就是右段唯一的 5。
例如问第 5 小时,第一轮就落在两个 6 的等值段内,直接返回 6。这也是三路划分处理重复值的好处:若把“等于”同时算入左右两边,名次会重叠,后续无法正确扣除已经排除的元素。
划分后的名次不变式
设当前待查区间被分成长度为 的小于、等于、大于三段,目标在当前区间内排第 。若 ,目标在小于段,名次仍为 ;若 ,答案等于基准;否则到大于段继续找第 小。三种情况互斥且穷尽。
划分只交换元素,不改变元素的多重集合;小于段任何值都排在等值段之前,等值段又排在大于段之前。因此上述名次扣除是正确的,不依赖各段内部已经有序。基准至少占据等值段的一个位置;只要尚未返回,下一轮区间严格缩小,有限输入必会停止。
下面代码把目标名次改成从 0 开始的全数组下标 。它在一份副本上原地做三路划分,因此即使左右边界不断移动,目标的全局下标也不必每次重算。循环保持:所求元素仍在 中。
快速选择的名次定位
每次划分后只保留第 4 小所在的区间。
静态配图与完整推导见本节正文;交互演示需浏览器启用 JavaScript。
Python 实现
输入是非空、元素之间可比较的序列和 ;函数返回一个值,不改动传入的序列。为了与图逐步对应,基准固定为当前区间首项。
def quick_select(items, k):
if not 1 <= k <= len(items):
raise ValueError("k must be between 1 and len(items)")
a = list(items)
target = k - 1
lo, hi = 0, len(a)
while True:
pivot = a[lo]
lt = i = lo
gt = hi
while i < gt:
if a[i] < pivot:
a[lt], a[i] = a[i], a[lt]
lt += 1
i += 1
elif a[i] > pivot:
gt -= 1
a[i], a[gt] = a[gt], a[i]
else:
i += 1
if target < lt:
hi = lt
elif target >= gt:
lo = gt
else:
return a[target]
if __name__ == "__main__":
values = [6, 3, 6, 2, 8, 3, 5]
assert quick_select(values, 4) == 5
assert quick_select(values, 5) == 6
assert [quick_select(values, k) for k in range(1, 8)] == sorted(values)
assert values == [6, 3, 6, 2, 8, 3, 5]
代价与排序的区别
一次三路划分扫描当前区间一次。若每轮保留的严格一侧至多占固定比例,扫描量形成 ,其中 。本代码固定首项为基准,有序且互异的输入可能每次只排除一个值,最坏时间为 ;不能把随机基准的期望线性结论直接套到这里。列表副本需 空间,循环和划分下标本身只需 额外空间。
快速排序要把两侧都排好,平衡时总时间为 ;快速选择每轮只保留一侧,目标仅是一个名次对应的值。若需要整张有序表,应直接排序;若只问少数名次,选择可省去无关区间的递归工作。
参考资料
- TheAlgorithms/Python:quick_select.py:同一问题的另一种三列表实现;本文代码、名次约定和例子独立编写。
- Sedgewick、Wayne,Quicksort and Selection:划分与选择的关系。
- 先修:算法与复杂度、快速排序;相关:归并排序。