AI时代,重温10大经典排序算法

AI时代,重温10大经典排序算法

AI可以轻松生成任何排序算法代码,那么我们还有必要学习算法吗?

AI时代,不需要人工手写排序算法了,但我们需要理解算法背后的思想------分治、贪心、空间换时间以及分桶映射等。掌握这些思想,有利于我们与AI协作时给出正确的决策和指导。

一、为什么还要学排序算法?

排序无处不在

信息流、搜索结果、商品列表、好友排名,背后都有排序算法在工作。数据库的 ORDER BY、搜索引擎的结果排序、推荐系统的优先级队列------排序是计算世界中最基础、最常见的计算。

实现排序算法并非难事,难的是遇到实际场景时能做出合理的判断和决策:

  • 100万条订单数据,应该用快速排序还是归并排序?为什么?
  • 用户ID是纯数字且范围有限,能不能用计数排序把O(n log n)优化到O(n)?
  • 排序结果传递给下游做二次排序好还是提前排好再给下游?性能和稳定性哪个更重要?

排序算法是算法思想的缩影

十大排序算法并不只是十个简单的程序,它们背后浓缩了计算机大师们的心血,是几种核心算法思想的具体体现:
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 30, 'padding': 20}}}%% graph TD ROOT(["排序算法的核心思想"]):::root ROOT --> A["分治思想"] ROOT --> B["贪心思想"] ROOT --> C["插入思想"] ROOT --> D["交换思想"] ROOT --> E["映射思想"] ROOT --> F["树形结构"] A --> A1["快速排序\n归并排序"] B --> B1["选择排序"] C --> C1["插入排序\n希尔排序"] D --> D1["冒泡排序"] E --> E1["计数排序\n基数排序\n桶排序"] F --> F1["堆排序"] %% root更突出 classDef root fill:#111827,color:#ffffff,stroke:#000000,stroke-width:2px,rx:12,ry:12 %% 分类层 style A fill:#11908A,stroke:#0F6E56,color:#ffffff,rx:10,ry:10 style B fill:#534AB7,stroke:#3C3489,color:#ffffff,rx:10,ry:10 style C fill:#D85A30,stroke:#993C1D,color:#ffffff,rx:10,ry:10 style D fill:#BA7517,stroke:#854F0B,color:#ffffff,rx:10,ry:10 style E fill:#185FA5,stroke:#0C447C,color:#ffffff,rx:10,ry:10 style F fill:#993556,stroke:#72243E,color:#ffffff,rx:10,ry:10 %% 叶子层 style A1 fill:#11908A,stroke:#0F6E56,color:#ffffff,rx:10,ry:10 style B1 fill:#534AB7,stroke:#3C3489,color:#ffffff,rx:10,ry:10 style C1 fill:#D85A30,stroke:#993C1D,color:#ffffff,rx:10,ry:10 style D1 fill:#BA7517,stroke:#854F0B,color:#ffffff,rx:10,ry:10 style E1 fill:#185FA5,stroke:#0C447C,color:#ffffff,rx:10,ry:10 style F1 fill:#993556,stroke:#72243E,color:#ffffff,rx:10,ry:10

学会这些思想,我们面对的就不仅仅是排序问题------而是所有需要"分解、组合、选择、映射"的工程问题。


二、排序算法全景图

排序算法分类体系

%%{init: {'flowchart': {'nodeSpacing': 15, 'rankSpacing': 25, 'padding': 20}}}%% graph TD ROOT(["十大排序算法"]) ROOT --> CMP("比较排序\n理论下界 O(n log n)") ROOT --> NCMP("非比较排序\n可突破 O(n log n)") CMP --> SWAP["交换排序"] CMP --> SEL["选择排序"] CMP --> INS["插入排序"] CMP --> MRG["归并排序"] SWAP --> BUB["冒泡排序"] SWAP --> QCK["快速排序"] SEL --> SSEL["选择排序"] SEL --> HEAP["堆排序"] INS --> DINS["插入排序"] INS --> SHELL["希尔排序"] NCMP --> CNT["计数排序"] NCMP --> RDX["基数排序"] NCMP --> BKT["桶排序"] classDef root fill:#111827,stroke:#2C2C2A,color:#fff classDef cat fill:#11908A,color:#fff,stroke:#0a2647 classDef sub fill:#533483,color:#fff,stroke:#2c1654 classDef leaf fill:#e94560,color:#fff,stroke:#c81e45 class ROOT root class CMP,NCMP cat class SWAP,SEL,INS,MRG sub class BUB,QCK,SSEL,HEAP,DINS,SHELL,CNT,RDX,BKT leaf

排序可以分为两大类别:

1. 比较排序:通过元素之间两两比较来排顺序。它的时间复杂度有个下限是 O(n log n),不管用多聪明的比较方法,都很难突破这个极限。

2. 非比较排序:是利用数据本身的特点(比如数字范围、位数这些)直接确定位置,它可以做到线性时间 O(n)。不过它对数据有一些要求,比如数值范围不能太大、能拆分,或者能映射到固定的区间。


三、十大排序算法详解

1. 冒泡排序(Bubble Sort)--- 最朴素的交换

算法原理:遍历数组,相邻元素两两比较,将大的项交换到右侧。每一轮遍历都会把当前最大的元素"冒泡"到数组末尾,就像汽水里的气泡一样往上浮。如果某一轮没有发生交换,说明数组已经有序,可以提前结束。

