【算法32】计算数组中的逆序对

问题描述

设 A[1...n] 是一个数组,如果对于 i < j 有 A[i] > A[j], 则 A[i] 和 A[j] 构成一对逆序。给定一个数组,计算数组中逆序对的个数。例如数组 a[] = {1, 4, 3, 2}, 则 {4, 3} {4, 2} {3, 2}是逆序对,返回 3。

解法一:暴力求解

两个 for 循环枚举所有的数对,如果是逆序对,则 count++,最终返回 count 即可。时间复杂度 O(n^2),代码如下:

 1 #include <iostream>
 2 #include <string>
 3 #include <vector>
 4 using namespace std;
 5
 6 int CountInversions(const vector<int>& a)
 7 {
 8     int cnt = 0;
 9     for (size_t i = 0; i < a.size(); i++)
10     {
11         for (size_t j = i + 1; j < a.size(); j++)
12         {
13             if (a[i] > a[j]) cnt++;
14         }
15     }
16     return cnt;
17 }
18
19 int main()
20 {
21     int a[] = {1, 4, 3, 2};
22     vector<int> v(a, a + 4);
23     cout << CountInversions(v) << endl;
24     return 0;
25 }

解法二:Divide & Conquer (修改归并排序)

这个解法在《算法导论》中归并排序那一节的思考题 2-4 中有提到。原题如下:

设 A[1...n] 是一包含 n 个不同数的数组,如果在 i < j 的情况下,有 A[i] > A[j], 则 (i, j) 称为 A 中的一个逆序 (inversion)。

(a) 列举出 <2, 3, 8, 6, 1> 的 5 个逆序对。

答: <2, 1> <3, 1> <8, 6> <8, 1> <6, 1>

(b) 如果数组的元素取自集合 {1, 2, 3, ..., n}, 那么怎么样的数组含有最多的逆序? 它包含多少个逆序?

答:n 个元素最多有 n(n-1) / 2 个数组,在最坏的情况下,所有的数对都是逆序,因而最多有 n(n-1)/2 个逆序。这样的数组为倒序 {n,n-1, ..., 2, 1}

(c) 插入排序的运行时间与输入数组中逆序对的数量之间有怎么样的关系?说明你的理由。

答:插入排序的核心过程如下,可以看到内循环的执行实际上是因为 {i, j} 构成了逆序,换句话说,内循环执行完毕后后 {i, j}的逆序被消去,所以插入排序总的循环次数 == 数组中逆序对个数。

1 for (int i = 1; i < n; ++i)
2 {
3     int x = a[i];
4     for (int j = i - 1; j >= 0 && a[j] > x; --j)
5     {
6         a[j + 1] = a[j];
7     }
8     a[j + 1] = x;
9 }

(d) 给出一个算法,它能用 O(nlogn) 的最坏情况运行时间,确定 n 个元素的任何排列中的逆序对数。(提示:修改归并排序)

归并排序的基本思想就是 Divide & Conquer: 将数组划分为左右两部分,归并排序左部分,归并排序右部分,然后 Merge 左右两个数组为一个新的数组,从而完成排序。按照这个基本思想,我们也可以运用到计算逆序对中来,假设我们将数组划分左右两部分,并且我告诉你左边部分的逆序对有 inv1 个, 右边部分的逆序对有 inv2 个,如下图所示

那么剩余的逆序对必然是一个数出现在左边部分,一个数出现在右边部分,并且满足出现左边部分的数 a[i] > a[j], 如下图所示, 值得说明是:左右两部排序与否,不会影响这种情况下的逆序对数,因为左右两部分的排序只是消除了两部分内部的逆序对,而对于 a[i] 来自左边, a[j] 来自右边构成的逆序,各自排序后还是逆序

