CatBase 排序算法实战,从内置 sorted 到手写冒泡
排序是程序员的必修课,也是检验一门新语言的最好切口。
原因很实在。一个排序算法下来,列表、循环、函数、比较运算四样东西全用上了。这四样是日常编码里出现频率最高的基础设施,排序写顺了,这门语言就算摸到门了。
前三篇我们写了 Hello World、配置体检工具、性能测试。这篇换个姿势,用算法来练手。
先看现成的答案
CatBase 手册列出的内置函数名里,有 sorted 和 reversed。排序这件事,语言本身给了现成的答案:
def main(args: list[str]) {
nums: list[int] = [5, 3, 8, 1, 9, 2]
print("原始: ", nums, "\n")
print("排序后: ", sorted(nums), "\n")
}
说句实用的建议:生产环境请直接用内置的 sorted()。它底层是成熟的排序实现,比你手写的任何版本都快,也比你手写的任何版本都难写错。
那为什么还要手写?三个理由。面试要考,原理要懂,练手要真。手写一遍冒泡排序,你对「比较」和「交换」的理解会深一个层次,这个层次在看框架源码时会回来帮你。
下面三个算法,全部只用手册里已确认的语法写成。
冒泡排序
原理一句话:反复比较相邻的两个数,大的往后挪,每一轮把当前最大的数送到队尾。
def bubble_sort(nums: list[int]) -> list[int] {
n: int = len(nums)
i: int = 0
while i < n {
j: int = 0
while j < n - 1 - i {
if nums[j] > nums[j + 1] {
tmp: int = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = tmp
}
j = j + 1
}
i = i + 1
}
return nums
}
代码里用到的语法,逐个对一遍手册:while 循环、if 比较、len()、列表索引读写、整数加减,全部有官方示例支撑。
注意 n - 1 - i 这个细节。每跑完一轮,队尾就固定一个最大值,所以内层循环的上界每轮减 1。少写这个优化,结果一样对,只是多做一些无用比较。
选择排序
原理换了个思路:每一轮在未排序区间里找出最小值,直接放到区间开头。
def selection_sort(nums: list[int]) -> list[int] {
n: int = len(nums)
i: int = 0
while i < n {
min_idx: int = i
j: int = i + 1
while j < n {
if nums[j] < nums[min_idx] {
min_idx = j
}
j = j + 1
}
if min_idx > i {
tmp: int = nums[i]
nums[i] = nums[min_idx]
nums[min_idx] = tmp
}
i = i + 1
}
return nums
}
它和冒泡排序的交换次数完全不是一个量级。冒泡可能每轮都在换,选择排序一轮最多换一次。数据搬运成本高的场景,这个差别很可观。
顺带一个写法上的取舍:判断「最小值是否就是当前位置」时,这里用 min_idx > i,没用 !=。因为 min_idx 从 i 出发只会越找越大,两者相等就意味着没发生替换,一个 > 就够了。
插入排序
原理最接近人整理扑克牌:左手是排好的部分,每次把新牌插到该在的位置。
def insertion_sort(nums: list[int]) -> list[int] {
n: int = len(nums)
i: int = 1
while i < n {
key: int = nums[i]
j: int = i - 1
while j >= 0 and nums[j] > key {
nums[j + 1] = nums[j]
j = j - 1
}
nums[j + 1] = key
i = i + 1
}
return nums
}
三个算法里,插入排序是唯一在「数据基本有序」时表现优异的。接近有序的数据跑它,时间复杂度会往 O(n) 靠。这也是为什么很多成熟排序实现,在数据量变小之后会切到插入排序。
一起跑一遍
写三个不如验一次。把四个方案摆到同一个乱序列表上,看结果是否一致:
def bubble_sort(nums: list[int]) -> list[int] {
n: int = len(nums)
i: int = 0
while i < n {
j: int = 0
while j < n - 1 - i {
if nums[j] > nums[j + 1] {
tmp: int = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = tmp
}
j = j + 1
}
i = i + 1
}
return nums
}
def selection_sort(nums: list[int]) -> list[int] {
n: int = len(nums)
i: int = 0
while i < n {
min_idx: int = i
j: int = i + 1
while j < n {
if nums[j] < nums[min_idx] {
min_idx = j
}
j = j + 1
}
if min_idx > i {
tmp: int = nums[i]
nums[i] = nums[min_idx]
nums[min_idx] = tmp
}
i = i + 1
}
return nums
}
def insertion_sort(nums: list[int]) -> list[int] {
n: int = len(nums)
i: int = 1
while i < n {
key: int = nums[i]
j: int = i - 1
while j >= 0 and nums[j] > key {
nums[j + 1] = nums[j]
j = j - 1
}
nums[j + 1] = key
i = i + 1
}
return nums
}
def main(args: list[str]) {
base: list[int] = [5, 3, 8, 1, 9, 2, 7, 4, 6]
l1: list[int] = [5, 3, 8, 1, 9, 2, 7, 4, 6]
l2: list[int] = [5, 3, 8, 1, 9, 2, 7, 4, 6]
l3: list[int] = [5, 3, 8, 1, 9, 2, 7, 4, 6]
print("原始数据 : ", base, "\n")
print("内置 sorted: ", sorted(base), "\n")
print("冒泡排序 : ", bubble_sort(l1), "\n")
print("选择排序 : ", selection_sort(l2), "\n")
print("插入排序 : ", insertion_sort(l3), "\n")
}
编译运行:
./bin/catbasecc sort.cat
./sort
四行排序结果应该完全一致。这就是验证算法正确性最朴素的办法:跟一个你信得过的实现对答案。
复杂度对比
| 算法 | 平均时间 | 最坏时间 | 额外空间 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 内置 sorted | O(n log n) | O(n log n) | O(n) | 稳定 |
三个手写版本的时间复杂度都是 O(n²)。数据量上千之后,和 O(n log n) 的差距会拉开到肉眼可见。所以手写是为了理解,不是为了上岗。
几句实话
这篇有两个地方需要你本地验证,我把话说在前面。
第一,变量索引赋值和嵌套循环,手册里还没有官方示例。常量索引赋值 nums[0] = 10 和变量索引读取 args[i] 都有示例,两者组合成的 nums[j] = x 本文用得很密,但手册确实没写。这是全文唯一需要你跑一遍确认的点。
第二,手册里没有递归的示例,所以快排和归并排序这篇没写。等官方文档确认了递归,下一篇补上。
另外 sorted() 只是手册列出的内置函数名,签名没有细写,具体用法以你本地版本为准。
版本还是 0.6,标准库第 7 章尚未补全。遇到跑不通的代码,把报错丢进官方微信群,也算帮他们补文档了。
写在最后
手写排序这件事,性价比高得离谱。三个算法加起来不到 80 行,换来的是对列表、循环、函数、比较运算的完整理解。
你还记得第一次写冒泡排序是跟谁学的吗?评论区聊聊。
*参考来源:CatBase 官网 catbase-lang.com、GitHub 仓库 DoRobotAi/catbase-lang README 与 doc/catbase.md。复杂度表格为算法通用结论。

浙公网安备 33010602011771号