在这篇博文《通过交换相邻数来完成排序所需要的最少交换次数》中,提到了逆序数的概念。它的用处还是很广泛的。本文研究一下怎么求逆序数。
按照定义来做的直观求解方法的时间复杂度是O(n^2)的,这个复杂度是不理想的。有没有O(nlogn)的方法呢?当然了,一种优化的求法引入了分治思想(Divide and Conquer)。
分治思想的两个经典应用,都在解决排序问题上,一个是归并排序算法,一个是快速快速排序算法。
对于一个问题,之前如何划分?之后如何合并?这是两个重要的事情。
1、如果在划分上做文章,目的是希望划分出的两个小较小问题有着自然的界限,可以很自然地合并。 快速排序就是这样的方法。
2、如果只是很自然的划分,那么就需要在合并上做文章,让两个已经解决的问题能够快速的合并为一体。 归并排序就是这样的方法。
正因为逆序数跟数的顺序有关,所以和排序过程息息相关。事实证明,使用归并排序和快速排序过程,都可以高效的求解出序列的逆序数。
划分思路大体是这样的。将数组切分两半,分别求各自的逆序数。求完之后,再去求合并后增加的逆序数。由于求半截数组各自的逆序数是个递归的子问题,所以主要难点是要解决如何求合并后增加的逆序数。怎么解决呢?我们可以把右半数组看作是要附加到左半数组中去的,每附加一个数都可能会产生若干个逆序。很明显,附加的数越小,新出现的逆序可能就越多。
首先来研究归并排序的过程和逆序数有什么关联。归并排序是一种特殊的归并,因为归并的对象是一个数组的左右两半。在两个半截数组进行合并的时候,情况一:如果左数组指针所指的元素比右数组所指的元素小,就先把左数组的元素放到整合数组中去。这时候,左数组指针所指的元素及其之前的元素都比右数组所指元素小,不产生新逆序;左数组指针所指元素之后的元素还没有判断到。情况二:如果左数组指针所指的元素比右数组所指的元素大,就先把右数组的元素放到整合数组中去。注意这时候,你会知道,左数组指针当前所指元素右边的所有数都比右数组指针当前所指元素要大,因此左数组当前数及其右边的每个数都因右数组指针所指的那个数增加1个逆序。由于我是两端选较小的数进行归并的,所以能确保因右边数而增加的逆序个数是正确的。
下面看程序,可以看出,完全是在归并排序基础之上添加了计数来完成的。
#include<stdio.h>
#include<stdlib.h>
int merge(int arr[], int temp[], int left, int mid, int right);
int _mergeSort(int arr[], int temp[], int left, int right);
int mergeSort(int arr[], int array_size)
{
int *temp = (int *)malloc(sizeof(int)*array_size);
return _mergeSort(arr, temp, 0, array_size-1);
}
int _mergeSort(int arr[], int temp[], int left, int right)
{
int mid, inv_count = 0;
if(right>left)
{
mid = (right+left)/2;
inv_count = _mergeSort(arr, temp, left, mid);
inv_count += _mergeSort(arr, temp, mid+1, right);
inv_count += merge(arr, temp, left, mid+1, right);
}
return inv_count;
}
int merge(int arr[], int temp[], int left, int mid, int right)
{
int i, j, k;
int inv_count = 0;
i = left;
j = mid;
k = left;
while((i<= mid-1)&&(j<=right))
{
if(arr[i] <= arr[j])
temp[k++] = arr[i++];
else
{
temp[k++] = arr[j++];
inv_count = inv_count + (mid - i);
}
}
while(i <= mid -1)
temp[k++] = arr[i++];
while(j<= right)
temp[k++] = arr[j++];
for(i=left;i<=right;i++)
arr[i] = temp[i];
return inv_count;
}
int a[100000];
int main()
{
int n;
scanf("%d",&n);
while(n>0)
{
n--;
int m;
scanf("%d", &m);
for(int i=0;i<m;i++)
scanf("%d", &a[i]);
printf("%d\n",mergeSort(a, m));
}
}
该算法的时间复杂度、空间复杂度当然就和归并排序一样了。注意一点,数列的逆序数的数量级是n^2的。如果数列元素非常多时,逆序数有可能超出int的表示范围。