数组逆序对(Array Inversion Pairs)

📌 定义

数组的逆序对是指数组中的两个元素 (i, j),满足 i < j 且 arr[i] > arr[j]。逆序对的数量反映了数组的混乱程度。

数组: [7, 5, 6, 4]

逆序对:
(7, 5), (7, 6), (7, 4), (5, 4), (6, 4)

逆序对总数: 5

核心思路

使用归并排序的思想,在归并过程中统计逆序对:

  • 分解:将数组分成左右两部分
  • 递归:分别统计左右两部分的逆序对
  • 合并:统计跨越左右两部分的逆序对

关键:在合并有序数组时,如果左边元素大于右边元素,则形成逆序对。

数组: [7, 5, 6, 4]
         ↓
    [7, 5]  [6, 4]
      ↓       ↓
   [7][5]  [6][4]

合并时统计逆序对:
- 左半部分: 1个 (7, 5)
- 右半部分: 1个 (6, 4)
- 跨越部分: 3个 (7, 6), (7, 4), (5, 4)
总计: 5个

状态与不变量

令 count(left, right) 表示子数组 arr[left:right+1] 内的逆序对数量。递归返回时同时保证两件事:

  1. 返回值已经统计了当前区间内的全部逆序对。
  2. 当前区间已经被原地整理为有序,供上一层合并使用。

当前区间的答案可以拆成三部分:

左半逆序对 + 右半逆序对 + 跨越左右两半的逆序对

合并时如果 arr[i] > arr[j],由于左半部分有序,arr[i] 到 arr[mid] 都大于 arr[j],所以一次累加 mid-i+1,而不是逐个枚举。

🎞️ 合并过程动画

(附件 merge-sort-split-merge.svg 未随站点发布)

看动画时只盯住合并阶段:右半部分元素先被取出时,左边剩余元素的数量就是新增逆序对数量。

复杂度分析

指标暴力枚举归并排序
时间复杂度O(n²)O(n log n)
空间复杂度O(1)O(n)
稳定性-可以保持稳定

Go 代码

Go 实现

package main
 
import "fmt"
 
func countInversions(arr []int) int {
    temp := make([]int, len(arr))
    return mergeSort(arr, temp, 0, len(arr)-1)
}
 
func mergeSort(arr, temp []int, left, right int) int {
    if left >= right {
        return 0
    }
 
    mid := (left + right) / 2
 
    count := mergeSort(arr, temp, left, mid)
    count += mergeSort(arr, temp, mid+1, right)
    count += merge(arr, temp, left, mid, right)
 
    return count
}
 
func merge(arr, temp []int, left, mid, right int) int {
    i := left
    j := mid + 1
    k := left
    count := 0
 
    for i <= mid && j <= right {
        if arr[i] <= arr[j] {
            temp[k] = arr[i]
            i++
        } else {
            temp[k] = arr[j]
            count += (mid - i + 1)  // 统计逆序对
            j++
        }
        k++
    }
 
    for i <= mid {
        temp[k] = arr[i]
        i++
        k++
    }
 
    for j <= right {
        temp[k] = arr[j]
        j++
        k++
    }
 
    for i := left; i <= right; i++ {
        arr[i] = temp[i]
    }
 
    return count
}
 
func main() {
    arr := []int{7, 5, 6, 4}
    inversions := countInversions(arr)
    fmt.Printf("逆序对数量: %d\n", inversions)
}

思路展开

逆序对统计原理

左半部分: [5, 7] (已排序)
右半部分: [4, 6] (已排序)

合并过程:
比较 5 和 4: 5 > 4
  → 4加入结果,逆序对 += 2 (5,4) (7,4)

比较 5 和 6: 5 < 6
  → 5加入结果

比较 7 和 6: 7 > 6
  → 6加入结果,逆序对 += 1 (7,6)

比较 7 (无): 7加入结果

结果: [4, 5, 6, 7]
跨越逆序对: 3个

为什么是 mid - i + 1?

当 arr[i] > arr[j] 时:

  • 左边从 i 到 mid 的所有元素都大于 arr[j]
  • 因为左半部分已经是有序的
  • 所以一次性统计 mid - i + 1 个逆序对

经典题目

LeetCode 问题

扩展问题

  • 求数组中的顺序对(正序对)
  • 区间逆序对查询
  • 二维数组逆序对

⚖️ 优缺点

优点

  • ✅ 高效:O(n log n)时间复杂度
  • ✅ 稳定:可以保持相等元素的相对顺序
  • ✅ 副作用:排序完成后数组也变有序

缺点

  • ❌ 空间复杂度:需要O(n)额外空间
  • ❌ 实现复杂:相比暴力法代码较复杂

🎨 应用场景

  1. 排序算法性能分析:逆序对数量反映数组混乱程度
  2. 推荐系统:计算用户偏好的相似度
  3. 数据挖掘:寻找数据中的异常模式
  4. 统计学:Kendall秩相关系数计算
  5. 竞赛排名:比较两个排名的差异

💡 变体问题

  • 翻转对:把判断条件从 arr[i] > arr[j] 改成 arr[i] > 2*arr[j],通常在归并前增加双指针统计。
  • 右侧小于当前元素的个数:对每个元素记录它在归并过程中被右侧元素跨过的次数。
  • 动态逆序对:如果数组会在线修改,通常改用树状数组、线段树或 CDQ 分治。
  • 二维偏序问题:先按一个维度排序,再把另一个维度转成逆序对统计。

相关主题


返回:分治算法 | 算法学习导航