排序

从乱序到有序。 排序是把一组元素按某种顺序(升序、降序或自定义比较)重新排列的过程,是算法里最基础也最常用的一类。基于「两两比较」的比较排序,理论上至少需要 O(n log n) 次比较;归并排序快速排序堆排序都能达到 O(n log n),是实际中最常用的三种。本章还会讲到稳定性:相等元素排序后相对顺序是否保持不变,对多关键字排序很重要。我们将用图示、Python 代码(类型标注与注释)和至少三道例题把比较排序与稳定性讲清楚。

一、排序概述与稳定性

比较排序:只通过「比较两个元素大小」得到顺序,不利用数值范围。可以证明:比较排序在最坏情况下至少需要 Ω(n log n) 次比较。稳定性:若排序前有相等元素 a 与 b 且 a 在 b 前,排序后 a 仍在 b 前,则称该排序算法稳定。稳定排序在多关键字排序时有用(先按次关键字排,再按主关键字排,主关键字相同时会保持次关键字的顺序)。

稳定排序保证相等元素相对顺序不变

二、简单排序(O(n²))简述

冒泡:相邻两两比较,逆序则交换,每轮把最大「冒」到末尾,O(n²),稳定。选择:每轮选未排序部分的最小元放到已排序末尾,O(n²),不稳定。插入:将未排序部分的首元素插入到已排序部分的正确位置,O(n²),稳定;对近似有序的序列很快。三者都是原地排序,适合 n 很小时或教学。

三、归并排序

归并排序(merge sort):分治。把数组分成两半,分别递归排序,再把两个有序数组合并成一个。合并时用双指针,每次取两段中较小的放入结果,O(n)。总时间 T(n)=2T(n/2)+O(n),即 O(n log n);需要额外 O(n) 空间存临时数组。归并排序是稳定的(合并时相等时取前一段的即可)。

归并:分治 + 合并有序段
from typing import List


def merge_sort(arr: List[int]) -> None:
    """
    归并排序:原地排序 arr。辅助数组用于合并,合并时取等取左以保证稳定。
    """
    n = len(arr)
    tmp: List[int] = [0] * n

    def merge(l: int, mid: int, r: int) -> None:
        i, j, k = l, mid, l
        while i < mid and j < r:
            if arr[i] <= arr[j]:  # 相等取左,稳定
                tmp[k] = arr[i]
                i += 1
            else:
                tmp[k] = arr[j]
                j += 1
            k += 1
        while i < mid:
            tmp[k] = arr[i]
            i += 1
            k += 1
        while j < r:
            tmp[k] = arr[j]
            j += 1
            k += 1
        arr[l:r] = tmp[l:r]

    def sort(l: int, r: int) -> None:
        if r - l <= 1:
            return
        mid = (l + r) // 2
        sort(l, mid)
        sort(mid, r)
        merge(l, mid, r)

    sort(0, n)

四、快速排序

快速排序(quick sort):选一个基准(如首元、尾元或随机),将数组划分成「小于基准」「等于基准」「大于基准」三段(或两段),递归排序左右。划分可双指针:左指针找大于基准的、右指针找小于基准的,交换后继续,直到相遇。平均时间 O(n log n),最坏 O(n²)(如已有序且总选到最值作基准);可随机选基准或三数取中降低最坏概率。快排一般不稳定;可写成稳定版但需额外空间。原地实现空间 O(log n) 递归栈。

快排划分:基准左侧 ≤ 基准,右侧 ≥ 基准
from typing import List
import random


def quick_sort(arr: List[int]) -> None:
    """
    快速排序:选最后一个为基准,双指针划分 [l, i) ≤ pivot,[i, j) 未处理,[j, r] ≥ pivot。
    然后交换 pivot 到位置 i,递归 [l, i) 和 [i+1, r]。
    """

    def partition(l: int, r: int) -> int:
        # 随机选一个与 arr[r-1] 交换,避免有序时退化
        rand = random.randint(l, r - 1)
        arr[rand], arr[r - 1] = arr[r - 1], arr[rand]
        pivot = arr[r - 1]
        i = l
        for j in range(l, r - 1):
            if arr[j] <= pivot:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
        arr[i], arr[r - 1] = arr[r - 1], arr[i]
        return i

    def sort(l: int, r: int) -> None:
        if r - l <= 1:
            return
        q = partition(l, r)
        sort(l, q)
        sort(q + 1, r)

    sort(0, len(arr))

五、堆排序

堆排序(heap sort):先建大根堆(上一章已讲),然后每次把堆顶(当前最大)与末尾交换,堆大小减一,对根做下沉,重复直到堆为空。这样从后往前得到升序。时间 O(n log n),原地,不稳定。Python 中可用 heapq 建小根堆再依次弹出得到升序,但那样需要额外空间。

from typing import List


