⌨️ 第四章 流程图与算法
📄 一句话概括

排序是算法界的 Hello World。选择排序:每轮从乱堆里挑最小的放前头;插入排序:像理扑克牌,抓一张、插进左边已理好的手牌——都是 O(n²) 的慢功夫,但思想是一切高速排序的地基。

📝 本节和前面几节的关系

第四章前半的三大件(分支、循环、函数)在本篇合体:排序是【分支+双层循环+交换】的综合练习。回收 3-8 的数组(排序的主战场)与 3-14 的交换(无临时变量的三连赋值)。

一、选择排序:每轮挑最小

思路(升序):第 i 轮从【第 i 个到末尾】里挑最小的,跟第 i 个交换——前半段永远是【已理好的最小前缀】。

[5, 3, 8, 1, 9, 2]
第1轮:全堆里最小是 1 -> 和 5 换  [1, 3, 8, 5, 9, 2]
第2轮:剩下里最小是 2 -> 和 3 换  [1, 2, 8, 5, 9, 3]
……直到只剩一个
a = [5, 3, 8, 1, 9, 2]
for i in range(len(a) - 1):
    m = i                          # 先假设当前位置最小
    for j in range(i + 1, len(a)):
        if a[j] < a[m]:
            m = j                  # 记下更小的下标(只记不换)
    a[i], a[m] = a[m], a[i]        # 一轮只换一次
print(a)                           # [1, 2, 3, 5, 8, 9]

性格:交换次数最少(每轮最多一次);比较次数固定 n(n-1)/2 次——数据多乱都这个量(O(n²))。

二、插入排序:理扑克牌

思路:左边的手牌已经理好;每次从右边抓一张新牌,从后往前比、插进该插的位置。

b = [5, 3, 8, 1, 9, 2]
for i in range(1, len(b)):
    key = b[i]                     # 抓起的新牌
    j = i - 1
    while j >= 0 and b[j] > key:   # 左边比它大的都往后挪
        b[j + 1] = b[j]
        j -= 1
    b[j + 1] = key                 # 插进空出的坑
print(b)                           # [1, 2, 3, 5, 8, 9]

性格:最坏 O(n²),但对【基本有序】的数据接近 O(n)(几乎不用挪)——这是它至今活着的理由:Timsort(Python 的 sorted 内置算法)就用插入排序处理小段与近有序段(4-13 展开)。

三、两者对比一张表

选择排序 插入排序
每轮动作 挑最小、换到前头 抓新牌、往左插
比较次数 固定 n(n-1)/2 最好 n 次、最坏 n(n-1)/2
交换(挪动)次数 每轮最多 1 次 多(挪一个算一次)
对基本有序数据 没有优势 接近 O(n),显著更快
稳定性 不稳定 稳定

四、动手 Demo:同一副牌两种理法

def selection_sort(arr):
    a = arr.copy()
    for i in range(len(a) - 1):
        m = i
        for j in range(i + 1, len(a)):
            if a[j] < a[m]:
                m = j
        a[i], a[m] = a[m], a[i]
    return a

def insertion_sort(arr):
    b = arr.copy()
    for i in range(1, len(b)):
        key, j = b[i], i - 1
        while j >= 0 and b[j] > key:
            b[j + 1] = b[j]
            j -= 1
        b[j + 1] = key
    return b

data = [5, 3, 8, 1, 9, 2]
print(selection_sort(data))    # [1, 2, 3, 5, 8, 9]
print(insertion_sort(data))    # [1, 2, 3, 5, 8, 9]
print(data)                    # [5, 3, 8, 1, 9, 2] —— 原数据没动(copy 的功劳)

看到什么算成功:两行 [1, 2, 3, 5, 8, 9] + 原始乱序一行。

读结果:结果相同、路径不同——选择排序的 1 是【被挑出来换到】第一位,插入排序的 1 是【抓起后插到】第一位。算法=目标相同、路径不同的过程,这正是比较排序思想的入口。

五、30 秒实验

拿一副真实扑克(或写下的十个数)手动理牌:你下意识用的就是插入排序(左边手牌保持有序、抓一张插一张)——30 秒亲手做一遍,比背十遍代码记得牢。

六、常见误区

  1. 【O(n²) 的排序没用,直接学快排】——错。插入排序在【小规模】与【基本有序】时比快排还快(无递归开销、常数小);Timsort 内部就用它。没有慢算法,只有用错场景的算法。
  2. 【选择排序和冒泡是一回事】——错。冒泡靠【相邻交换】把大的顶到后面(交换频繁);选择是【记录下标、每轮一次交换】——下一节专门对比。
  3. 【排序函数会改原数组,无所谓】——有所谓。Python 的 list.sort() 原地改、sorted() 返回新表—— Demo 里先 copy 再排就是为了不动原数据;实际项目里改不改原数据是接口设计的一部分。
自测三问(点开对答案) 1. **选择排序每轮做什么?总共比较多少次?** 答:从剩余部分挑最小、与当前位置交换;比较次数固定 n(n-1)/2 次。 2. **插入排序为什么对基本有序的数据特别快?** 答:每张新牌只需比一两次就到位(几乎不用挪),整体接近 O(n)。 3. **两者的稳定性差异?** 答:插入排序稳定(相等元素不越过彼此);选择排序不稳定(远距离交换可能打乱相等元素的次序)。

相关笔记