CC++ & Algorithm

排序算法比较与应用

困难0
语言版本:C++
概述:用整理书本和排队的例子,教你比较冒泡、选择和插入三种排序的优缺点,并学会在Python中选择合适的排序方法。

排序算法比较与应用:帮你选择最合适的排序方法

什么是排序?为什么要学它?

排序就是把一堆乱糟糟的数字(或数据),按照从小到大(或从大到小)的顺序重新排列。就像你每天早上整理书包——把所有书本按大小码好,这样上课时就能快速找到。在编程中,排序是最基础的操作之一,无论是处理成绩表、排队顺序,还是查找某个数据,排序常常是第一步。

今天我们要比较三种简单又常用的排序算法:冒泡排序选择排序插入排序。学完它们,你不仅能写出排序程序,还能根据情况选择最快的方法。


三种排序算法,从生活例子讲起

1. 冒泡排序 —— 像小鱼吐泡泡

生活例子:想象班里的同学排成一排,老师要求按身高从矮到高重新站。你从左到右依次比较相邻两个人,如果前面的人比后面高,就让他们交换位置。这样,最高的同学就会像泡泡一样,慢慢地“浮”到队伍的右边。重复这个操作,直到所有人都排好。

工作原理

  • 从左到右遍历列表,依次比较相邻两个元素。
  • 如果前一个 > 后一个,就交换它们。
  • 每遍历一次,最大的数就“冒泡”到末尾。
  • 重复 n-1 次(n 为元素个数),列表就排好序。

代码与注释

def bubble_sort(arr):
    n = len(arr)                     # 列表长度
    for i in range(n):               # 外层循环:需要遍历 n 次
        for j in range(0, n-i-1):    # 内层循环:每次比较的范围逐渐缩小
            if arr[j] > arr[j+1]:    # 如果前一个比后一个大
                arr[j], arr[j+1] = arr[j+1], arr[j]  # 交换位置

新手容易犯的错误

  • 内层循环忘记写 n-i-1,写成 n-1,导致每次都比较所有位置,浪费时间。
  • 忘记交换变量,写成 arr[j] = arr[j+1] 导致数据丢失。

2. 选择排序 —— 像选美比赛

生活例子:老师让同学们按身高从矮到高排好队。第一次,老师从所有人中选出最矮的同学,让他站到第一位。然后从剩下的同学中再选出最矮的,让他站到第二位……这样依次选出,直到所有人都站好位置。

工作原理

  • 在未排序部分中找到最小的元素。
  • 把它放到已排序部分的末尾(即与当前位置交换)。
  • 重复 n-1 次,完成排序。

代码与注释

def selection_sort(arr):
    n = len(arr)                     # 列表长度
    for i in range(n):               # 外层循环:每次确定一个位置
        min_idx = i                  # 假设当前 i 位置的值最小
        for j in range(i+1, n):      # 在 i 后面找更小的
            if arr[j] < arr[min_idx]:
                min_idx = j          # 更新最小值的索引
        arr[i], arr[min_idx] = arr[min_idx], arr[i]  # 将最小值放到 i 位置

新手容易犯的错误

  • 忘记更新 min_idx,导致交换时还是初始的 i 位置,排序错误。
  • 把交换写在了内层循环里,反复交换,变成冒泡了。

关于稳定性:选择排序是 不稳定 的。例如 [5a, 5b, 3](a和b是相同数字,但来自不同位置),第一次找到最小值3,与第一个5a交换,变成 [3, 5b, 5a],两个5的顺序颠倒了。如果你的程序要求相同值的顺序不能变(比如按成绩排序,同分按学号),选择排序就可能会出问题。


3. 插入排序 —— 像玩扑克牌时理牌

生活例子:你打扑克摸牌时,每摸到一张新牌,就把它插入到手牌中已经排好顺序的牌里。如果新牌比某张牌小,就插到它前面,否则往后放。这样手里的牌始终是有序的。

工作原理

  • 把列表分为“已排序部分”(左边)和“未排序部分”(右边)。
  • 每次从未排序部分取第一个元素,从右向左扫描已排序部分,找到合适位置插入。
  • 重复直到未排序部分为空。

代码与注释

