🔄 排序 — 从比较到非比较的进化

排序不只是调用sort(),理解排序思想是解决TopK和区间问题的钥匙

📖 排序的核心概念

排序是算法的基础中的基础。面试中,你不需要手写快排,但需要理解各种排序的特点:哪些稳定、哪些原地、哪些 O(n log n)、哪些可以 O(n)。更重要的是,排序常作为其他算法的预处理步骤——排序 + 双指针、排序 + 二分、排序 + 贪心。

排序算法全家福: ┌─ 比较排序 ──────────────────────────────────┐ │ 算法 时间(平均) 时间(最坏) 空间 稳定 │ │ 冒泡排序 O(n²) O(n²) O(1) ✓ │ │ 选择排序 O(n²) O(n²) O(1) ✗ │ │ 插入排序 O(n²) O(n²) O(1) ✓ │ │ 归并排序 O(nlogn) O(nlogn) O(n) ✓ │ │ 快速排序 O(nlogn) O(n²) O(logn)✗ │ │ 堆排序 O(nlogn) O(nlogn) O(1) ✗ │ └─────────────────────────────────────────────┘ 比较排序下界: Ω(n log n) ┌─ 非比较排序 ────────────────────────────────┐ │ 计数排序 O(n+k) O(n+k) O(k) ✓ │ │ 基数排序 O(d(n+k)) O(d(n+k)) O(n+k)✓ │ │ 桶排序 O(n) O(n²) O(n) ✓ │ └─────────────────────────────────────────────┘ 突破比较排序下界! (有条件限制)

1. 排序在面试中的考点

考点一:排序作为预处理
很多问题排序后更好解决:两数之和(有序版双指针)、合并区间、TopK。
面试策略:先说排序 O(n log n) 的解法,再考虑能否优化。
考点二:自定义排序规则
Python 的 sort(key=..., reverse=...)
多关键字排序:先按 A 排,A 相同按 B 排。
面试考点:比较函数的设计。
考点三:部分排序 / 选择
不需要完全排序,只需找第 K 大/小。
快速选择 O(n) 或 堆 O(n log k)。

2. Python 排序速查

# list.sort() — 原地排序
nums = [3, 1, 4, 1, 5]
nums.sort()               # [1, 1, 3, 4, 5]
nums.sort(reverse=True)   # [5, 4, 3, 1, 1]

# sorted() — 返回新列表
new_list = sorted(nums)

# 自定义 key
words = ["banana", "pie", "Washington"]
words.sort(key=len)       # 按长度: ["pie", "banana", "Washington"]
words.sort(key=lambda x: x.lower())  # 忽略大小写

# 多关键字排序
students = [("Alice", 90), ("Bob", 85), ("Charlie", 90)]
students.sort(key=lambda x: (-x[1], x[0]))  # 分数降序,名字升序

🎯 题目一:数组中的第K个最大元素 (LC 215)

题目描述:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

示例: nums = [3,2,1,5,6,4], k = 2 排序后: [1,2,3,4,5,6] → 第2大 = 5 方法1: 排序 → nums[-k] O(n log n) 方法2: 小顶堆维护TopK O(n log k) 方法3: 快速选择 (QuickSelect) O(n) 平均 QuickSelect过程 (找第2大=第5小): pivot=3 → 分区: [2,1] [3] [5,6,4] 第5小 > 2+1=3 → 搜索右半 [5,6,4], 找第5-3=2小 pivot=5 → 分区: [4] [5] [6] 第2小 > 1 → 搜索右半 [6], 找第2-1=1小 [6] 只有1个 → 返回6? 不对,重新来...找第5小: pivot=4 → [3,2,1] [4] [5,6] 第5小 > 3+1=4 → 右半[5,6], 找第1小=5 ✓

思路分析

方法一:排序(最简单):
排序后取 nums[-k]。
时间复杂度:O(n log n)
方法二:小顶堆(推荐):
维护大小为 k 的小顶堆,堆顶即第 k 大。
遍历每个元素:堆未满直接入;堆满且元素>堆顶则替换。
时间复杂度:O(n log k),空间复杂度:O(k)
方法三:快速选择(最优平均):
类似快排的分区,但只递归一侧。
平均 O(n),最坏 O(n²)。随机化 pivot 可避免最坏。
面试加分项!

代码实现

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        # 方法二: 小顶堆
        import heapq
        heap = []
        for num in nums:
            heapq.heappush(heap, num)
            if len(heap) > k:
                heapq.heappop(heap)
        return heap[0]

复杂度分析

时间复杂度:O(n log k)

空间复杂度:O(k)

变体练习

🎯 题目二:前 K 个高频元素 (LC 347)

题目描述:给你一个整数数组 nums 和一个整数 k,请你返回其中出现频率前 k 高的元素。

