0

0

使用归并排序高效统计数组中的“坏对”数量

心靈之曲

心靈之曲

发布时间:2025-10-03 14:35:00

|

987人浏览过

|

来源于php中文网

原创

使用归并排序高效统计数组中的“坏对”数量

本文探讨了如何高效统计数组中不满足降序排列条件的“坏对”数量,即前一个元素小于后一个元素的情况。文章首先介绍了一种直观的O(N^2)暴力解法,随后重点阐述了如何通过改进归并排序算法,在O(N log N)的时间复杂度内完成统计,并提供了详细的代码实现和注意事项,旨在帮助读者理解并应用该优化技术。

引言:理解“坏对”问题

在数组处理中,有时我们需要识别不符合特定排序规则的元素对。本教程关注的是一种特殊的“坏对”:给定一个整数数组 hs,如果存在索引 i < j 使得 hs[i] < hs[j],则 (hs[i], hs[j]) 被视为一个“坏对”。我们的目标是统计数组中所有此类“坏对”的总数。

例如:

  • 对于 hs = [7, 3, 5, 4, 1],坏对有 (3, 5) 和 (3, 4),总数为 2。
  • 对于 hs = [8, 5, 6, 7, 2, 1],坏对有 (5, 6)、(5, 7) 和 (6, 7),总数为 3。

这个问题本质上是统计数组中的“逆序对”,但通常逆序对定义为 arr[i] > arr[j] 且 i < j。这里我们是统计 arr[i] < arr[j] 且 i < j 的对,可以理解为在目标是降序排列时,元素顺序错误的对。

暴力解法:O(N^2) 时间复杂度

最直观的方法是使用两层嵌套循环遍历数组,对每对满足 i < j 的元素进行比较。

算法思路

  1. 初始化一个计数器 count 为 0。
  2. 外层循环 i 从 0 遍历到 hs.length - 1。
  3. 内层循环 j 从 i + 1 遍历到 hs.length - 1。
  4. 在内层循环中,如果 hs[i] < hs[j],则 count 加 1。
  5. 返回 count。

示例代码

public class BadPairCounter {