它和选择排序的思路正好相反:选择排序是"每次选出一个最小(或最大)放到末尾",而冒泡排序是"逐步交换,让最大(或最小)冒到末尾"。

生活类比:体育课排队,让相邻两人比身高,矮的站前面,高的站后面。一轮下来,最高的人一定到了队尾。

流程图

%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 25, 'padding': 15}}}%% graph LR S(["开始"]) --> INIT["未排序区间 <br> = [0, n-1]"] INIT --> OUTER{"区间长度 > 1 ?"} OUTER -->|"否"| END(["排序完成"]) OUTER -->|"是"| LOOP["从左到右<br>依次比较相邻元素"] LOOP --> CMP{"前一个 > 后一个 ?"} CMP -->|"否"| NEXT["继续比较"] CMP -->|"是"| SWAP["交换"] SWAP --> NEXT NEXT --> CHECK{"到达区间末尾 ?"} CHECK -->|"否"| LOOP CHECK -->|"是"| SHRINK["缩小区间<br>(最大值归位)"] SHRINK --> |"开始下一轮"| OUTER %% 样式 classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px class S,END start class OUTER,CMP,CHECK decision class INIT,LOOP,NEXT,SWAP,SHRINK process

伪代码

js 复制代码
function BubbleSort(arr):
    n = length(arr)
    for i = 0 to n - 2:                     // 外层循环:共 n-1 轮
        swapped = false                     // 优化点:设置已交换标志
        for j = 0 to n - 2 - i:             // 内层循环:每轮减少一个
            if arr[j] > arr[j + 1]:         // 相邻比较
                swap(arr[j], arr[j + 1])    // 逆序则交换
                swapped = true
        if not swapped:                     // 本轮无交换,已有序,跳过
            break
    return arr

应用场景

  • 算法入门教学:逻辑最简单、最直观,是所有算法教材的首选入门排序
  • 近乎有序的小规模数据:加入提前终止优化后,对基本有序的数据可达到 O (n) 效率
  • 嵌入式与极简环境:代码极短、占用空间极小,适合资源受限设备

复杂度

最好 平均 最坏 空间 稳定性
O(n) O(n²) O(n²) O(1) 稳定

冒泡排序非常好理解,性能也很稳定,虽然现实中使用不多,但是因为很简单、很形象,所以通常是学习排序算法的第一课。


2. 选择排序(Selection Sort)--- 最少的交换

算法原理:遍历数组,每一轮在未排序区域中选出最小(或最大)的元素,放到已排序区域的末尾。整体需要进行大量比较,但交换次数很少,每一轮最多只交换一次。

它和插入排序的思路正好相反:选择排序是"先选一个,再放过去";而插入排序是"从未排序中取一个,按顺序插入到已排序里"。

生活类比:就像从一堆没有次序的苹果里,每次都挑出最小(或最大)的一个,放到一边按大小排好。每次只挑一个,慢慢就把所有苹果按顺序排好了。

流程图

%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 25, 'padding': 15}}}%% graph LR S(["开始"]) --> INIT["未排序区间 = [0, n-1]"] INIT --> OUTER{"区间长度 > 1 ?"} OUTER -->|"否"| END(["排序完成"]) OUTER -->|"是"| FIND["在未排序区间<br>挑选最小值"] FIND --> DOSWAP{"最小值是否<br>在当前位置 ?"} DOSWAP -->|"否"| SWAP["交换最小值<br>到当前位置"] DOSWAP -->|"是"| NEXT["无需交换"] SWAP --> NEXT NEXT --> SHRINK["缩小未排序区间<br>长度 - 1"] SHRINK --> |"开始下一轮"| OUTER %% 样式 classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px class S,END start class OUTER,DOSWAP decision class INIT,FIND,SWAP,NEXT,SHRINK process

伪代码

js 复制代码
function SelectionSort(arr):
    n = length(arr)
    for i = 0 to n - 2:                     // 遍历每个位置
        minIdx = i                          // 假设当前位置是最小值
        for j = i + 1 to n - 1:             // 在未排序区间找最小值
            if arr[j] < arr[minIdx]:
                minIdx = j                  // 更新最小值位置
        if minIdx != i:
            swap(arr[i], arr[minIdx])       // 只交换一次
    return arr

应用场景

  • 写入敏感的存储设备:如 Flash、ROM 等,选择排序交换次数最少,能显著降低写入损耗
  • 小规模数据排序:实现简单、逻辑清晰,数据量小时性能差异不明显
  • 朴素 Top‑K 问题:只需执行 K 轮即可选出前 K 大 / 前 K 小元素,无需完整排序

复杂度

最好 平均 最坏 空间 稳定性
O(n²) O(n²) O(n²) O(1) 不稳定

选择排序不太稳定,因为交换时可能把原本顺序相同的元素颠倒掉。比如 [5, 5, 4],第一轮把4和第一个5交换,第一个5就跑到最后面了。选择排序也是非常好理解的方式,每次从剩下的一堆里把最大或最小的挑出来,放到已排序里面去。


3. 插入排序(Insertion Sort)--- 扑克牌式排序

算法原理:遍历数组,把数组分为已排序和未排序两部分,每次从未排序中取出一个元素,插入到已排序部分的合适位置,已排序元素右移腾出位置。

生活类比:就像打扑克牌一样,把牌分成两堆------已排好序的牌未排的牌。一开始,第一张牌算作已排序部分,其余都是未排序部分。然后每次从未排序中拿一张牌,插入到已排序中的正确位置,已排序的牌往右移一位,未排序的牌则减少一张。

