算法篇:逆序对

目录

  • 逆序对
  • 逆序对的计算
    • 1. 朴素算法
    • 2. 借助冒泡排序
    • 3. 借助插入排序
    • 4. 借助归并排序
    • 5. 借助树状数组

文章最后修改时间:2020-08-30 18:50

逆序对

  设 A A A 为一个有 n n n 个数字的有序集 ( n > 1 ) (n>1) (n>1),其中所有数字各不相同。
  如果存在正整数 i , j i, j i,j,使得 1 ≤ i < j ≤ n 1 ≤ i < j ≤ n 1i<jn 并且 A [ i ] > A [ j ] A[i] > A[j] A[i]>A[j],则 < A [ i ] , A [ j ] > \left< A[i], A[j] \right> A[i],A[j] 这个 有序对 称为 A 的一个 逆序对,也称作逆序数(简单来说,就是如果i比j小,但是 A[i]比A[j]大,那么就是一个逆序对)

  排序的过程就是消除逆序对的过程。逆序对越多,相对来说排序所需要的时间就越多。

逆序对的计算

  有序集中的逆序对数目是按照组合来计算的,对于大小为 n n n的有序集,那么有 n ( n + 1 ) 2 \frac{n(n+1)}{2} 2n(n+1)种组合。

1. 朴素算法

  枚举数组元素的所有组合, 对于 长度为 n n n 的数组, 一共有   n ( n − 1 ) 2 \ {n(n - 1)} \over 2 2 n(n1)种组合。
  (对于数组乱序,正序和逆序三种情况,逆序反而用的时间最少, 正序次之,乱序用的时间最多。。。不知道什么情况,按照代码,处理逆序对是应该多执行一个语句才对)

int countInversions(int a[], int length)
{
	int count = 0;
	for (int i = 0; i < length - 1; i++) {
		for (int j = i + 1; j < length; j++) {
			if (a[i] > a[j])
				count++;
		}
	}
	return count;
}

2. 借助冒泡排序

  冒泡排序的每一次交换就会消除一个逆序对, 交换一个相邻的逆序对,不会影响到其它的逆序对,所以可以计算冒泡排序在排序过程一共进行了多少次交换,由此得出数组的逆序对数。最普通的冒泡排序和枚举的循环次数一样多。但是枚举的过程没有元素交换,会比使用冒泡排序计算逆序对数快很多。

  • 借助冒泡排序只需要在交换两个元素的同时逆序对计数加1即可。普通的冒泡算法会试遍所有的组合,而改进的冒泡算法则可以减少这些检查,逆序对数少时就比枚举法快很多,但是逆序对数多的话就不行,因为排序有很大一部分时间花在元素交换上。
  • 缺点是计算的过程会消除逆序对,计算完成后,逆序对也被消除了,如果想保留数列,则需要复制数组,使用副本计算。

时间复杂度为   O ( n 2 ) \ O(n^2)  O(n2)

int countInversions_bubbleSort(int a[], int length)
	{
		int count = 0;
		int left = 0, right = length - 1;	//遍历的范围
		int leftBound, rightBound;			//记录乱序的边界

		while (left < right) {
			leftBound = right, rightBound = left;
			for (int i = left; i < right; i++) {
				if (a[i] > a[i + 1]) {
					count++;
					int temp = a[i];
					a[i] = a[i + 1];
					a[i + 1] = temp;

					rightBound = i;				//修改右边界
					
				}
			}

			right = rightBound;			//修改范围

			for (int i = right; i > left; i--) {
				if (a[i - 1] > a[i]) {
					count++;
					int temp = a[i - 1];
					a[i - 1] = a[i];
					a[i] = temp;

					leftBound = i;				//修改左边界
				}
			}

			left = leftBound;			//修改范围
		}
		return count;
	}

3. 借助插入排序

  插入排序的每一次元素移动可以看做相邻元素互换, 移动一次就消除一个逆序。
  所以可以用插入排序计算逆序对数,并且比用冒泡排序快。

时间复杂度为   O ( n 2 ) \ O(n^2)  O(n2)

int countInversions_insertionSort(int a[], int length)
{
	int count = 0;
	for (int i = 1; i < length; i++) {
		int key = a[i];
		int j;
		for (j = i - 1; (j >= 0) && (a[j] > key); j--) {
			a[j + 1] = a[j];
			count++;
		}
		a[j + 1] = key;
	}
	return count;
}

4. 借助归并排序

  一个升序数组,逆序对为0。数组中一个连续段之间的逆序对交换,只会影响段内的逆序对数,而不会影响段外的逆序对,段外的元素与段内的元素之间的逆序对数也不受影响
  将一个数组A从中间分成左右两部分, 分别是   A [ l e f t ] . . . A [ m i d ] 和 A [ m i d + 1 ] . . . A [ r i g h t ] \ A[ left ]...A[mid] 和 A[mid+1]...A[right]  A[left]...A[mid]A[mid+1]...A[right]
  假设两部分分别有序,在归并过程中,i, j 分别表示左右两部分归并到的元素下标, 必有i < j。如果有A[i] > A[j], 那么左边A[i]之后的元素也都会大于A[j], 所以A[j] 与 A[i] 到A[mid]的逆序对为   m i d − i + 1 \ mid - i +1  midi+1
  归并排序的两个有序数列归并,这两个有序数列在数组中是相邻的,所以交换这两个数列的元素,只会影响这两个数列元素之间的逆序对数。
  由此修改归并排序来计算逆序对, 逆序对在归并时计算。为了简便,就使用最普通的递归归并排序来说明。

  • 下面的临时数组是全局变量,是为了方便。如果想要把整个计算过程封装到函数内,可以参考递归归并排序的避免频繁开辟内存写法

时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)

int a[LEN], temp[LEN];

//合并有序数列,并在合并时计算逆序对
int mergeCountInversions(int a[], int left, int mid, int right)
{
	int count = 0;
	//归并到临时数组中
	int i = left, j = mid + 1;
	for (int k = left; k <= right; k++) {
		if ((j > right) || (i <= mid) && (a[i] <= a[j]))
			temp[k] = a[i++];
		else {
			temp[k] = a[j++];
			count += mid - i + 1;		//计算逆序对数
		}
	}

	//拷贝回数组a
	for (int i = left; i <= right; i++)
		a[i] = temp[i];

	return count;
}

//通过归并排序计算逆序对
int countInversions_mergeSort(int a[], int left, int right)
{
	if (left < right) {
		int mid = left + (right - left) / 2;
		//当前数组内的逆序对数等于两个子数组的逆序对数加上归并时计算出的逆序对数
		return countInversions_mergeSort(a, left, mid) 
			+ countInversions_mergeSort(a, mid + 1, right)
			+ mergeCountInversions(a, left, mid, right);
	}
	else
		return 0;
}

5. 借助树状数组

  树状数组C, C[x] 中保存等于x的元素的个数,初始值都为0。
  倒序扫描数列A, 每扫描到一个元素x,就在树状数组C[x]上加1,表示扫描到的x元素个数加1。然后在树状数组中查询前缀和S(x-1), 这个前缀和表示小于目前已经扫描到的元素中,比元素X小的有多少个,因为是从后面开始扫描的,所以先扫描的元素位置比元素x的位置靠后,又比元素x小,就是逆序了。树状数组的前缀和就是与元素X构成逆序的元素个数。

时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)

//add(n, x): 单点修改(a[n] += x)
//ask(n): 查询前缀和S[n]
for (int i = n - 1; i>= 0; i--) {
	add(a[i], 1);
	ans += ask(a[i] - 1);	
}

你可能感兴趣的:(算法,逆序对,算法)