ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

归并排序解决LeetCode翻转对问题

归并排序解决LeetCode翻转对问题

1. 问题背景与核心挑战

LeetCode 493题"翻转对"(Reverse Pairs)是算法练习中的一道经典难题,要求统计数组中满足i < j且nums[i] > 2*nums[j]的元素对数。这个问题看似简单,但直接使用双重循环的暴力解法时间复杂度为O(n²),在数据量较大时(如10^5级别)会超时。

我在实际刷题和面试准备过程中发现,这道题考察的核心是分治思想与归并排序的灵活运用。相比单纯的排序问题,它需要我们在归并过程中同步完成特定条件的统计,这对理解算法本质提出了更高要求。

2. 解法思路与技术选型

2.1 暴力解法的局限性

最直观的解法是两层循环遍历所有(i,j)组合:

int count = 0; for(int i=0; i<nums.length; i++){ for(int j=i+1; j<nums.length; j++){ if(nums[i] > 2L*nums[j]) count++; } } return count;

当n=5×10^4时,操作次数将达到25亿次(远超一般OJ系统1秒内能处理的10^8次操作限制)。

2.2 分治与归并排序的优势

归并排序天然具有分治特性:

  1. 将数组分成两半分别处理(分治)
  2. 合并两个有序子数组时进行特定统计
  3. 时间复杂度优化到O(n log n)

关键突破点在于:在合并两个有序子数组前,可以高效统计跨子数组的翻转对数量。因为左右子数组已经各自有序,可以利用这个性质通过双指针技巧在O(n)时间内完成统计。

3. 归并排序解法实现细节

3.1 Java实现框架

public int reversePairs(int[] nums) { return mergeSort(nums, 0, nums.length-1); } private int mergeSort(int[] nums, int left, int right){ if(left >= right) return 0; int mid = left + (right-left)/2; int count = mergeSort(nums, left, mid) + mergeSort(nums, mid+1, right); count += merge(nums, left, mid, right); return count; }

3.2 关键统计逻辑实现

private int merge(int[] nums, int left, int mid, int right){ // 统计翻转对 int i = left, j = mid+1; int count = 0; while(i <= mid && j <= right){ if(nums[i] > 2L * nums[j]){ count += mid - i + 1; j++; }else{ i++; } } // 标准归并排序合并过程 int[] temp = new int[right-left+1]; // ...省略合并代码... return count; }

注意:必须使用2L强制转换为long类型,避免大数相乘导致的整数溢出问题。这是实际编码中常见的坑点。

4. 树状数组解法对比分析

4.1 离散化处理

由于原始数值范围可能很大(如[-2^31, 2^31-1]),需要先对数组进行离散化:

  1. 收集所有nums[i]和2*nums[i]+1(确保严格大于)
  2. 排序后去重,建立值到排名的映射

4.2 树状数组操作

// 离散化后的实现 public int reversePairs(int[] nums) { // 离散化代码省略... BIT bit = new BIT(discretized.size()); int res = 0; for(int i=nums.length-1; i>=0; i--){ int val = discretized.get(nums[i]); res += bit.query(lowerBound(discretized, 2L*nums[i]+1)); bit.update(val, 1); } return res; }

4.3 性能对比

方法时间复杂度空间复杂度编码复杂度
归并排序O(n log n)O(n)中等
树状数组O(n log n)O(n)较高
暴力解法O(n²)O(1)简单

归并排序版本在实际面试中更受青睐,因为:

  1. 不需要处理离散化的边缘情况
  2. 代码结构更清晰直观
  3. 空间使用更可控

5. 常见错误与调试技巧

5.1 整数溢出问题

错误示例:

if(nums[i] > 2 * nums[j]) // 当nums[j]>1e9时会溢出

正确写法:

if(nums[i] > 2L * nums[j]) // 使用long类型

5.2 统计时机错误

必须在合并两个有序数组前完成统计,如果在合并后才统计,会漏掉跨子数组的翻转对。

5.3 边界条件处理

测试用例应包括:

  • 空数组
  • 全相同元素数组
  • 最大/最小整数值
  • 完全正序/逆序数组

6. 算法扩展与变种

6.1 CDQ分治解法

CDQ分治是处理三维偏序问题的利器,虽然本题是二维偏序,但可以用其思想:

  1. 将每个元素视为(i, nums[i])的二元组
  2. 第一维按i排序(天然满足i<j)
  3. 第二维用归并处理nums[i]>2*nums[j]

6.2 实际工程应用

类似算法可用于:

  • 金融交易系统中的异常交易检测
  • 基因组序列比对中的反转位点统计
  • 版本控制系统中的代码变更影响分析

7. 性能优化实践

7.1 归并排序的空间优化

可以复用临时数组而非每次新建:

// 类成员变量 private int[] temp; // 初始化时分配一次 temp = new int[nums.length];

7.2 提前终止优化

当左子数组最小值已经>2*右子数组最大值时,所有左子数组元素都满足条件:

if(nums[left] > 2L * nums[right]){ count += (mid-left+1)*(right-mid); // 快速合并剩余元素... }

8. 不同语言实现要点

8.1 C++实现注意

  • 使用vector代替原生数组更安全
  • 注意iterator的使用范围
int mergeSort(vector<int>& nums, int left, int right){ if(left >= right) return 0; int mid = left + (right-left)/2; int count = mergeSort(nums, left, mid) + mergeSort(nums, mid+1, right); // 统计逻辑 int i = left, j = mid+1; while(i <= mid && j <= right){ if(nums[i] > 2LL * nums[j]){ count += mid - i + 1; j++; }else{ i++; } } // ...合并逻辑 return count; }

8.2 Python实现特点

  • 利用切片简化代码
  • 注意整数自动转为long的特性
def reversePairs(nums): def merge_sort(l, r): if l >= r: return 0 mid = (l + r) // 2 count = merge_sort(l, mid) + merge_sort(mid+1, r) # 统计逻辑 j = mid + 1 for i in range(l, mid+1): while j <= r and nums[i] > 2 * nums[j]: j += 1 count += j - (mid + 1) # 合并 nums[l:r+1] = sorted(nums[l:r+1]) return count return merge_sort(0, len(nums)-1)

9. 测试用例设计策略

完整的测试应包含以下场景:

  1. 常规测试
    Input: [1,3,2,3,1] Output: 2
  2. 边界测试
    Input: [2147483647,2147483647,2147483647] // MAX_INT Output: 0
  3. 性能测试
    Input: [10000000,9999999,...,1] // 1e5个逆序元素 Expected: 在1秒内完成
  4. 特殊值测试
    Input: [] // 空数组 Output: 0

10. 实际编码中的经验总结

  1. 调试技巧:在归并过程中打印子数组状态,可视化统计过程:

    System.out.printf("Processing [%d,%d] and [%d,%d]\n", left, mid, mid+1, right);
  2. 性能分析:使用JMH进行微基准测试,比较不同实现的吞吐量:

    @Benchmark public void testMergeSortSolution(Blackhole bh) { bh.consume(solution.reversePairs(testData)); }
  3. 代码风格:将统计逻辑与合并逻辑分离,提高可读性:

    private int countPairs(int[] nums, int left, int mid, int right){ // 纯统计逻辑 } private void merge(int[] nums, int left, int mid, int right){ // 纯合并逻辑 }
  4. 扩展思考:如果条件改为nums[i] > 3*nums[j],算法结构是否变化?实际上只需要修改比较条件,整体框架保持不变。

返回列表