坦白说,排序算法学了这么多年,真正能在日常开发里随口说清实现细节的,反而往往是那几个“看着简单”的算法。选择排序和计数排序就是两个很有意思的代表:一个靠朴素的“每轮挑最小”走天下,一个靠“数清楚每个值出现了多少次”另辟蹊径。前者我经常拿来给新人讲“原地理清思路”,后者则是我在遇到特定范围数据时真正会考虑用到的工程方案。
这篇东西就基于我实际的调试过程来写,不做教科书式的罗列。我会把选择排序和计数排序的思路、写法、复杂度、稳定性、常用坑全部交代一遍,并用真实代码走通整个流程。适合刚学排序、准备面试复习,或者工作中突然想找一个“不用动脑的稳定排序方案”的同学。如果你能跟着代码敲一遍,收获会比只看一遍大很多。
1. 两种排序算法到底在解决什么问题
排序这件事,本质上是在回答“如何将一组元素按照某个规则重新排列”。选择排序和计数排序解决的问题是一样的,但思考角度完全不同:一个站在“位置”上看问题,一个站在“值”上看问题。
1.1 选择排序的思路是先锁定位置再找元素
玩过扑克牌理牌的朋友应该都有手感:把最小的一张牌放到最左边,然后从剩下的牌里再找最小的放到第二位,如此反复。选择排序就是这么干的。
选择排序的核心思想非常直白:先找到全数组最小的元素,把它和下标 0 的位置交换;再从下标 1 开始的区间里找最小元素,和下标 1 的位置交换;继续下去,直到整个数组有序。
这个过程中间没有“插入”和“腾位置”这种动作,只有两个步骤在循环:扫描区间找最小值,然后把最小值放到目标位置。所以每轮确认的只是位置,至于区间内其他元素的相对顺序如何被打乱,它其实完全不关心。
这个思路有个性格特点:它的交换次数非常少,每一轮最多只做一次交换。就算数组完全逆序,它也照样一轮只换一次。这一特点在数据元素本身很“重”的时候是有价值的,因为频繁交换大对象往往比比较更昂贵。
1.2 计数排序的思路是先数清楚每个值再铺回去
与选择排序逐个比较不同,计数排序试图绕开“比较”这种操作。它的出发点是:如果我就是知道每个数出现过多少次,那直接按从小到大的顺序把这些数铺回去不就行了吗?
比如数组 [2, 1, 2, 3, 1, 0],值范围是 0 到 3。先数一数:0 出现 1 次,1 出现 2 次,2 出现 2 次,3 出现 1 次。这时候已经能还原出排好序的结果了:[0, 1, 1, 2, 2, 3]。整个过程中没做过任何一次元素之间的比较,靠的是一个“计数数组”。
但请注意,这种思路有一个明显的适用前提:数据必须是非负整数或可以被映射成非负整数,且取值范围不能过分散。计数排序的时间复杂度取决于数据范围 k,范围太大时它反而不划算。
1.3 两种思路的差异与应用场景
如果把排序算法按“是否基于比较”分类,选择排序属于典型的比较排序,计数排序则属于非比较排序。这个差异直接决定了它们的性能特征:
| 观察维度 | 选择排序 | 计数排序 |
|---|---|---|
| 基本策略 | 每轮找最小并交换 | 统计频次后重建序列 |
| 时间复杂度 | O(n²) | O(n + k),k 为数据范围 |
| 空间复杂度 | O(1),原地排序 | O(k),需要额外计数数组 |
| 稳定性 | 不稳定 | 可稳定 |
| 适用数据 | 无限制 | 整数且范围适中 |
有没有发现,计数排序在某些场景下是可能做到 O(n) 级别时间的。这正是它迷人的地方:当一个问题的数据范围很集中时,理论上你是可以跑赢那些 O(n log n) 的“高级算法”的。但在数据范围非常稀疏的场景下,比如一亿个数里只有 0 和 99999999 两个值,k 就会大到离谱,计数排序瞬间变得不划算了。
理解选择排序和计数排序之间的这种互补关系,是我个人觉得排序入门阶段最重要的一件事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 选择排序的完整拆解与实操实现
2.1 从思路到代码:如何一步一步写出正确的实现
选择排序的代码结构是一个双重循环,外层循环控制“我们要把位置 i 放上什么值”,内层循环负责“从 i 到末尾找最小值所在下标”。
下面我用 Python 写一个最基础版,这个版本是我平时给同事讲思路时用的:
python复制def selection_sort(arr):
n = len(arr)
for i in range(n - 1):
min_index = i
for j in range(i + 1, n):
if arr[j] < arr[min_index]:
min_index = j
if min_index != i:
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
这里有个细节容易被忽略:外层循环只走到 n - 1。最后一个元素其实不需要再处理了,因为当前面 n-1 个位置都已经放上了正确元素时,剩下的那一个位置自然也就是正确的。
内层循环从 i + 1 开始找最小值
