首页 > Java > java教程 > 正文

Java 中使用归并排序高效统计“逆序对”:不满足降序排列条件的数对

花韻仙語
发布: 2025-10-03 14:44:01
原创
535人浏览过

java 中使用归并排序高效统计“逆序对”:不满足降序排列条件的数对

本文详细探讨了如何在 Java 中统计数组中不满足降序排列条件的数对(即“逆序对”),其中 a[i] < a[j] 且 i < j。文章首先介绍了一种直观的 O(N^2) 暴力解法,随后重点阐述了如何利用归并排序的特性,在 O(N log N) 的时间复杂度内高效完成计数,并提供了修正后的归并排序代码及关键优化点。

引言

在数组处理中,我们经常需要识别元素之间的特定关系。一个常见的需求是统计数组中“不满足降序排列条件的数对”,这通常指的是存在两个索引 i 和 j,满足 i < j 但 a[i] < a[j] 的情况。这类数对在计算机科学中被称为“逆序对”(Inversions),尽管在某些上下文中它也可以指代其他排序条件。本文将以 Java 语言为例,探讨两种解决此问题的方法:一种是简单直观的暴力解法,另一种是基于归并排序的优化方案。

问题定义与示例

给定一个整数数组 hs,我们需要统计所有满足以下条件的数对 (hs[i], hs[j]) 的数量:

  1. i < j (即 hs[i] 出现在 hs[j] 之前)
  2. hs[i] < hs[j] (即 hs[i] 小于 hs[j])

示例:

  • 对于 hs = [7, 3, 5, 4, 1]:
    • 3 < 5 (3 在 5 之前)
    • 3 < 4 (3 在 4 之前)
    • 总计 2 个“坏对”。
  • 对于 hs = [8, 5, 6, 7, 2, 1]:
    • 5 < 6 (5 在 6 之前)
    • 5 < 7 (5 在 7 之前)
    • 6 < 7 (6 在 7 之前)
    • 总计 3 个“坏对”。

简单双循环法 (暴力解法)

最直接的解决方案是使用嵌套循环遍历数组中的所有可能数对。外层循环遍历每个元素 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)。

核心思想:在归并过程中计数

序列猴子开放平台
序列猴子开放平台

具有长序列、多模态、单模型、大数据等特点的超大规模语言模型

序列猴子开放平台 0
查看详情 序列猴子开放平台

归并排序的 merge 函数负责将两个已排序的子数组合并成一个完整的排序数组。假设我们正在合并左子数组 L 和右子数组 R,目标是生成一个降序排列的数组(因为我们统计的是不满足降序排列的数对)。

当我们将 L 和 R 中的元素合并到最终数组时:

  1. 如果 L 的当前元素 l[lIdx] 大于或等于 R 的当前元素 r[rIdx],则 l[lIdx] 符合降序排列的要求,我们将其放入结果数组。此时不产生逆序对。
  2. 如果 L 的当前元素 l[lIdx] 小于 R 的当前元素 r[rIdx](即 l[lIdx] < r[rIdx]),这意味着 l[lIdx] 与 r[rIdx] 构成一个“坏对”。更重要的是,由于 L 和 R 内部已经分别降序排列,所以 r[rIdx] 不仅大于 l[lIdx],它也大于 L 中所有剩余的元素(从 l[lIdx] 到 L 的末尾)。因此,r[rIdx] 会与 L 中所有这些剩余元素构成“坏对”。此时,我们将 r[rIdx] 放入结果数组,并将逆序对计数器增加 L 中剩余元素的数量 (l.length - lIdx)。

代码实现:

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
    }
}
登录后复制

注意事项:

  • 避免副作用: countBadPairsMergeSort 函数首先创建了输入数组的副本 (Arrays.copyOf),以确保原始数组不会被归并排序过程修改。这是良好的编程实践,可以防止意外的副作用。
  • 累加递归结果: mergeSort 函数不仅返回当前合并操作产生的逆序对,还会递归地累加左右子数组内部的逆序对计数。这是确保所有逆序对都被统计的关键。
  • 效率优化: 使用 System.arraycopy 而不是手动循环来复制数组元素,可以显著提高大型数组的复制效率,因为 System.arraycopy 是一个本地方法,由 JVM 优化。
  • 降序排序: 在 merge 函数中,if (l[lIdx] >= r[rIdx]) 的条件决定了合并后的数组将是降序排列的。这是为了正确识别“不满足降序排列条件”的数对。

时间复杂度分析: 归并排序的时间复杂度为 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中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号