
在数组处理中,我们经常需要识别元素之间的特定关系。一个常见的需求是统计数组中“不满足降序排列条件的数对”,这通常指的是存在两个索引 i 和 j,满足 i < j 但 a[i] < a[j] 的情况。这类数对在计算机科学中被称为“逆序对”(Inversions),尽管在某些上下文中它也可以指代其他排序条件。本文将以 Java 语言为例,探讨两种解决此问题的方法:一种是简单直观的暴力解法,另一种是基于归并排序的优化方案。
给定一个整数数组 hs,我们需要统计所有满足以下条件的数对 (hs[i], hs[j]) 的数量:
示例:
最直接的解决方案是使用嵌套循环遍历数组中的所有可能数对。外层循环遍历每个元素 hs[i],内层循环遍历 hs[i] 之后的所有元素 hs[j]。如果 hs[i] < hs[j],则计数器加一。
立即学习“Java免费学习笔记(深入)”;
代码实现:
public class ArrayInversionCounter {
/**
* 使用双循环统计不满足降序排列条件的数对(逆序对)。
* 时间复杂度:O(N^2)
*
* @param hs 输入数组
* @return 逆序对的数量
*/
public static int countBadPairsBruteForce(int[] hs) {
int count = 0;
for (int i = 0; i < hs.length; i++) {
for (int j = i + 1; j < hs.length; j++) {
// 比较 hs[i] 和所有后续元素 hs[j]
if (hs[i] < hs[j]) {
// System.out.println("Found bad pair: (" + hs[i] + "," + hs[j] + ")"); // 可选:打印出坏对
count++;
}
}
}
return count;
}
public static void main(String[] args) {
System.out.println("--- 暴力解法测试 ---");
int[] arr1 = {7, 3, 5, 4, 1};
System.out.println("数组: " + java.util.Arrays.toString(arr1) + ", 逆序对数量: " + countBadPairsBruteForce(arr1)); // 预期输出: 2
int[] arr2 = {8, 5, 6, 7, 2, 1};
System.out.println("数组: " + java.util.Arrays.toString(arr2) + ", 逆序对数量: " + countBadPairsBruteForce(arr2)); // 预期输出: 3
}
}时间复杂度分析: 这种方法的缺陷在于其时间复杂度为 O(N^2),其中 N 是数组的长度。对于大规模数组,这种方法效率低下。
归并排序(Merge Sort)是一种分治算法,它将数组递归地分成两半,分别排序,然后将两个已排序的子数组合并。在合并过程中,我们可以巧妙地利用元素比较的机会来统计逆序对。归并排序的这种特性使其成为统计逆序对的理想选择,可以将时间复杂度降低到 O(N log N)。
核心思想:在归并过程中计数
归并排序的 merge 函数负责将两个已排序的子数组合并成一个完整的排序数组。假设我们正在合并左子数组 L 和右子数组 R,目标是生成一个降序排列的数组(因为我们统计的是不满足降序排列的数对)。
当我们将 L 和 R 中的元素合并到最终数组时:
代码实现:
import java.util.Arrays; // 引入Arrays类以便使用toString和arraycopy
public class MergeSortInversionCounter {
/**
* 统计不满足降序排列条件的数对(逆序对)的入口函数。
* 为了避免修改原始数组,先复制一份。
*
* @param hs 输入数组
* @return 逆序对的数量
*/
public static int countBadPairsMergeSort(int[] hs) {
// 创建数组副本,避免修改原始输入数组
int[] tempArray = Arrays.copyOf(hs, hs.length);
return mergeSort(tempArray, tempArray.length);
}
/**
* 归并排序主函数,递归地分割数组并累加逆序对计数。
*
* @param a 待排序和计数的数组(或子数组)
* @param n 数组(或子数组)的长度
* @return 当前子数组中的逆序对数量
*/
public static int mergeSort(int[] a, int n) {
// 基本情况:如果数组只有一个元素,无需排序,逆序对为0
if (n < 2) {
return 0;
}
int mid = n / 2;
int[] l = new int[mid];
int[] r = new int[n - mid];
// 使用 System.arraycopy 替代循环进行数组复制,提高效率
System.arraycopy(a, 0, l, 0, mid);
System.arraycopy(a, mid, r, 0, n - mid);
// 递归计算左右子数组中的逆序对,并将结果累加
int result = 0;
result += mergeSort(l, mid);
result += mergeSort(r, n - mid);
// 合并左右子数组并计算跨子数组的逆序对
result += merge(a, l, r);
return result;
}
/**
* 合并两个已排序的子数组,并计算合并过程中产生的逆序对。
* 同时将合并后的结果存回原数组 a 中,使其按降序排列。
*
* @param a 原始数组的引用,用于存放合并后的降序结果
* @param l 左子数组 (已降序)
* @param r 右子数组 (已降序)
* @return 合并过程中产生的逆序对数量
*/
public static int merge(int[] a, int[] l, int[] r) {
// System.out.println("Merging " + Arrays.toString(l) + " and " + Arrays.toString(r)); // 可选:打印合并过程
int inversionCount = 0;
int lIdx = 0, rIdx = 0, aIdx = 0; // 左子数组索引,右子数组索引,目标数组索引
// 遍历左右子数组,进行比较和合并
while (lIdx < l.length && rIdx < r.length) {
if (l[lIdx] >= r[rIdx]) {
// 如果左边元素更大或相等,符合降序排列,将其放入结果数组
a[aIdx++] = l[lIdx++];
} else {
// 如果右边元素更大(即 l[lIdx] < r[rIdx]),则 l[lIdx] 与 r[rIdx] 构成逆序对。
// 并且由于 l 数组已降序,l[lIdx] 之后的所有元素都小于或等于 l[lIdx],
// 所以 r[rIdx] 也比 l 数组中所有剩余元素都大。
// 此时,l 数组中从 lIdx 到末尾的所有元素都与 r[rIdx] 构成逆序对。
// System.out.println(" Found bad pair: (" + l[lIdx] + "," + r[rIdx] + ")"); // 仅作演示
inversionCount += (l.length - lIdx); // 累加逆序对数量
a[aIdx++] = r[rIdx++]; // 将 r[rIdx] 放入结果数组
}
}
// 将剩余的左子数组元素复制到结果数组
if (lIdx < l.length) {
System.arraycopy(l, lIdx, a, aIdx, l.length - lIdx);
}
// 将剩余的右子数组元素复制到结果数组
if (rIdx < r.length) {
System.arraycopy(r, rIdx, a, aIdx, r.length - rIdx);
}
return inversionCount;
}
public static void main(String[] args) {
System.out.println("\n--- 归并排序解法测试 ---");
int[] arr1 = {7, 3, 5, 4, 1};
System.out.println("数组: " + java.util.Arrays.toString(arr1) + ", 逆序对数量: " + countBadPairsMergeSort(arr1)); // 预期输出: 2
int[] arr2 = {8, 5, 6, 7, 2, 1};
System.out.println("数组: " + java.util.Arrays.toString(arr2) + ", 逆序对数量: " + countBadPairsMergeSort(arr2)); // 预期输出: 3
}
}注意事项:
时间复杂度分析: 归并排序的时间复杂度为 O(N log N),其中 N 是数组的长度。在合并过程中进行常数次操作来计数逆序对,因此总体的计数过程也保持在 O(N log N)。这比暴力解法有了显著的性能提升。
| 特性 | 简单双循环法 (暴力解法) | 基于归并排序的优化方案 |
|---|---|---|
| 时间复杂度 | O(N^2) | O(N log N) |
| 空间复杂度 | O(1) | O(N) (用于临时数组) |
| 实现难度 | 简单直观 | 相对复杂,需要理解归并排序原理 |
| 适用场景 | 数组规模较小 | 数组规模较大,对性能有要求 |
总结:
当需要统计数组中“不满足降序排列条件的数对”(逆序对)时,如果数组规模较小,简单的双循环暴力解法可能足够。然而,对于大规模数组,其 O(N^2) 的时间复杂度将导致性能瓶颈。此时,基于归并排序的优化方案是更优的选择,它利用归并过程中天然的比较操作,在 O(N log N) 的时间复杂度内高效完成计数,是解决此类问题的标准方法。理解其核心思想和实现细节对于
以上就是Java 中使用归并排序高效统计“逆序对”:不满足降序排列条件的数对的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号