那么接下来就需要我们就需要在 Merge 的过程中计算 a[i], a[j] 分别来自左右两部分的逆序对数,如下图所示,如果所有 a[i] 都小于 b[j] 的第一个数,显然是没有逆序的。只有当 a[i] > b[j] 是才会发生逆序,由于我们事先对 a[] 和 b[] 已经排好序,而所以如果发生 a[i] > b[j], 那么所有 a[ii] (ii > i) 也都满足 a[ii] > b[j], 也就是说和 b[j] 构成逆序的数有 {a[i], a[i+1]...a[end]},所以逆序数增加 end- i + 1个, 所以我们每次碰到 a[i] > b[j] 的情况, 逆序对数增加 (end - i + 1) 个即可。

至此整个算法的框架图可以如下表示:

整个算法的代码如下,时间复杂度当然是 O(nlogn)

 1 #include <iostream>
 2 #include <string>
 3 #include <vector>
 4 using namespace std;
 5
 6
 7 typedef long long llong;
 8
 9 llong MergeAndCount(vector<unsigned int> &a, int left, int mid, int right, vector<unsigned int> &temp);
10 llong MergeSortAndCount(vector<unsigned int>& a, int left, int right, vector<unsigned int>& temp);
11 llong CountInversions(vector<unsigned int>& a);
12
13 int main()
14 {
15     //freopen("data.in", "r", stdin);
16
17     int N;
18     cin >> N;
19
20     vector<unsigned int> a(N, 0);
21     for (int i = 0; i < N; ++i)
22     {
23         cin >> a[i];
24     }
25
26     llong result = CountInversions(a);
27     cout << result << endl;
28     return 0;
29 }
30
31 llong MergeAndCount(vector<unsigned int> &a, int left, int mid, int right, vector<unsigned int> &temp)
32 {
33     int i = left;
34     int j = mid + 1;
35     int k = left;
36     llong cnt = 0;
37
38     while (i <= mid && j <= right)
39     {
40         if (a[i] <= a[j])
41         {
42             temp[k++] = a[i++];
43         }
44         else
45         {
46             temp[k++] = a[j++];
47             cnt += mid - i + 1;    // key step
48         }
49     }
50
51     while (i <= mid) temp[k++] = a[i++];
52     while (j <= right) temp[k++] = a[j++];
53
54     for (i = left; i <= right; ++i)
55     {
56         a[i] = temp[i];
57     }
58     return cnt;
59 }
60
61 llong MergeSortAndCount(vector<unsigned int>& a, int left, int right, vector<unsigned int>& temp)
62 {
63     // base case
64     if (left >= right) return 0;
65
66     int mid = (left + right) / 2;
67
68     llong InversionCnt1 = MergeSortAndCount(a, left, mid, temp);
69     llong InversionCnt2 = MergeSortAndCount(a, mid+1, right, temp);
70     llong MergeInversionCnt = MergeAndCount(a, left, mid, right, temp);
71
72     return InversionCnt1 + InversionCnt2 + MergeInversionCnt;
73 }
74
75 llong CountInversions(vector<unsigned int>& a)
76 {
77     int n = a.size();
78     vector<unsigned int> temp(a.begin(), a.end());
79     llong ans = MergeSortAndCount(a, 0, n-1, temp);
80     return ans;
81 }

参考文献

[1] 《算法导论》中文第二版, P24,2-4 逆序对。

[2]  http://blog.csdn.net/qcgrxx/article/details/8005221

[3]  www.cs.umd.edu/class/fall2009/cmsc451/lectures/Lec08-inversions.pdf

时间: 2024-10-02 03:51:17

【算法32】计算数组中的逆序对的相关文章

【算法题目】数组中的逆序对

题目来源:<剑指offer>面试题36 题目:在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这两个数组中的逆序对的总数.例如数组{7,5,6,4}中,一共存在5个逆序对,分别是(7,6),(7,5),(7,4),(6,4)和(5,4). 下面把<剑指offer>的分析贴上来:                                                                             代码:

剑指offer:数组中的逆序对