def heap_sort(arr: List[int]) -> None:
    """
    堆排序:先建大根堆,再不断将堆顶与末尾交换并下沉,得到升序。
    """
    n = len(arr)

    def sift_down(start: int, end: int) -> None:
        """大根堆下沉:从 start 到 end 的子树中,start 不满足则与较大子交换。"""
        i = start
        while 2 * i + 1 < end:
            left = 2 * i + 1
            right = left + 1
            largest = i
            if arr[left] > arr[largest]:
                largest = left
            if right < end and arr[right] > arr[largest]:
                largest = right
            if largest == i:
                break
            arr[i], arr[largest] = arr[largest], arr[i]
            i = largest

    # 建堆:从最后一个非叶开始下沉
    for i in range((n - 2) // 2, -1, -1):
        sift_down(i, n)
    # 每次堆顶与末尾交换,堆范围减 1,再下沉
    for end in range(n - 1, 0, -1):
        arr[0], arr[end] = arr[end], arr[0]
        sift_down(0, end)

六、算法对比

算法平均时间最坏时间空间稳定
冒泡 / 选择 / 插入O(n²)O(n²)O(1)冒泡/插入稳定,选择不稳定
归并O(n log n)O(n log n)O(n)稳定
快排O(n log n)O(n²)O(log n)不稳定
堆排O(n log n)O(n log n)O(1)不稳定
常见比较排序的时间、空间与稳定性
小贴士: 实际中快排常数小、缓存友好,常用;需要稳定或链表排序时用归并;堆排不需额外空间且最坏 O(n log n),适合对最坏时间有要求的场景。

七、小结

比较排序下界 Ω(n log n)。归并分治合并、稳定、O(n) 空间;快排划分、平均 O(n log n)、不稳定;堆排原地、最坏 O(n log n)、不稳定。下一章讲查找与二分

八、例题

以下例题使用 Python 3 类型标注与注释。

例题 1:排序链表

题目描述:给定单链表头节点 head,要求 O(n log n) 时间、O(1) 额外空间(递归栈不计)内排序并返回新头。实际常用归并:找中点、断链、递归排序两段、合并两有序链表。

from typing import Optional


class ListNode:
    def __init__(self, val: int = 0, next: Optional["ListNode"] = None) -> None:
        self.val = val
        self.next = next


def sort_list(head: Optional[ListNode]) -> Optional[ListNode]:
    """归并排序链表:找中点(快慢指针)、断链、递归、合并。"""

    def mid_and_split(h: Optional[ListNode]) -> tuple:
        if not h or not h.next:
            return h, None
        slow, fast = h, h.next
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        mid = slow.next
        slow.next = None
        return h, mid

    def merge(l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0)
        p = dummy
        while l1 and l2:
            if l1.val <= l2.val:
                p.next = l1
                l1 = l1.next
            else:
                p.next = l2
                l2 = l2.next
            p = p.next
        p.next = l1 or l2
        return dummy.next

    if not head or not head.next:
        return head
    left, right = mid_and_split(head)
    left = sort_list(left)
    right = sort_list(right)
    return merge(left, right)

讲解:快慢指针找中点并断成两段,递归排序后合并两有序链表。时间 O(n log n),空间 O(log n) 递归栈。

例题 2:颜色分类(荷兰国旗 / 三路划分)

题目描述:数组 nums 只含 0、1、2,原地按 0、1、2 顺序排列。单次扫描、O(1) 额外空间。

思路:三路划分:维护 [0,i) 为 0,(i,j) 为 1,[k,n) 为 2;j 为当前扫描指针,根据 nums[j] 与 1 比较交换到对应区间。

from typing import List


def sort_colors(nums: List[int]) -> None:
    """
    三路划分:i 左侧全 0,k 右侧全 2,j 为当前指针;遇到 0 与 i 交换并 i,j 右移,遇到 2 与 k 交换并 k 左移,遇到 1 仅 j 右移。
    """
    i, j, k = 0, 0, len(nums) - 1
    while j <= k:
        if nums[j] == 0:
            nums[i], nums[j] = nums[j], nums[i]
            i += 1
            j += 1
        elif nums[j] == 2:
            nums[j], nums[k] = nums[k], nums[j]
            k -= 1
        else:
            j += 1

讲解:一次遍历,0 换到前面、2 换到后面、1 留在中间。时间 O(n),空间 O(1)。

例题 3:合并区间

题目描述:给定若干区间 intervals[i]=[start, end],合并所有重叠区间,返回不重叠的区间列表。

思路:按 start 排序后,顺序扫描:若当前区间与结果中最后一个区间重叠则合并(更新 last 的 end),否则加入结果。

from typing import List


def merge_intervals(intervals: List[List[int]]) -> List[List[int]]:
    if not intervals:
        return []
    intervals.sort(key=lambda x: x[0])
    result: List[List[int]] = [intervals[0][:]]
    for i in range(1, len(intervals)):
        start, end = intervals[i][0], intervals[i][1]
        last = result[-1]
        if start <= last[1]:
            last[1] = max(last[1], end)
        else:
            result.append([start, end])
    return result

讲解:排序后区间按左端点有序,重叠等价于当前 start ≤ 上一区间的 end,合并时取 max(end)。时间 O(n log n),空间 O(n)。

例题 4:最大数(自定义排序)

题目描述:非负整数数组 nums,将数字拼接成最大的整数(字符串形式)。例如 [10,2] → "210"。

思路:自定义比较:若 str(a)+str(b) > str(b)+str(a) 则 a 应排在 b 前。用该比较做排序后顺序拼接即可。

from typing import List
from functools import cmp_to_key


def largest_number(nums: List[int]) -> str:
    """比较规则:a 在前当且仅当 str(a)+str(b) >= str(b)+str(a)。"""
    strs = [str(x) for x in nums]

    def cmp(a: str, b: str) -> int:
        if a + b > b + a:
            return -1
        if a + b < b + a:
            return 1
        return 0

    strs.sort(key=cmp_to_key(cmp))
    result = "".join(strs)
    return "0" if result[0] == "0" else result

讲解:拼接后要最大,等价于相邻两数 a、b 应满足 a+b ≥ b+a。用 cmp_to_key 转成 key 函数排序。时间 O(n log n · L),L 为数字位数。