流程图

%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 25, 'padding': 15}}}%% graph LR S(["开始"]) --> INIT["未排序区间 <br>= [1, n-1]"] INIT --> OUTER{"区间长度 > 0 ?"} OUTER -->|"否"| END(["排序完成"]) OUTER -->|"是"| KEY["选取当前元素<br>作为待插入 key"] KEY --> INNER{"前方元素<br>是否 > key ?"} INNER -->|"是"| SHIFT["向后移动元素<br>为 key 腾位置"] SHIFT --> INNER INNER -->|"否"| PLACE["将 key 放入合适位置"] PLACE --> SHRINK["未排序区间长度 - 1"] SHRINK --> |"开始下一轮"| OUTER %% 样式 classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px class S,END start class OUTER,INNER decision class INIT,KEY,SHIFT,PLACE,SHRINK process

伪代码

js 复制代码
function InsertionSort(arr):
    n = length(arr)
    for i = 1 to n - 1:                    // 从第2个元素开始
        key = arr[i]                       // 取出待插入的牌
        j = i - 1
        while j >= 0 and arr[j] > key:     // 从右往左找位置
            arr[j + 1] = arr[j]            // 比key大的元素后移
            j = j - 1
        arr[j + 1] = key                   // 插入到正确位置
    return arr

应用场景

  • 小规模数据排序:n < 50时,插入排序的常数因子极小,实际速度往往最快
  • Timsort的子过程 :Python的sorted()和Java的Collections.sort()底层都用插入排序处理小分区
  • 在线流式排序:数据逐个到达时,可随时插入到正确位置,无需重排整体
  • 近乎有序的数据:最优情况可达到 O (n),是简单排序中效率最高的一种

复杂度

最好 平均 最坏 空间 稳定性
O(n) O(n²) O(n²) O(1) 稳定

插入排序稳定、直观,对小规模或几乎有序的数据特别高效,就像整理扑克牌一样,这是我们大家都会的方式,是经过实践检验的经典算法。


4. 希尔排序(Shell Sort)--- 跳跃式插入

算法原理:希尔排序是插入排序的改进版,先用较大的步长(gap)将数组分成若干组,对每组分别进行插入排序,然后逐步缩小gap,最终gap=1完成整体排序。通过先对间隔较大的子序列排序,可以显著减少元素移动次数,从而提高排序效率,尤其是对于较大或无序的数据序列。

生活类比:就像整理扑克牌,如果手里有很多牌,一次只按相隔一定间距(比如每隔10张牌)把牌插入到已排好的位置,先把大块牌大致排好序,再缩小间距,一次次精细调整,最后整个牌堆就排好了。相比插入排序"每次拿一张牌插入",希尔排序就像先粗略排,再精细排,效率更高。

流程图

%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 25, 'padding': 15}}}%% graph LR S(["开始"]) --> GAP["初始化<br>gap = n / 2"] GAP --> GCHK{"gap > 0 ?"} GCHK -->|"否"| END(["排序完成"]) GCHK -->|"是"| OUTER["遍历未排序区间<br>i = gap → n-1"] OUTER --> KEY["取当前元素<br>作为待插入 key"] KEY --> INNER{"按 gap 向前比较<br>前方元素是否 > key ?"} INNER -->|"是"| SHIFT["向后移动元素<br>为 key 腾位置"] SHIFT --> INNER INNER -->|"否"| PLACE["将 key 插入<br>正确位置"] PLACE --> NEXT["处理下一个 i"] NEXT --> OUTER OUTER --> GSHR["gap /= 2"] GSHR --> |"开始下一轮"| GCHK %% 样式 classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px class S,END start class GCHK,INNER decision class GAP,OUTER,KEY,SHIFT,PLACE,NEXT,GSHR process

伪代码

js 复制代码
function ShellSort(arr):
    n = length(arr)
    gap = n / 2                             // 初始步长
    while gap > 0:                          // 逐步缩小步长
        for i = gap to n - 1:               // 对每个分组做插入排序
            key = arr[i]
            j = i - gap
            while j >= 0 and arr[j] > key:  // 组内插入排序
                arr[j + gap] = arr[j]
                j = j - gap
            arr[j + gap] = key
        gap = gap / 2                       // 步长减半
    return arr

应用场景

  • 中等规模数据(100~10000 条):效率远超 O (n²) 算法,又不需要快排的递归栈空间
  • 嵌入式与小型系统:原地排序、代码简洁,性能稳定
  • Linux 内核等底层场景:部分版本使用希尔排序处理中等规模数据排序

复杂度

最好 平均 最坏 空间 稳定性
O(n log n) O(n^1.3) O(n²) O(1) 不稳定

希尔排序的时间复杂度取决于步长(gap)的选择。希尔建议的初始步长是 N/2,即每次把数组分成两半进行排序。这种取法在大多数情况下比直接插入排序效率高,但也并不是最优选择。希尔排序给我们的启迪是将大量数据拆分成小组来处理。


5. 快速排序(Quick Sort)--- 分治的经典

算法原理:选一个基准元素(pivot),把数组划分成两部分:一部分比它小,一部分比它大,然后分别对这两部分继续做同样的操作。通过不断拆分,直到每一部分都有序,整体自然就排好序了。快速排序时分治思想的体现:先把大问题拆成小问题,再逐个击破。

