Sorting(排序算法)
排序是算法基础中的基础。PDF 里按考察频率把排序分成了三类:
- 常考的:Merge Sort、Quick Sort(以及 Quick Select)、Bucket Sort
- 偶尔考的:Counting Sort、Heap Sort(在 Heap 部分讲)、Pancake Sort
- 不考的:Bubble Sort、Selection Sort、Insertion Sort、Shell Sort、Radix Sort(了解即可)
本专题重点讲前三个常考的,它们的时间复杂度都是 O(n log n),并且都蕴含了重要的算法思想。
1. 归并排序(Merge Sort)
核心思想
归并排序是**分治法(Divide and Conquer)**的教科书式实现:
- 分:将数组不断从中间二分,直到子数组只剩一个元素
- 治:将两个有序的子数组合并成一个有序数组
特点是稳定排序 ,且时间复杂度在任何情况下都是 O(n log n)。空间复杂度 O(n)(需要临时数组)。
代码模板(两份:上浮法 + 下沉法)
java
// 主函数:递归分治
public int[] mergeSort(int[] nums) {
if (nums == null || nums.length <= 1) return nums;
int[] temp = new int[nums.length]; // 全局临时数组,避免反复创建
mergeSort(nums, 0, nums.length - 1, temp);
return nums;
}
private void mergeSort(int[] nums, int left, int right, int[] temp) {
if (left >= right) return; // 递归终止:只剩一个元素
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid, temp); // 分:排好左边
mergeSort(nums, mid + 1, right, temp); // 分:排好右边
merge(nums, left, mid, right, temp); // 治:合并两个有序数组
}
// 合并两个有序数组 [left, mid] 和 [mid+1, right]
private void merge(int[] nums, int left, int mid, int right, int[] temp) {
int i = left, j = mid + 1, k = left;
// 两个指针比较,把较小的放入 temp
while (i <= mid && j <= right) {
temp[k++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];
}
// 把剩余元素补上
while (i <= mid) temp[k++] = nums[i++];
while (j <= right) temp[k++] = nums[j++];
// 拷回原数组
for (int p = left; p <= right; p++) {
nums[p] = temp[p];
}
}
重要应用
- 链表排序(LeetCode 148. Sort List)最适合用归并排序
- 求逆序对(LeetCode 493)------ 在
merge过程中统计nums[i] > nums[j]的情况
2. 快速排序(Quick Sort)与快速选择(Quick Select)
核心思想
快速排序也是分治法,但和归并排序思路相反:
- 选枢轴(pivot):选一个基准元素
- 分区(partition):把小于 pivot 的放左边,大于 pivot 的放右边,pivot 到了它最终的正确位置
- 递归:对 pivot 的左边和右边分别做同样的事
平均 O(n log n) ,排序不需要额外空间(但递归栈 O(log n))。最差 O(n²),可以用随机化 pivot 或 shuffle 来避免。
分区模板(两种写法)
写法一:墙法(更直观)
java
private int partition(int[] nums, int left, int right) {
int pivot = nums[right]; // 选最右边为 pivot
int wall = left; // 墙的左边全比 pivot 小
for (int i = left; i < right; i++) {
if (nums[i] < pivot) {
swap(nums, i, wall); // 把小于 pivot 的数放到墙的左边
wall++;
}
}
swap(nums, wall, right); // pivot 放到墙的位置(最终位置)
return wall; // 返回 pivot 的位置
}
写法二:双指针法
java
private int partition2(int[] nums, int left, int right) {
int pivot = nums[right];
int start = left, end = right - 1;
while (start <= end) {
if (nums[start] <= pivot) start++;
else if (nums[end] > pivot) end--;
else swap(nums, start++, end--);
}
swap(nums, start, right); // start 就是 pivot 最终位置
return start;
}
完整快排代码
java
public int[] quickSort(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
private void quickSort(int[] nums, int left, int right) {
if (left >= right) return;
int pivotIndex = partition(nums, left, right);
quickSort(nums, left, pivotIndex - 1);
quickSort(nums, pivotIndex + 1, right);
}
快速选择(Quick Select)------ 找第 K 大元素
核心 :不用把数组完全排序。每次 partition 后 pivot 到了正确位置 pos,如果 pos 正好是我们要找的索引,直接返回;否则根据 pos 与 target 的关系,只递归一边。
时间复杂度 :平均 O(n),最坏 O(n²)。
例题:Kth Largest Element in an Array(215, Medium)
java
public int findKthLargest(int[] nums, int k) {
// 第 K 大 = 第 (n - k) 小,对应索引 nums.length - k
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
private int quickSelect(int[] nums, int left, int right, int targetIdx) {
if (left == right) return nums[left]; // 区间只有一个数了
int pos = partition(nums, left, right);
if (pos == targetIdx) return nums[pos];
else if (pos < targetIdx) return quickSelect(nums, pos + 1, right, targetIdx);
else return quickSelect(nums, left, pos - 1, targetIdx);
}
3. 桶排序(Bucket Sort)
核心思想
当数据分布均匀时,桶排序是最快的。
- 建立若干个桶(bucket)
- 根据某种映射把元素分配到对应的桶中
- 每个桶内各自排序(通常用插入排序)
- 按顺序合并所有桶
时间复杂度 :最好 O(n + k),其中 k 是桶的数量
典型例题:Top K Frequent Elements(347, Medium)
题目:给定一个数组,返回出现频率前 K 高的元素。
思路:
- 用 HashMap 统计每个元素的频率
- 用一个
List<Integer>[]数组作为桶,索引表示频率,桶内存放该频率的所有元素 - 从高频到低频遍历桶,收集 K 个元素
代码:
java
public int[] topKFrequent(int[] nums, int k) {
// 1. 统计频率
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
// 2. 建立桶,桶索引 = 频率,最多可能有 nums.length 次出现
List<Integer>[] bucket = new List[nums.length + 1];
for (int key : freqMap.keySet()) {
int freq = freqMap.get(key);
if (bucket[freq] == null) {
bucket[freq] = new ArrayList<>();
}
bucket[freq].add(key);
}
// 3. 从高频率桶向低频率桶遍历,收集结果
List<Integer> res = new ArrayList<>();
for (int i = bucket.length - 1; i >= 0 && res.size() < k; i--) {
if (bucket[i] != null) {
res.addAll(bucket[i]);
}
}
// 转成 int[]
return res.stream().mapToInt(i -> i).toArray(); // 或者直接 List<Integer> 也行
}
计数排序(Counting Sort)
是桶排序的特例,当元素范围确定(如 0-255)时可以直接用计数数组。后文堆排序将在 Heap 部分专门讲解。
总结:排序算法速查
| 排序算法 | 时间(平均) | 时间(最坏) | 空间 | 稳定 | 核心考点 |
|---|---|---|---|---|---|
| Merge Sort | O(n log n) | O(n log n) | O(n) | ✅ 稳定 | 分治思想、合并两个有序数组、链表排序 |
| Quick Sort | O(n log n) | O(n²) | O(log n) | ❌ 不稳定 | partition 模板、随机 pivot 避免最坏 |
| Quick Select | O(n) | O(n²) | O(log n) | ❌ | 第K大/小元素、只递归一边 |
| Bucket Sort | O(n + k) | O(n²) | O(n + k) | 取决于桶内 | 频率桶(Top K Frequent) |
| Heap Sort | O(n log n) | O(n log n) | O(1) | ❌ | heapify 建堆、详细见 Heap 专题 |
与二分、分治的关系:
- Quick Select + Binary Search:在有序性可以帮助缩小范围时两者结合(比如在一个排序数组中找某个条件的位置)
- Sorting 是分治法的子领域,Merge Sort 和 Quick Sort 都是典型分而治之