题目描述在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数P.并将P对1000000007取模的结果输出. 即输出P%1000000007输入描述:题目保证输入的数组中没有的相同的数字 数据范围: 对于%50的数据,size<=10^4 对于%75的数据,size<=10^5 对于%100的数据,size<=2*10^5 示例1输入 1,2,3,4,5,6,7,0输出 7 # -*- coding: utf-8 -*

编程算法 - 数组中的逆序对 代码(C)

数组中的逆序对 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 在数组中的两个数字如果前面一个数字大于后面的数字, 则这两个数字组成一个逆序对. 输入一个数组, 求出这个数组中的逆序对的总数. 使用归并排序的方法, 辅助空间一个排序的数组, 依次比较前面较大的数字, 算出整体的逆序对数, 不用逐个比较. 时间复杂度: O(nlogn) 代码: /* * main.cpp * * Created on: 2014.6.12 * Author:

经典算法——数组中的逆序对

一.题目描述 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. 二.解题方法 利用归并排序的思想,先把数组分隔成子数组,先统计出子数组内部的逆序对的数目,然后再统计出两个相邻子数组之间的逆序对的数目.注意在合并两个已排序的子数组后,要更新数组. class Solution { public: int InversePairs(vector<int> data) { int n=data.size(); return

32.数组中的逆序对

数组中的逆序对 参与人数:2205时间限制:1秒空间限制:32768K 本题知识点: 数组 算法知识视频讲解 题目描述 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. 没想到这道题能暴力破解. class Solution { public: int InversePairs(vector<int> data) { if ( data.size() <= 0 ) return 0 ; int length =

剑指offer系列源码-数组中的逆序对

题目1348:数组中的逆序对 时间限制:1 秒内存限制:32 兆特殊判题:否提交:2133解决:500 题目描述: 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. 输入: 每个测试案例包括两行: 第一行包含一个整数n,表示数组中的元素个数.其中1 <= n <= 10^5. 第二行包含n个整数,每个数组均为int类型. 输出: 对应每个测试案例,输出一个整数,表示数组中的逆序对的总数. 样例输入: 4 7 5 6 4

剑指Offer 面试题36:数组中的逆序对及其变形(Leetcode 315. Count of Smaller Numbers After Self)题解

剑指Offer 面试题36:数组中的逆序对 题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. 例如, 在数组{7,5,6,4}中,一共存在5个逆序对,分别是(7,6),(7,5),(7,4),(6,4)和(5,4),输出5. 提交网址: http://www.nowcoder.com/practice/96bd6684e04a44eb80e6a68efc0ec6c5?tpId=13&tqId=11188 或 htt

【剑指offer】面试题51:数组中的逆序对

题目 * 面试题51:数组中的逆序对 * 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对. * 输入一个数组,求出这个数组中的逆序对的总数P. * 并将P对1000000007取模的结果输出. 即输出P%1000000007 思路 1.暴力 ,时间复杂度O(n^2) 2.归并排序的思路 :时间复杂度O(nlogn) * (1) 先将数组分成两半,递归分别计算左半边的逆序对数目leftCnt 和右半边的逆序对数目rightCnt * (2)再计算合并之后新增的逆序对

数组中的逆序对与归并中的分治思想

首先考虑归并排序: 归并排序为什么能相比普通的排序方法,将时间复杂度从O(n^2)提升至O(nlogn)? 最主要的一点是引入了两个有序数组合并的思想,真正提升效率就是在这个地方. 首先我们考虑,如果两个数组无序的话,比如: 如果使用O(n^2)的方法,在这样一个数组中,每一个数都要跟其他的数比较一下,这样复杂度就为n*n=n^2 而如果这两个数组是有序的呢? 我们用两个指针p1和p2分别指向两个数组的尾部:首先49比97小,97放入排序数组的尾部,因为它是最大的: 然后移动p2至65,再次与4