生活类比:就像给一群人排队,先选一个人当"基准",比他矮的站左边,比他高的站右边,然后左右两边也重复这个过程。随着不断分组,每一小组都会越来越有序,直到每组只剩下一个人时,整个队伍就排好了。

流程图

%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 25, 'padding': 15}}}%% graph LR S(["开始"]) --> CHK["当前区间长度 > 1 ?"] CHK -->|"否"| END(["返回"]) CHK -->|"是"| PIVOT["选择 pivot 元素"] PIVOT --> PART["分区操作:<br>小于 pivot 在左<br>大于 pivot 在右<br>返回 pivot 位置 p"] PART --> LEFT["递归排序左半区<br>quickSort(low, p-1)"] LEFT --> RIGHT["递归排序右半区<br>quickSort(p+1, high)"] RIGHT --> |"返回上一层"| CHK %% 节点样式 classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef pivot fill:#ffb703,color:#000,stroke:#e09f00,stroke-width:2px classDef partition fill:#A3F1B5,color:#003d2e,stroke:#04a777,stroke-width:2px classDef recurse fill:#11908A,color:#fff,stroke:#0b5f7a,stroke-width:2px %% 应用样式 class S,END start class CHK decision class PIVOT pivot class PART partition class LEFT,RIGHT recurse

伪代码

js 复制代码
function QuickSort(arr, low, high):
    if low < high:
        p = Partition(arr, low, high)       // 分区,返回pivot的最终位置
        QuickSort(arr, low, p - 1)          // 递归排序左半部分
        QuickSort(arr, p + 1, high)         // 递归排序右半部分

function Partition(arr, low, high):
    pivot = arr[high]                       // 选最后一个元素作为基准
    i = low - 1                             // i 指向"小于pivot区域"的末尾
    for j = low to high - 1:
        if arr[j] <= pivot:                 // 当前元素比pivot小
            i = i + 1
            swap(arr[i], arr[j])            // 放到左边区域
    swap(arr[i + 1], arr[high])             // pivot放到中间
    return i + 1                            // 返回pivot的位置

应用场景

  • 通用排序的首选 :C标准库的qsort()、Java的Arrays.sort()(基本类型)都基于快排变体
  • 数据库与搜索引擎:内存中的 ORDER BY、结果集排序大量使用快速排序
  • 大数据 Shuffle 阶段:MapReduce、Spark 等分布式框架广泛使用快排思想
  • 为什么实际最快:缓存友好(在连续内存上操作)、内层循环极其紧凑、原地排序节省内存
  • 通用内存排序首选:C、C++、Java、Go 等语言标准库的默认排序均基于快排变体

复杂度

最好 平均 最坏 空间 稳定性
O(n log n) O(n log n) O(n²) O(log n) 不稳定

快速排序高效而优雅,通过不断选取基准、拆分左右区间,把复杂问题层层拆解,是开发实践中最常用的排序算法之一。看似简单的"分一分",却蕴含着强大的力量,这也是分治思想的体现。


6. 归并排序(Merge Sort)--- 稳定的分治

算法原理:每次将数组一分为二,递归对左右两半继续拆分,直到每个子数组只有一个元素(自然有序)。然后从最底层开始,逐层向上合并,将两个有序子数组合并成一个更大的有序数组,最终得到完整排序的数组。

生活类比 :先把一筐苹果分成两篮 ,再每篮分成两篮......直到每篮只有一个苹果。然后从底层开始合并,每次合并按大小排序,直到所有苹果排好。

流程图

%%{init: {'flowchart': {'nodeSpacing': 25, 'rankSpacing': 25, 'padding': 15}}}%% graph LR S(["开始"]) --> CHK["当前数组长度 > 1 ?"] CHK -->|"否"| END(["返回"]) CHK -->|"是"| SPLIT["从中间二分为<br>左半部分 + 右半部分"] SPLIT --> LSORT["递归排序左半区<br>mergeSort(left)"] LSORT --> RSORT["递归排序右半区<br>mergeSort(right)"] RSORT --> MERGE["合并两个有序子数组<br>返回有序数组"] MERGE --> |"返回上一层"| CHK %% 节点样式 classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px %% 应用样式 class S,END start class CHK decision class SPLIT,LSORT,RSORT,MERGE process

伪代码

js 复制代码
function MergeSort(arr):
    if length(arr) <= 1:
        return arr                          // 单个元素自然有序
    mid = length(arr) / 2
    left = MergeSort(arr[0 : mid])          // 递归排序左半部分
    right = MergeSort(arr[mid : end])       // 递归排序右半部分
    return Merge(left, right)               // 合并两个有序数组

function Merge(left, right):
    result = []
    i = 0, j = 0
    while i < length(left) and j < length(right):
        if left[i] <= right[j]:             // 取出小的
            result.append(left[i])
            i++
        else:
            result.append(right[j])
            j++
    result.append(left[i:])                 // 追加剩余元素
    result.append(right[j:])
    return result

应用场景

  • 需要稳定排序的场景:数据库多字段排序、UI列表渲染保持相同元素的原始顺序
  • 外部排序:海量数据无法一次装入内存时,分块排序再合并,天然适合归并思想
  • 链表排序:链表上做归并排序不需要额外空间(不需要随机访问),比快速排序更合适
  • Python / Java标准库 :Python的sorted()和Java的Collections.sort()底层均使用 Timsort(归并 + 插入)

复杂度