def insertion_sort(arr):
    for i in range(1, len(arr)):     # 从第二个元素开始,因为第一个默认已排好
        key = arr[i]                 # 当前要插入的元素(新牌)
        j = i - 1                    # 从已排序部分的最后一个开始往前比较
        while j >= 0 and arr[j] > key:   # 如果前面的数比 key 大
            arr[j+1] = arr[j]        # 就把前面的数往后挪一位
            j -= 1
        arr[j+1] = key               # 把 key 插入到正确位置

新手容易犯的错误

  • 忘记写 j >= 0 条件,导致索引变成负数,程序崩溃。
  • arr[j+1] = arr[j] 写成 arr[j] = arr[j+1],顺序全乱。
  • 认为插入排序只能从小到大,其实只要修改比较条件,也能从大到小。

特殊优点:当数据 基本有序 时,插入排序非常快,几乎只需要扫描一遍,时间复杂度接近 O(n)。


怎么比较这三种排序?

算法最快情况时间复杂度最慢情况时间复杂度平均情况时间复杂度是否稳定数据交换次数
冒泡排序O(n) (已有序)O(n²)O(n²)稳定很多
选择排序O(n²)O(n²)O(n²)不稳定较少 (n-1次)
插入排序O(n) (已有序)O(n²) (逆序)O(n²)稳定少 (主要移动)

简单理解

  • 冒泡:每次都必须交换好多对,像气泡慢慢上浮,最慢,但容易写。
  • 选择:每次只交换一次,但无论如何都要从头到尾扫描所有剩余元素,所以也是 O(n²),但实际运行比冒泡快一点(因为交换次数少)。
  • 插入:如果数据已经差不多排好,它几乎不用怎么移动,非常快;如果数据完全逆序,它和最慢的冒泡一样慢。

适用场景(用你的零花钱来举例):

  • 数据很少(比如不到 10 个数):三种差别不大,就像买 3 根笔,用哪只手拿都行。
  • 数据部分有序(比如考试排名只差几分乱了顺序):插入排序是王者,像整理一个基本整齐的书架。
  • 数据很多(比如上千个):最好别用这三种,改用更快的算法(如快速排序、归并排序)。Python 自带的 list.sort() 就是用的 Timsort(一种混合排序)。

Python 完整测试与运行结果

下面一个程序会生成两种列表:

  1. 随机列表:包含 1000 个随机数。
  2. 部分有序列表:前 90% 已经是排序的,后 10% 是乱序的(模拟考试成绩单基本排好但几个调皮鬼插队)。

你会看到插入排序在部分有序时快得惊人。

import random
import time

def bubble_sort(arr):
    """冒泡排序"""
    n = len(arr)                     # 列表长度
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]

def selection_sort(arr):
    """选择排序"""
    n = len(arr)                     # 列表长度
    for i in range(n):
        min_idx = i                  # 假设当前 i 位置最小
        for j in range(i+1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]

def insertion_sort(arr):
    """插入排序"""
    for i in range(1, len(arr)):
        key = arr[i]                 # 当前要插入的元素
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j+1] = arr[j]        # 向后移动
            j -= 1
        arr[j+1] = key               # 插入

# ---------- 测试1: 随机列表 ----------
print("===== 测试1: 1000个随机数 =====")
data = [random.randint(0, 10000) for _ in range(1000)]

data1 = data.copy()
data2 = data.copy()
data3 = data.copy()

start = time.time()
bubble_sort(data1)
t_bubble = time.time() - start

start = time.time()
selection_sort(data2)
t_selection = time.time() - start

start = time.time()
insertion_sort(data3)
t_insertion = time.time() - start

print(f"冒泡排序用时:{t_bubble:.4f}秒")
print(f"选择排序用时:{t_selection:.4f}秒")
print(f"插入排序用时:{t_insertion:.4f}秒")

# ---------- 测试2: 部分有序的列表 ----------
print("\n===== 测试2: 1000个部分有序的数(前900个已排序) =====")
data_part = list(range(1, 901)) + [random.randint(1, 1000) for _ in range(100)]

data1 = data_part.copy()
data2 = data_part.copy()
data3 = data_part.copy()