    /**
     * 使用暴力法统计数组中的“坏对”数量。
     * 时间复杂度:O(N^2)
     *
     * @param hs 输入整数数组
     * @return 坏对的总数
     */
    public static int countBaadBruteForce(int[] hs) {
        int count = 0;
        for (int i = 0; i < hs.length; i++) {
            for (int j = i + 1; j < hs.length; 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) {
        int[] arr1 = {7, 3, 5, 4, 1};
        System.out.println("Array: " + java.util.Arrays.toString(arr1) + ", Bad pairs (Brute Force): " + countBaadBruteForce(arr1)); // Expected: 2

        int[] arr2 = {8, 5, 6, 7, 2, 1};
        System.out.println("Array: " + java.util.Arrays.toString(arr2) + ", Bad pairs (Brute Force): " + countBaadBruteForce(arr2)); // Expected: 3
    }
}

局限性

暴力解法简单易懂,但其时间复杂度为 O(N^2),对于大型数据集,性能会急剧下降,导致计算时间过长。

归并排序优化法:O(N log N) 时间复杂度

为了提高效率,我们可以利用归并排序(Merge Sort)的特性来解决这个问题。归并排序在合并两个已排序的子数组时,会进行元素间的比较,这正是我们统计“坏对”的绝佳时机。

Cursor
Cursor

一个新的IDE,使用AI来帮助您重构、理解、调试和编写代码。

下载

核心思想

标准的归并排序将数组递归地分成两半,直到每个子数组只包含一个元素。然后,它将这些子数组两两合并,每次合并都确保子数组是有序的。在这个合并过程中,我们可以有效地统计“坏对”。

为了统计 hs[i] < hs[j] 且 i < j 的对,我们可以修改归并排序,使其在合并两个降序排列的子数组时进行计数。

假设我们有两个已经按降序排列的子数组 L 和 R。在合并它们时,我们从 L 和 R 的头部开始比较:

  • 如果 L[lIdx] >= R[rIdx],这意味着 L[lIdx] 已经比 R[rIdx] 大或相等,符合降序要求,我们将其放入结果数组。
  • 如果 L[lIdx] < R[rIdx],这意味着 L[lIdx] 小于 R[rIdx]。由于 L 和 R 都是降序排列的,并且 L[lIdx] 是 L 中当前最小(即最靠后)的未处理元素,那么 L[lIdx] 不仅小于 R[rIdx],它也小于 R 中所有比 R[rIdx] 更小的元素。更重要的是,R[rIdx] 比 L 中所有从 L[lIdx] 到 L 结尾的元素都大。因此,R[rIdx] 与 L 中所有剩余的元素(L[lIdx] 到 L[L.length-1])都构成了“坏对”。此时,我们将 R[rIdx] 放入结果数组,并将 count 增加 L 中剩余元素的数量 (L.length - lIdx)。

算法步骤

  1. countBaad 方法:作为入口,调用 mergeSort 方法。为了避免修改原始数组,可以先复制一份。
  2. mergeSort 方法
    • 基准情况:如果数组长度小于 2(即 0 或 1),则无需排序,也没有“坏对”,返回 0。
    • 分割:将数组 a 分成左右两个子数组 l 和 r。可以使用 System.arraycopy 提高效率。
    • 递归:递归调用 mergeSort(l, mid) 和 mergeSort(r, n - mid),并将它们返回的“坏对”数量累加到 result 中。
    • 合并与计数:调用 merge(a, l, r) 方法,将左右子数组合并到 a 中,并返回合并过程中发现的“坏对”数量,同样累加到 result 中。
    • 返回 result。
  3. merge 方法
    • 初始化 size(用于计数坏对)、lIdx、rIdx、aIdx 为 0。
    • 当 lIdx 和 rIdx 都在各自数组范围内时:
      • 如果 l[lIdx] >= r[rIdx]:将 l[lIdx] 放入 a,lIdx 和 aIdx 递增。
      • 如果 l[lIdx] < r[rIdx]:这意味着 l[lIdx] 与 r[rIdx] 及其后面所有比 l[lIdx] 大的元素形成“坏对”。更准确地说,r[rIdx] 比 L 中所有从 lIdx 到 L.length-1 的元素都大。因此,将 L 中剩余的元素数量 (l.length - lIdx) 加到 size 中。然后将 r[rIdx] 放入 a,rIdx 和 aIdx 递增。
    • 处理剩余元素:将 l 或 r 中剩余的元素直接复制到 a 的末尾。这里同样可以使用 System.arraycopy。
    • 返回 size。

示例代码

import java.util.Arrays;

public class BadPairMergeSortCounter {

    /**
     * 入口方法,统计数组中的“坏对”数量。
     * 为了不修改原始数组,先进行复制。
     *
     * @param hs 输入整数数组
     * @return 坏对的总数
     */
    public static int countBaad(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) {
        if (n <= 1) { // 基准情况:数组长度为0或1,没有坏对
            return 0;
        }

        int mid = n / 2;
        int[] l = new int[mid];
        int[] r = new int[n - mid];

        // 分割数组
        System.arraycopy(a, 0, l, 0, mid);
        if (n - mid > 0) { // 确保右子数组有元素
            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;
    }

    /**
     * 合并两个已降序排序的子数组,并统计合并过程中产生的“坏对”。
     *
     * @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 badPairCount = 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 数组和 r 数组都是降序排列的,
                // r[rIdx] 比 l 数组中从 lIdx 位置到末尾的所有元素都大。
                // 因此,r[rIdx] 与 l 数组中所有剩余元素 (l.length - lIdx 个) 都构成“坏对”。
                badPairCount += (l.length - lIdx);
                // System.out.println("  Found " + (l.length - lIdx) + " bad pairs with " + r[rIdx] + " from " + Arrays.toString(Arrays.copyOfRange(l, lIdx, l.length))); // 可选:详细日志
                a[aIdx++] = 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 badPairCount;
    }

    public static void main(String[] args) {
        int[] arr1 = {7, 3, 5, 4, 1};
        System.out.println("Array: " + java.util.Arrays.toString(arr1) + ", Bad pairs (Merge Sort): " + countBaad(arr1)); // Expected: 2

        int[] arr2 = {8, 5, 6, 7, 2, 1};
        System.out.println("Array: " + java.util.Arrays.toString(arr2) + ", Bad pairs (Merge Sort): " + countBaad(arr2)); // Expected: 3
    }
}

性能与注意事项

  1. 时间复杂度:归并排序本身的时间复杂度为 O(N log N)。在 merge 过程中,我们只增加了常数时间的比较和计数操作,因此整体时间复杂度依然保持在 O(N log N),这比暴力解法 O(N^2) 有显著提升。
  2. 空间复杂度:归并排序需要额外的空间来存储子数组,因此空间复杂度为 O(N)。
  3. 避免副作用:mergeSort 算法会对其操作的数组进行排序。如果仅仅需要统计“坏对”数量而不希望修改原始数组,务必在 countBaad 方法中对输入数组进行深拷贝,例如使用 Arrays.copyOf(hs, hs.length)。
  4. System.arraycopy:在复制子数组时,使用 System.arraycopy 比手动循环赋值效率更高,尤其对于大型数组。

总结

统计数组中特定类型的“坏对”是一个常见的算法问题。虽然暴力解法简单直接,但其 O(N^2) 的时间复杂度使其不适用于大规模数据。通过巧妙地修改归并排序算法,我们可以在 O(N log N) 的时间复杂度内高效地解决这个问题,这在处理大量数据时具有显著的性能优势。理解归并排序中 merge 阶段的计数逻辑是实现这一优化的关键。在实际应用中,还需注意对原始数据的保护(通过复制数组)以及使用高效的数组操作(如 System.arraycopy)。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

203

2023.11.20

sort排序函数用法
sort排序函数用法

sort排序函数的用法:1、对列表进行排序,默认情况下,sort函数按升序排序,因此最终输出的结果是按从小到大的顺序排列的;2、对元组进行排序,默认情况下,sort函数按元素的大小进行排序,因此最终输出的结果是按从小到大的顺序排列的;3、对字典进行排序,由于字典是无序的,因此排序后的结果仍然是原来的字典,使用一个lambda表达式作为key参数的值,用于指定排序的依据。

409

2023.09.04

length函数用法
length函数用法

length函数用于返回指定字符串的字符数或字节数。可以用于计算字符串的长度,以便在查询和处理字符串数据时进行操作和判断。 需要注意的是length函数计算的是字符串的字符数,而不是字节数。对于多字节字符集,一个字符可能由多个字节组成。因此,length函数在计算字符串长度时会将多字节字符作为一个字符来计算。更多关于length函数的用法,大家可以阅读本专题下面的文章。

954

2023.09.19

页面置换算法
页面置换算法

页面置换算法是操作系统中用来决定在内存中哪些页面应该被换出以便为新的页面提供空间的算法。本专题为大家提供页面置换算法的相关文章,大家可以免费体验。

500

2023.08.14

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

25

2026.03.13

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

44

2026.03.12

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

177

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

50

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

92

2026.03.09

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.3万人学习

Java 教程
Java 教程

共578课时 | 81.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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