最好 平均 最坏 空间 稳定性
O(n log n) O(n log n) O(n log n) O(n) 稳定

归并排序是唯一一个在最好、平均、最坏情况下都保持O(n log n)且稳定的比较排序算法,代价是需要额外空间。这就是算法设计中经典的时间-空间权衡


7. 堆排序(Heap Sort)--- 树形选择

算法原理:利用堆(完全二叉树)这种数据结构来排序。先把数组构建成一个大顶堆(每个父节点都大于等于子节点),然后不断取出堆顶(最大值)放到数组末尾,再重新调整堆。

堆的精妙之处在于:利用的是完全二叉树的结构,用数组直接表示,父子节点关系通过索引计算完成。对于位置i的元素,其左子节点在2i+1,右子节点在2i+2,父节点在(i-1)/2,无需额外指针。

  1. 先把数组构建成大顶堆(每个父节点 ≥ 子节点)。
  2. 每次取出堆顶(最大值)放到数组末尾,然后缩小堆的范围并重新调整堆,使剩余元素依然保持大顶堆性质。
  3. 重复上述步骤,直到所有元素排序完成。

生活类比:就像整理一堆水果,把最大的放在顶上,每次取出最顶上的水果放到盘子里,然后让剩下的水果重新"自动堆成一座山",下一次再取最大的。不断重复,最终就把所有水果从大到小排好。

流程图

%%{init: {'flowchart': {'nodeSpacing': 15, 'rankSpacing': 25, 'padding': 20}}}%% graph LR S(["开始"]) --> BUILD["构建大顶堆\n从最后一个非叶节点\n到根逐个下沉"] BUILD --> LOOP{"未排序部分\n长度 > 1 ?"} LOOP -->|"否"| END(["排序完成"]) LOOP -->|"是"| SWAP["交换堆顶与\n未排序部分末尾"] SWAP --> SHRINK["未排序范围 - 1"] SHRINK --> HEAPIFY["对堆顶执行下沉\nsift down"] HEAPIFY --> LOOP classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px class S,END start class LOOP decision class BUILD,SWAP,SHRINK,HEAPIFY process

伪代码

js 复制代码
function HeapSort(arr):
    n = length(arr)
    // 建堆:从最后一个非叶节点开始,自底向上堆化
    for i = n/2 - 1 downto 0:
        SiftDown(arr, i, n)

    // 排序:反复取堆顶放到末尾
    for i = n - 1 downto 1:
        swap(arr[0], arr[i])                // 堆顶(最大值)放到末尾
        SiftDown(arr, 0, i)                 // 对剩余元素重新堆化

function SiftDown(arr, parent, size):
    while 2 * parent + 1 < size:            // 还有子节点
        child = 2 * parent + 1              // 左子节点
        if child + 1 < size and arr[child + 1] > arr[child]:
            child = child + 1               // 选较大的子节点
        if arr[parent] >= arr[child]:
            break                           // 父节点已经最大,停止
        swap(arr[parent], arr[child])       // 下沉
        parent = child

应用场景

  • 优先队列调度:操作系统进程调度、网络包调度底层均基于堆结构
  • 大规模 Top‑K 问题:从10亿级数据中取前K个值,只需维护大小为K的小顶堆,复杂度 O (n log k)
  • 内存受限环境:原地排序 O (1) 空间,最坏复杂度稳定 O (n log n)
  • 高性能定时器:Nginx、Go Runtime、Redis 均使用堆管理定时器

复杂度

最好 平均 最坏 空间 稳定性
O(n log n) O(n log n) O(n log n) O(1) 不稳定

堆排序理论上非常理想------原地排序且时间复杂度稳定为 O(n log n),但在实际应用中通常比快速排序慢。原因在于堆化操作需要在数组中频繁跳跃访问父子节点,父节点和子节点在内存中相距较远,CPU 缓存命中率低,导致效率下降。


8. 计数排序(Counting Sort)--- 用空间换时间

算法原理:统计每个元素出现的次数,用额外数组记录到对应下标,再按顺序输出实现排序,不进行元素比较。适合元素范围不大、为整数的数组,时间复杂度 O(n + k)。

生活类比: 就像统计考试分数:准备一个 0--100 的计数表,遍历所有试卷,把每个分数出现的次数加 1。最后从 0 分到 100 分依次输出,每个分数出现几次就写几次,这样就得到排序好的成绩单。

流程图

%%{init: {'flowchart': {'nodeSpacing': 10, 'rankSpacing': 20, 'padding': 15}}}%% graph LR S(["开始"]) --> RANGE["找到最大值 max\n创建计数数组 count[0..max]"] RANGE --> COUNT["遍历数组\n统计每个元素出现次数"] COUNT --> PREFIX["对 count 做前缀和\n确定每个元素的位置"] PREFIX --> FILL["反向遍历原数组\n按 count 放入输出数组"] FILL --> END(["排序完成"]) classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px class S,END start class RANGE,COUNT,PREFIX,FILL process

伪代码

js 复制代码
function CountingSort(arr):
    max_val = max(arr)                      // 找到最大值
    count = array of (max_val + 1) zeros    // 创建计数数组

    for x in arr:                           // 统计每个值的出现次数
        count[x] = count[x] + 1

    for i = 1 to max_val:                   // 前缀和:count[i]变为"<=i的元素总数"
        count[i] = count[i] + count[i - 1]

    output = array of length(arr)
    for i = length(arr) - 1 downto 0:       // 反向遍历保证稳定性
        output[count[arr[i]] - 1] = arr[i]
        count[arr[i]] = count[arr[i]] - 1

    return output

