1. 归并排序算法原理与力扣刷题实战指南
归并排序(Merge Sort)作为分治算法的经典代表,在力扣(LeetCode)算法题库中占据着重要地位。这个算法之所以被频繁考察,是因为它完美体现了"分而治之"的编程思想,同时又是理解递归和排序算法的绝佳案例。在实际面试中,超过60%的候选人会被要求手写归并排序实现或解决相关变种问题。
1.1 分治思想的核心要义
分治算法(Divide and Conquer)的精髓可以用三个步骤概括:
- 分解:将原问题划分为若干个规模较小的子问题
- 解决:递归地解决这些子问题
- 合并:将子问题的解合并为原问题的解
关键提示:归并排序的时间复杂度稳定为O(nlogn),这是因为它每次都将数组对半分割(logn层),每层需要进行O(n)的比较操作。这种效率在需要稳定排序的场景下非常可贵。
1.2 归并排序的标准实现
以Java为例,标准的归并排序实现包含两个核心方法:
java复制// 归并排序主方法
public void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // 防止整数溢出
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
// 合并两个有序数组
private void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++]; // 保持稳定性
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
System.arraycopy(temp, 0, arr, left, temp.length);
}
实际编码时容易踩的坑:
- 忘记处理剩余元素(第二个while循环)
- 临时数组索引计算错误
- 递归终止条件写错(应该是left < right而非left <= right)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 力扣经典题目分类解析
2.1 基础排序类题目
题目#88 合并两个有序数组
这道题是归并排序中merge操作的直接应用。关键点在于从后向前合并,避免频繁移动元素:
python复制def merge(nums1, m, nums2, n):
p1, p2, p = m-1, n-1, m+n-1
while p1 >= 0 and p2 >= 0:
if nums1[p1] > nums2[p2]:
nums1[p] = nums1[p1]
p1 -= 1
else:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
nums1[:p2+1] = nums2[:p2+1] # 处理剩余元素
2.2 分治策略进阶题目
题目#315 计算右侧小于当前元素的个数
这道题需要结合归并排序和逆序对统计:
java复制class Solution {
private int[] index;
private int[] temp;
private int[] tempIndex;
private int[] ans;
public List<Integer> countSmaller(int[] nums) {
this.index = new int[nums.length];
this.temp = new int[nums.length];
this.tempIndex = new int[nums.length];
this.ans = new int[nums.length];
for (int i = 0; i < nums.length; ++i) index[i] = i;
mergeSort(nums, 0, nums.length - 1);
List<Integer> list = new ArrayList<>();
for (int num : ans) list.add(num);
return list;
}
public void mergeSort(int[] nums, int left, int right) {
if (left >= right) return;
int mid = (left + right) >> 1;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
merge(nums, left, mid, right);
}
public void merge(int[] nums, int left, int mid, int right) {
int i = left, j = mid + 1, p = left;
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
temp[p] = nums[i];
tempIndex[p] = index[i];
ans[index[i]] += (j - mid - 1);
++i;
++p;
} else {
temp[p] = nums[j];
tempIndex[p] = index[j];
++j;
++p;
}
}
while (i <= mid) {
temp[p] = nums[i];
tempIndex[p] = index[i];
ans[index[i]] += (j - mid - 1);
++i;
++p;
}
while (j <= right) {
temp[p] = nums[j];
tempIndex[p] = index[j];
++j;
++p;
}
for (int k = left; k <= right; ++k) {
nums[k] = temp[k];
index[k] = tempIndex[k];
}
}
}
2.3 链表归并排序
题目#148 排序链表
归并排序是链表排序的最佳选择,因为它的空间复杂度可以优化到O(1):
python复制class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
# 使用快慢指针找到中点
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
mid = slow.next
slow.next = None
left = self.sortList(head)
right = self.sortList(mid)
return self.merge(left, right)
def merge(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0)
curr = dummy
while l1 and l2:
if l1.val < l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 if l1 else l2
return dummy.next
3. 归并排序的优化技巧
3.1 小规模数组切换插入排序
当子数组规模较小时(通常设定为7-15个元素),递归带来的开销会超过排序本身。此时切换为插入排序能提升约10-15%的性能:
java复制private static final int INSERTION_SORT_THRESHOLD = 7;
public void mergeSort(int[] arr, int left, int right) {
if (right - left <= INSERTION_SORT_THRESHOLD) {
insertionSort(arr, left, right);
return;
}
// ...原有归并排序逻辑
}
private void insertionSort(int[] arr, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int key = arr[i];
int j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
3.2 避免重复分配临时数组
通过在外部预分配临时数组,可以显著减少GC压力:
java复制public void sort(int[] arr) {
int[] temp = new int[arr.length]; // 一次性分配
mergeSort(arr, 0, arr.length - 1, temp);
}
private void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid, temp);
mergeSort(arr, mid + 1, right, temp);
merge(arr, left, mid, right, temp);
}
}
3.3 自然归并排序(Natural Merge Sort)
利用输入数组中已经存在的有序段(run),可以减少合并次数:
python复制def natural_merge_sort(arr):
n = len(arr)
runs = []
start = 0
# 找出所有有序段
while start < n:
end = start + 1
while end < n and arr[end] >= arr[end-1]:
end += 1
runs.append((start, end-1))
start = end
# 合并有序段
while len(runs) > 1:
new_runs = []
for i in range(0, len(runs)-1, 2):
left, right = runs[i], runs[i+1]
merged = merge(arr, left[0], left[1], right[0], right[1])
new_runs.append((left[0], right[1]))
if len(runs) % 2 == 1:
new_runs.append(runs[-1])
runs = new_runs
4. 力扣刷题实战技巧
4.1 识别归并排序适用场景
当题目出现以下特征时,考虑使用归并排序思路:
- 需要稳定排序(相对顺序保持不变)
- 涉及逆序对统计
- 链表排序需求
- 需要外部排序(大数据量无法全部加载到内存)
- 问题可以分解为子问题再合并结果
4.2 调试技巧与常见错误
栈溢出问题:
递归实现的归并排序在数据量极大时可能导致栈溢出。解决方法:
- 改用迭代实现
- 设置递归深度限制
- 使用尾递归优化(部分语言支持)
边界条件处理:
- 数组为空或单元素
- 包含重复元素
- 负数和大数情况
- 自定义对象的比较
4.3 性能优化检查表
| 优化点 | 效果 | 实现难度 |
|---|---|---|
| 小数组切换插入排序 | 提升10-15% | ★★☆ |
| 预分配临时数组 | 减少GC压力 | ★☆☆ |
| 自然归并排序 | 最佳情况O(n) | ★★★ |
| 并行化处理 | 多核加速 | ★★★★ |
| 内存访问优化 | 提升缓存命中率 | ★★★☆ |
5. 进阶题目挑战
5.1 题目#493 翻转对
这道题需要在归并过程中统计满足条件的翻转对数量:
python复制def reversePairs(nums):
def mergeSort(left, right):
if left >= right:
return 0
mid = (left + right) // 2
count = mergeSort(left, mid) + mergeSort(mid + 1, right)
# 统计翻转对
j = mid + 1
for i in range(left, mid + 1):
while j <= right and nums[i] > 2 * nums[j]:
j += 1
count += j - (mid + 1)
# 合并
nums[left:right+1] = sorted(nums[left:right+1])
return count
return mergeSort(0, len(nums) - 1)
5.2 题目#327 区间和的个数
这道题需要结合前缀和与归并排序:
java复制public int countRangeSum(int[] nums, int lower, int upper) {
long[] prefixSum = new long[nums.length + 1];
for (int i = 0; i < nums.length; i++) {
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
return mergeSort(prefixSum, 0, prefixSum.length - 1, lower, upper);
}
private int mergeSort(long[] sum, int left, int right, int lower, int upper) {
if (left >= right) return 0;
int mid = (left + right) / 2;
int count = mergeSort(sum, left, mid, lower, upper)
+ mergeSort(sum, mid + 1, right, lower, upper);
// 统计满足条件的区间
int i = mid + 1, j = mid + 1;
for (int k = left; k <= mid; k++) {
while (i <= right && sum[i] - sum[k] < lower) i++;
while (j <= right && sum[j] - sum[k] <= upper) j++;
count += j - i;
}
// 合并
Arrays.sort(sum, left, right + 1);
return count;
}
6. 面试常见问题解析
6.1 归并排序 vs 快速排序
| 比较维度 | 归并排序 | 快速排序 |
|---|---|---|
| 时间复杂度 | 稳定O(nlogn) | 平均O(nlogn),最差O(n²) |
| 空间复杂度 | O(n) | O(logn) |
| 稳定性 | 稳定 | 不稳定 |
| 适用场景 | 链表排序、外部排序 | 内存排序、需要原地排序 |
| 缓存友好 | 较差 | 较好 |
6.2 如何实现原地归并排序?
标准的归并排序需要额外O(n)空间,但通过复杂算法可以实现原地合并:
python复制def merge_in_place(arr, start, mid, end):
i = start
j = mid + 1
while i <= mid and j <= end:
if arr[i] <= arr[j]:
i += 1
else:
# 将arr[j]插入到arr[i]前面
temp = arr[j]
for k in range(j, i, -1):
arr[k] = arr[k - 1]
arr[i] = temp
i += 1
mid += 1 # 因为插入了一个元素
j += 1
注意:这种实现虽然节省了空间,但时间复杂度会退化为O(n²),实际应用中很少使用。
6.3 多路归并排序的应用
当需要合并k个有序数组时,可以使用多路归并:
java复制public int[] mergeKSortedArrays(int[][] arrays) {
PriorityQueue<Element> minHeap = new PriorityQueue<>(
(a, b) -> a.value - b.value
);
// 初始化堆
for (int i = 0; i < arrays.length; i++) {
if (arrays[i].length > 0) {
minHeap.offer(new Element(i, 0, arrays[i][0]));
}
}
List<Integer> result = new ArrayList<>();
while (!minHeap.isEmpty()) {
Element curr = minHeap.poll();
result.add(curr.value);
if (curr.index + 1 < arrays[curr.array].length) {
minHeap.offer(new Element(
curr.array,
curr.index + 1,
arrays[curr.array][curr.index + 1]
));
}
}
return result.stream().mapToInt(i->i).toArray();
}
class Element {
int array, index, value;
public Element(int array, int index, int value) {
this.array = array;
this.index = index;
this.value = value;
}
}