start = time.time()
bubble_sort(data1)
t_bubble = time.time() - start

start = time.time()
selection_sort(data2)
t_selection = time.time() - start

start = time.time()
insertion_sort(data3)
t_insertion = time.time() - start

print(f"冒泡排序用时:{t_bubble:.4f}秒")
print(f"选择排序用时:{t_selection:.4f}秒")
print(f"插入排序用时:{t_insertion:.4f}秒")

可能的运行结果(每次不同,但规律相似)

===== 测试1: 1000个随机数 =====
冒泡排序用时:0.0890秒
选择排序用时:0.0450秒
插入排序用时:0.0460秒

===== 测试2: 1000个部分有序的数(前900个已排序) =====
冒泡排序用时:0.0280秒
选择排序用时:0.0440秒
插入排序用时:0.0030秒

你能看到

  • 在随机数时,插入排序和选择排序接近,冒泡最慢。
  • 在部分有序时,插入排序快了一个数量级(0.003秒 vs 0.028秒),因为它只需要处理后面乱序的那一点点。

常见错误总结(新手一定注意!)

  1. 冒泡排序:内层循环的终点写错了,导致越界或效率低。

    • for j in range(0, n-1): 会多比较已经排好的部分。
    • for j in range(0, n-i-1): 每次减少比较次数。
  2. 选择排序:忘记更新最小值索引,或者交换时用错了变量。

    • if arr[j] < arr[min_idx]: 后面没有更新 min_idx
    • ✅ 必须写 min_idx = j
  3. 插入排序:while 循环条件忘记 j >= 0,导致 arr[-1] 访问(Python 会取最后一个元素,逻辑错误)。

    • while arr[j] > key: 当 j 为 -1 时,arr[-1] 是列表最后一个数,条件可能意外成立。
    • while j >= 0 and arr[j] > key:
  4. 混淆排序方向:如果想从大到小排,只需要把 > 改成 <,但注意三个函数的修改要统一。

  5. 直接修改原数据:有时候你不想改变原列表,记得用 .copy() 复制一份再排序。


小结与进阶指引

  • 数据少于 100:随便选一个,冒泡最简单,插入最实用。
  • 数据接近有序:插入排序是王者,速度快,写法也简单。
  • 数据很多(成千上万):别用这三种,改用 快速排序归并排序 或直接调用 Python 内置的 list.sort()sorted()。内置排序是用 C 语言实现的 Timsort,又快又稳定。

想继续学习?

  • 快速排序(Quick Sort)—— 分治思想,适合大部分情况。
  • 归并排序(Merge Sort)—— 稳定,适合链表或大数据量。
  • 桶排序、计数排序—— 针对特定范围的数据,可以快到 O(n)。
  • 在竞赛中,如果数据规模在 10^3 以内,手写插入排序有时比内置排序还快(因为常数小);超过 10^4 就放心用内置排序吧。

最后一句:排序算法不仅是考试重点,更是你编程思维的训练——学会从“如何做”到“怎么做更好”的思考。动手写写代码,跑跑测试,很快就能掌握它们。

例题精讲

1单选题

在冒泡排序、选择排序和插入排序中,以下关于时间复杂度的描述正确的是?

A三种排序的平均时间复杂度都是O(n^2)
B冒泡排序的最好时间复杂度是O(n),选择排序和插入排序的最好时间复杂度也是O(n)
C选择排序在任何情况下的时间复杂度都是O(n^2)
D插入排序的平均时间复杂度是O(n log n)
2单选题

假设要对列表 [5, 3, 8, 6, 2] 进行升序排序,如果使用冒泡排序并加入优化(当某一轮没有发生交换时提前结束),那么在第几轮排序后可以提前结束?

A第1轮后
B第2轮后
C第3轮后
D第4轮后
3判断题

插入排序在数组基本有序的情况下,其时间复杂度可以接近O(n)。

4判断题

选择排序是一种稳定的排序算法,因为它在交换元素时不会改变相等元素的相对顺序。

5填空题
请完成下面的插入排序函数,将列表 arr 按升序排序。

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            ___  # 将arr[j]后移一位
            j -= 1
        arr[j + 1] = key

请填写正确的代码。