应用场景

  • 年龄排序:范围 0--150 固定且集中,计数排序可实现极致线性效率
  • 考试成绩排序:分数 0--100 范围有限,无需比较即可完成排序与统计
  • 字符频率统计:ASCII 字符集 0--127,非常适合计数排序
  • 基数排序的子过程:基数排序每一位的稳定排序通常由计数排序实现

复杂度

最好 平均 最坏 空间 稳定性
O(n + k) O(n + k) O(n + k) O(n + k) 稳定

计数排序高效、直接,不依赖比较,尤其适合数值范围有限的数据,就像统计考试分数一样,是一种巧妙且实用的排序方法。


9. 基数排序(Radix Sort)--- 逐位排序

算法原理:不直接比较数字的大小,而是把数字按个十百千位来分别处理。从最低位(个位)开始,对每一位用稳定排序(通常是计数排序),逐位向高位处理。每轮都是稳定排序,保证低位顺序在高位处理时不被破坏,最终得到完整有序序列。

生活类比:就像整理邮政编码的信件,先按最后一位数字分堆,再按倒数第二位分堆......逐位处理,直到按完整邮编顺序排列好所有信件。

流程图

%%{init: {'flowchart': {'nodeSpacing': 15, 'rankSpacing': 25, 'padding': 20}}}%% graph LR S(["开始"]) --> MAXD["计算数组<br>最大位数 d"] MAXD --> DINIT["digit = 1\n从最低位开始"] DINIT --> DCHK{"digit ≤ d ? <br>(最低位 → 最高位)"} DCHK -->|"否"| END(["排序完成"]) DCHK -->|"是"| STABLE["对数组 a[0..n-1]\n按 digit 位进行稳定排序\n(计数排序)"] STABLE --> NEXT["digit++"] NEXT --> |"处理下一位"| DCHK %% 节点样式 classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px %% 应用样式 class S,END start class DCHK decision class MAXD,DINIT,STABLE,NEXT process

伪代码

js 复制代码
function RadixSort(arr):
    max_val = max(arr)
    d = number_of_digits(max_val)           // 最大位数

    exp = 1                                 // 当前位的权重:1, 10, 100, ...
    for digit = 1 to d:
        CountingSortByDigit(arr, exp)       // 按当前位做稳定排序
        exp = exp * 10

function CountingSortByDigit(arr, exp):
    count = array of 10 zeros               // 0-9 十个桶
    output = array of length(arr)

    for x in arr:
        digit = (x / exp) % 10             // 取出当前位
        count[digit]++

    for i = 1 to 9:
        count[i] += count[i - 1]            // 前缀和

    for i = length(arr) - 1 downto 0:       // 反向遍历保证稳定性
        digit = (arr[i] / exp) % 10
        output[count[digit] - 1] = arr[i]
        count[digit]--

    copy output to arr

应用场景

  • 手机号排序:11 位定长数字,效率 O (11n),远快于 O (n log n)
  • IP 地址排序:4 段固定格式数字,天然适合逐段排序
  • 身份证号排序:定长数字编码,规则统一,可实现线性时间排序
  • 大规模整数 / 定长字符串:位数固定时,O (dn) 接近线性,海量数据下优势巨大

复杂度

最好 平均 最坏 空间 稳定性
O(n × d) O(n × d) O(n × d) O(n + k) 稳定

基数排序不依赖比较,高效处理大规模整数或固定长度字符串,就像邮局分拣信件一样,是实践中验证过的排序智慧。


10. 桶排序(Bucket Sort)--- 分桶映射

算法原理:先把数据按数值区间划分成若干个桶,每个桶内部单独排序(通常是插入排序),然后按桶的顺序依次合并。关键是假设数据分布均匀,这样每个桶元素少,总体效率高。

生活类比:就像收集水果,把苹果按大小或颜色放到不同的篮子里,每个篮子里再整理一下,最后按篮子顺序排列所有苹果,就得到整齐的果堆。

流程图

%%{init: {'flowchart': {'nodeSpacing': 15, 'rankSpacing': 35, 'padding': 20}}}%% graph LR S(["开始"]) --> INIT["创建 k 个空桶\n确定值域范围"] INIT --> DIST["遍历数组\n将每个元素<br>分配到对应桶"] DIST --> SORT["对每个非空桶\n内部排序"] SORT --> CONCAT["按桶顺序\n依次拼接所有元素"] CONCAT --> NEXT{"还有未处理的桶 ?"} NEXT -->|"是,处理下一桶"| SORT NEXT -->|"否"| END(["排序完成"]) %% 样式 classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px classDef decision fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px classDef process fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px class S,END start class NEXT decision class INIT,DIST,SORT,CONCAT process

伪代码

js 复制代码
function BucketSort(arr):
    n = length(arr)
    min_val = min(arr)
    max_val = max(arr)
    bucket_count = n                        // 桶数量通常取n
    bucket_size = (max_val - min_val + 1) / bucket_count

    buckets = array of bucket_count empty lists

    for x in arr:                           // 将元素分配到桶中
        idx = (x - min_val) / bucket_size
        buckets[idx].append(x)

    for each bucket in buckets:             // 桶内排序(通常用插入排序)
        InsertionSort(bucket)

    result = concatenate all buckets        // 按桶顺序拼接
    return result