示例: nums = [1,1,1,2,2,3], k = 2 频率统计: 1 → 3次 2 → 2次 3 → 1次 Top 2: [1, 2] 方法1: Counter + 排序 O(n log n) 方法2: Counter + 小顶堆 O(n log k) 方法3: Counter + 桶排序 O(n)

思路分析

方法一:Counter + 堆:
1. Counter 统计频率 O(n)
2. 小顶堆维护频率 TopK O(n log k)
时间复杂度:O(n log k)
方法二:Counter + 桶排序:
1. Counter 统计频率 O(n)
2. 按频率分桶:bucket[freq].append(num)
3. 从高频率到低频率收集 k 个元素
时间复杂度:O(n)

代码实现

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        from collections import Counter
        import heapq
        count = Counter(nums)
        return heapq.nlargest(k, count.keys(), key=count.get)

复杂度分析

时间复杂度:O(n log k) — nlargest 内部用堆

空间复杂度:O(n)

变体练习

🎯 题目三:排序数组 (LC 912)

题目描述:给你一个整数数组 nums,请你将该数组升序排列。要求时间复杂度 O(n log n)。

快速排序过程: [5, 3, 6, 2, 1, 4] pivot=5 → [3,2,1,4] [5] [6] 递归左: [3,2,1,4], pivot=3 → [2,1] [3] [4] 递归左: [2,1], pivot=2 → [1] [2] 递归右: [6] → 已排序 合并: [1,2,3,4,5,6] 归并排序过程: [5,3,6,2,1,4] → [5,3,6] [2,1,4] → [5] [3,6] [2] [1,4] → [5] [3][6] [2] [1][4] → [5] [3,6] [2] [1,4] → [3,5,6] [1,2,4] → [1,2,3,4,5,6]

思路分析

快速排序(面试重点):
1. 选 pivot(随机化避免最坏情况)
2. 分区:小于 pivot 的在左,大于的在右
3. 递归排序左右两半
平均 O(n log n),最坏 O(n²),原地排序
归并排序(稳定排序):
1. 分治:从中间切开
2. 递归排序两半
3. 合并两个有序数组
始终 O(n log n),但需要 O(n) 额外空间

代码实现(归并排序)

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        if len(nums) <= 1:
            return nums
        mid = len(nums) // 2
        left = self.sortArray(nums[:mid])
        right = self.sortArray(nums[mid:])
        return self._merge(left, right)
    
    def _merge(self, left, right):
        result = []
        i = j = 0
        while i < len(left) and j < len(right):
            if left[i] <= right[j]:
                result.append(left[i])
                i += 1
            else:
                result.append(right[j])
                j += 1
        result.extend(left[i:])
        result.extend(right[j:])
        return result

复杂度分析

时间复杂度:O(n log n)

空间复杂度:O(n)

变体练习

成就解锁:排序大师 — 掌握比较排序与非比较排序的适用场景,能灵活选择排序策略
LeetCode AC验证:LC 215 Kth Largest Element in an Array ✅ | LC 347 Top K Frequent Elements ✅ | LC 912 Sort an Array ✅

📝 课后练习

  1. LC 912 用快速排序实现(练习手写快排)
  2. LC 56 合并区间(排序+贪心)
  3. LC 75 颜色分类(荷兰国旗问题,三路快排)
  4. LC 378 有序矩阵中第 K 小的元素
  5. LC 451 根据字符出现频率排序

🔑 本课要点回顾

📚 扩展阅读

思考题:为什么 Python 的内置 sort 使用 TimSort(归并+插入的混合)而不是快速排序?TimSort 在什么场景下可以接近 O(n)?

🔬 排序的实战策略

面试中很少要求手写排序,但排序作为预处理步骤的场景非常多。这里总结排序在面试中的实战策略。

1. 排序 + 双指针

许多看似需要 O(n²) 的问题,排序后用双指针可以降到 O(n log n + n)。

# 排序 + 双指针的典型场景
# 两数之和(有序版)、三数之和、最接近的三数之和
# 关键: 排序后可以根据大小关系决定指针移动方向

def twoSumSorted(nums, target):
    nums.sort()  # O(n log n)
    left, right = 0, len(nums) - 1
    while left < right:  # O(n)
        s = nums[left] + nums[right]
        if s == target:
            return [left, right]
        elif s < target:
            left += 1
        else:
            right -= 1

2. 自定义排序 key

Python 的 sort 支持任意 key 函数,灵活运用可以简化很多问题。

# 按多个条件排序
# 例: 先按分数降序,同分按名字升序
students.sort(key=lambda x: (-x.score, x.name))

# 按自定义规则排序
# 例: 重新排列日志(字母日志在前,数字在后)
logs.sort(key=lambda x: (x.split()[1].isdigit(), 
                          x.split()[1:], x.split()[0]))
面试时遇到"找配对"、"最近"、"合并区间"类问题,先想想排序后是否可以用双指针或贪心。