应用场景

  • 均匀分布浮点数排序:如 0~1 随机浮点数,分桶后接近线性时间
  • 分段统计排序:成绩分段、商品价格区间、用户年龄段归类等业务场景
  • 图像颜色直方图:按像素值分桶统计,是图像处理经典应用
  • 请求负载均衡:按特征值分桶分发流量,实现均匀调度与分流

复杂度

最好 平均 最坏 空间 稳定性
O(n + k) O(n + k) O(n²) O(n + k) 稳定

桶排序适合均匀分布的数据,高效且直观,就像按篮子整理水果一样,是实践中处理特定场景的稳定排序方法。


四、10大排序算法特点回顾

算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 说明
冒泡排序 O(n²) O(n²) O(1) 稳定 相邻元素两两比较,最大或最小元素逐轮"冒"到最后
选择排序 O(n²) O(n²) O(1) 不稳定 每轮选择未排序里最小(或最大)元素放到已排序末尾
插入排序 O(n²) O(n²) O(1) 稳定 类似打扑克,从未排序中取元素插入到已排序序列中
快速排序 O(n log n) O(n²) O(log n) 不稳定 分治+分区,选基准元素将数组拆分,递归排序左右区间
归并排序 O(n log n) O(n log n) O(n) 稳定 递归拆分数组到单个元素,再不断向上合并两个有序子数组
堆排序 O(n log n) O(n log n) O(1) 不稳定 利用大顶堆选择最大元素放到末尾,原地排序
希尔排序 O(n^1.3) O(n²) O(1) 不稳定 分组式插入排序,先大步长分组排序再逐步缩小步长
计数排序 O(n + k) O(n + k) O(n + k) 稳定 不比较元素大小,统计每个值出现的次数并直接输出
基数排序 O(n × d) O(n × d) O(n + k) 稳定 按位从低到高分别排序,最后从高到低合并数据
桶排序 O(n + k) O(n²) O(n + k) 稳定 将数据分成若干桶,桶内排序后再将全部桶合并

10大排序算法的多语言实现(C/Java/Go/Python/JS/TS/Rust)源码地址:https://github.com/microwind/algorithms

五、AI时代,如何指导AI选择排序算法?

前面回顾了10大排序算法的特点与适用场景,现在再回到文章开头的问题:

AI可以轻松生成算法代码了,那作为程序员,我们还有必要学习算法吗?

AI时代我们无需手写代码了,但我们需要理解算法背后的思想,这样才能根据实际场景,指导AI做出合适的选择

我们来看下AI引起的编程变革。

1. AI编程的发展阶段

%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 40, 'padding': 10}}}%% graph LR %% ===== 时间线主线 ===== P(["2023以前<br/>传统模式"]) --> Q(["2023-2024<br/>Copilot模式"]) --> R(["2025+<br/>Agent模式"]) --> S(["2026+<br/>Agentic模式"]) %% ===== 核心能力纵向显示 ===== P1("手写代码<br/>实现功能<br/>角色:执行者") Q1("L1 AI Copilot<br/>辅助编码<br/>角色:增强执行") R1("L2 AI Agent<br/>指导AI<br/>角色:指挥者") S1("L3 Agentic AI<br/>驱动AI<br/>角色:决策者") P --> P1 Q --> Q1 R --> R1 S --> S1 %% ===== 主节点 ===== style P fill:#615B5B,stroke:#282A2D,color:#ffffff,stroke-width:2px,rx:12,ry:12 style Q fill:#FF6253,stroke:#c94c4c,color:#ffffff,stroke-width:2px,rx:12,ry:12 style R fill:#6a5acd,stroke:#1D4ED8,color:#ffffff,stroke-width:2px,rx:12,ry:12 style S fill:#11908A,stroke:#15803D,color:#ffffff,stroke-width:2px,rx:12,ry:12 %% ===== 子节点 ===== style P1 fill:#E6E8EB,stroke:#90959D,color:#15181E,rx:10,ry:10 style Q1 fill:#FDE2DF,stroke:#F59E0B,color:#78350F,rx:10,ry:10 style R1 fill:#F2E7FA,stroke:#7C04AB,color:#1E3A8A,rx:10,ry:10 style S1 fill:#C2F8F1,stroke:#23B659,color:#14532D,rx:10,ry:10

2. 工作方式发生了本质变化

%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 45, 'padding': 20}}}%% graph LR PAST(["过去<br/>人工写排序代码"]):::past NOW(["现在<br/>人工定义策略 + 约束<br/>指导AI选择算法"]):::now FUTURE(["将来<br/>人工只描述目标<br/>AI自主决策与执行"]):::future PAST --> NOW --> FUTURE classDef past fill:#615B5B,stroke:#282A2D,color:#ffffff,stroke-width:2px,rx:12,ry:12 classDef now fill:#6a5acd,color:#fff,stroke:#483d8b,stroke-width:2px,rx:14,ry:14 classDef future fill:#11908A,color:#fff,stroke:#008080,stroke-width:2px,rx:14,ry:14

过去: 程序员写排序代码
现在: 人工定义算法策略(需求拆解)和约束条件(性能/稳定性/成本), 指导AI实现
未来: 人只告诉AI需求目标,AI自主确定策略,自主执行合适方案

3. 先分析需求和选择策略,再让AI实现代码

  • 先明确约束:根据数据规模、稳定性要求、内存限制、键值范围与分布特征,确定算法边界
  • 通用场景首选:普通数组优先使用快速排序;链表或需要稳定排序时选择归并排序
  • 小规模/近乎有序数据:插入排序效果最好;也可用"无交换提前结束"的冒泡作为简单有序性检测
  • 整数且范围有限:计数排序、基数排序、桶排序可将时间复杂度降至线性级别,性能优势极大
  • 最坏情况防护:快排应使用随机或三数取中选基准,小分区切换插入排序;大量重复元素时使用三路划分

%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 15, 'padding': 10}}}%% graph TB START(["需要排序"]):::start --> Q1{"数据规模?"} Q1 -->|"小规模 n < 50"| A1(["插入排序"]):::small Q1 -->|"小规模 n < 50"| A1b(["冒泡排序"]):::small Q1 -->|"小规模 n < 50"| A1c(["选择排序"]):::small Q1 -->|"中等/大规模"| Q2{"数据特征?"} Q2 -->|"整数且范围有限"| A2(["计数排序"]):::noncmp Q2 -->|"多位整数/定长字符串"| A3(["基数排序"]):::noncmp Q2 -->|"均匀分布"| A4(["桶排序"]):::noncmp Q2 -->|"通用数据"| Q3{"额外要求?"} Q3 -->|"需要稳定"| A5(["归并排序"]):::cmp Q3 -->|"内存受限"| A6(["堆排序"]):::cmp Q3 -->|"追求速度"| A7(["快速排序"]):::cmp Q3 -->|"近乎有序"| A8(["插入排序 / 希尔排序"]):::cmp %% 样式定义(统一圆角 + 分层颜色) classDef start fill:#FF6253,color:#fff,stroke:#c94c4c,stroke-width:2px,rx:12,ry:12 classDef question fill:#0f3460,color:#fff,stroke:#0f69a1,stroke-width:2px,rx:10,ry:10 classDef small fill:#DCA8FA,stroke:#483d8b,stroke:#7913CC,rx:10,ry:10 classDef cmp fill:#51D0EA,color:#052e16,stroke:#15807C,rx:10,ry:10 classDef noncmp fill:#f59e0b,color:#422006,stroke:#b45309,rx:10,ry:10 class START start class Q1,Q2,Q3 question

4. 实际场景多采用混合策略

编程语言中,会根据数据规模、分布特征和运行时情况,动态组合多种算法

  • C 标准库 (qsort):QuickSort 变种 + 小数组插入排序优化
  • C++ STL (std::sort):Introsort(快速排序 + 堆排序 + 插入排序)
  • Java (Arrays.sort):双轴 QuickSort(基本类型)、Timsort(对象数组,归并 + 插入排序)
  • Python (sorted / list.sort):Timsort(归并 + 插入排序)
  • Go (sort 包):PDQSort + 插入排序
  • JavaScript (Array.prototype.sort):Timsort / QuickSort + 插入排序
  • Rust (slice::sort / sort_unstable)sort → Timsort 变体;sort_unstable → PDQSort(快排优化版)

开源软件与系统中,也会根据数据特点与内存动态选择排序策略

  • MySQL :内存内 ORDER BY 使用快速排序,超 sort_buffer_size 后切换为外部归并排序
  • MongoDB :内存排序 QuickSort/Timsort,结果集 >32 MB 时自动降级为外部归并排序 (allowDiskUse)
  • Elasticsearch (Lucene) :多段合并归并排序;doc_values 字段用计数排序或快速排序变体
  • PostgreSQL :内存排序 QuickSort,超过 work_mem 后切换外部归并
  • Redis:小数据量 QuickSort,大数据量归并排序,支持外部临时文件
  • Spark / Flink:分区内用 Timsort,跨分区全局排序用采样+范围划分,流式 Top‑N 用堆
  • 推荐系统 Top‑N:召回用近似排序(ANN/桶/堆),精排用快速排序或归并排序,整体用堆维护动态 Top‑K
  • 实时计算(Flink SQL):流式 Top‑N 用堆排序,批处理超内存用外部归并排序

六、总结

10大排序算法,从朴素的冒泡、选择、插入,到基于分治思想的快速排序、归并排序,再到利用数据特征实现线性时间的计数、基数和桶排序,每一种都体现了前人解决问题的智慧。

在实际应用中,我们经常要做出判断:何时优先选择快排,何时切换到线性时间的计数/基数/桶排序,何时为了稳定性选择归并,这些都取决于数据特征与约束条件。

AI时代,实现排序算法本身已经不重要了,重要的是选择哪种算法、为什么这么选。理解这些算法思想,指导AI时我们才会从容不迫。


相关链接

相关推荐
m0_730115112 小时前
模板编程中的SFINAE技巧
开发语言·c++·算法
掘金一周2 小时前
你每个月花多少钱在AI上,有没有省钱妙招啊😭 | 沸点周刊 3.26
ai编程·沸点
NAGNIP2 小时前
面试官:给我讲一下卷积吧!
算法·面试
2401_831824962 小时前
高性能计算集群部署
开发语言·c++·算法
NAGNIP2 小时前
一文搞懂卷积神经网络!
算法·面试
菜鸟小九2 小时前
hot100(91-100)
数据结构·算法·排序算法
add45a2 小时前
C++代码移植性设计
开发语言·c++·算法
ccLianLian2 小时前
leetcode-hot100
算法·leetcode·职场和发展
qq_148115372 小时前
分布式系统容错设计
开发语言·c++·算法