?
?
引言
?
?
一开始接触到这题还觉得挺有意思的,但后来发现其深层次的含义就是一个归并排序,只是在归并排序的过程中做了一点小动作而已,这也再次证明了很多东西都是万变不离其宗的
?
?
本文首先讲了一下归并排序的过程,用了自己比较喜欢的简洁的方式,然后对比归并排序与求逆序对之间的关系,发现需要稍微修改一下合并两个已排序数组的方法,要从两个数组的最后的数开始
?
?
最后考虑到如果求逆序对的字符是固定数目的话,比如4个,详情可以参考后面的,那么时间复杂度不用归并排序的nlogn了,如果我们可以建立辅助数组的话,那么就可以降到n的复杂度。这里又是一种空间换时间的方法
?
?
分析问题
?
?
最开始最直接的分析方法就是循环遍历每一个数,然后把每一个数都与后面所有的数进行比较,看是否逆序,但这种方法时间复杂度过高,需要n平方
?
?
所以我们需要再动动脑筋思考一下,一般这种复杂度高的问题都可以用二分法的思想去降低复杂度
?
?
我们同样可以利用二分的思想,比如7564统计逆序对的过程如下
?
?
?
?
有没有发现这个过程跟归并排序很像,或者其实就是归并排序,只是在排序的时候同时需要统计逆序对罢了
?
?
而统计逆序对的过程也有技巧可循,并不是一个一个统计,如果子数组A的当前数字a大于子数组B的当前数字b,那么a与b之前的数字,包括b,都构成逆序对
?
?
还是上面这个例子
?
?
a中p1指向的7大于p2指向的6,所以这里就有p2的长度个逆序对
?
?
解决问题
?
?
我们首先需要知道归并排序是怎样运作的
?
?
归并排序
?
?
归并排序其实主体只要四行就行了,我喜欢这种简洁的写法
?
?
static void mergeSort(int[] data, int[] temp, int start, int end) {
if (start < end) {
int mid = (start + end) / 2;
mergeSort(data, temp, start, mid);
mergeSort(data, temp, mid + 1, end);
mergeArray(data, temp, start, mid, end);
}
}
?
?
这里我们需要一个辅助数组来存储合并之后的数组,辅助数组要作为一个参数传到递归的深层次去,合并过程也用到递归的思想
?
?
也是根,左右的思想,当把左右都排序好了,就要用到合并两个已排好序的数组的思想,注意这里讨了巧的就是我只建立了一个辅助数组,这个辅助数组贯穿了始终
?
?
下面我们看看合并两个已排好序的数组的过程
?
?
private static void mergeArray(int[] data, int[] temp, int start, int mid,
int end) {
// TODO Auto-generated method stub
int leftPoint = start;
int leftEnd = mid;
int rightPoint = mid + 1;
int rightEnd = end;
int k = 0;
while (leftPoint <= leftEnd && rightPoint <= rightEnd) {
if (data[leftPoint] <= data[rightPoint]) {
temp[k++] = data[leftPoint++];
} else {
temp[k++] = data[rightPoint++];
}
}
while (leftPoint <= leftEnd) {
temp[k++] = data[leftPoint++];
}
while (rightPoint <= rightEnd) {
temp[k++] = data[rightPoint++];
}
}
?
?
最后得到的temp数组即是需要的排好序的数组
?
?
那么下面我们就可以在这个基础上加上我们需要统计的逆序数了
?
?
统计逆序对的数目
?
?
我们之前说过,统计逆序对的时候是从两个已排好序的数组的最后一个数开始比较,这样能够简单粗暴的统计出逆序对的数目
?
?
但是我们之前的归并排序算法是从两个已排好序的数组的第一个数开始比较走的
?
?
不过没关系,我们稍加修改一下代码即可,让合并两个已排好序的数组从后面开始比较也可以合并,注意在合并的时候已完成统计的工作,我们让count作为返回值
?
?
private static int mergeArray(int[] data, int[] temp, int start, int mid,
int end) {
// TODO Auto-generated method stub
int leftPoint = mid;
int leftEnd = start;
int rightPoint = end;
int rightEnd = mid+1;
int k = end;
int count=0;
while (leftPoint >= leftEnd && rightPoint >= rightEnd) {
if (data[leftPoint] > data[rightPoint]) {
temp[k--] = data[leftPoint--];
count+=rightPoint-mid;
} else {
temp[k--] = data[rightPoint--];
}
}
while (leftPoint >= leftEnd) {
temp[k--] = data[leftPoint--];
}
while (rightPoint >= rightEnd) {
temp[k--] = data[rightPoint--];
}
return count;
}
?
?
?
?
另外我们的merge函数也要修改,我们递归的时候要返回左半边和右半边的count数
?
?
static int mergeSort(int[] data, int[] temp, int start, int end) {
int left=0;
int right=0;
int mergeCount=0;
if (start < end) {
int mid = (start + end) / 2;
left=mergeSort(data, temp, start, mid);
right=mergeSort(data, temp, mid + 1, end);
mergeCount=mergeArray(data, temp, start, mid, end);
}
return left+right+mergeCount;
}
?
?
测试代码
?
?
public static void main(String[] args) {
int[] data = { 1, 3, 5, 2, 4, 6 };
int[] temp = data.clone();
System.out.println(mergeSort(data, temp, 0, data.length - 1));
for (int k : temp)
System.out.println(k);
?
?
}
?
?
?
?
如果字符有范围
?
?
比如字符只能是ACGT,相当于只能是1234,那么可以在O(n)的时间复杂度中完成,而上面的归并方法是nlogn的时间复杂度
?
?
解决问题
?
?
首先我们定义一个int数组用于存放逆序对第一个数字是A还是C还是G还是T。
?
?
我们从字符数组的末尾开始,如果这个数是A,那么说明之前如果有数,必然构成逆序对,所以我们在C的那个位上++,在G的那个位上++,在T的那个位上++
?
?
如果这个数是C,那么只能在G的那个位上++,以及T的那个位上++
?
?
如果这个数十G,那么只能在T的那个位上++
?
?
另外要注意的是count++必须在每个case里去加,而不能说最后把left 123全加起来,相当于碰到哪个数就清算一下该数组成的逆序对
?
?
其原因可以看实例说明
?
?
static int inversePairsATCG(String string) {
int count = 0;
char[] chars = string.toCharArray();
int[] left = new int[4];
for (int i = chars.length-1; i >=0; i--) {
?
?
char a = chars[i];
System.out.println(a);
switch (a) {
case ‘A‘:
left[1]++;
left[2]++;
left[3]++;
break;
case ‘C‘:
left[2]++;
left[3]++;
count += left[1];
break;
case ‘G‘:
left[3]++;
count += left[2];
break;
case ‘T‘:
count += left[3];
break;
default:
break;
}
}
return count;
?
?
}
?
?
?
?
实例说明
?
?
public class InversePairsATCG {
public static void main(String[] args) {
String string="ATAGC";
System.out.println(inversePairsATCG(string));
}
}
?
?
首先最后一个数十C,那么在G位++,T位++,由于没有碰到T和G,这时候count还等于0
?
?
下一个数是G,那么在T位++,count要把G为的数加上来,也就是现在逆序对有1个了
?
?
下一个数是A,那么在C位,G位,T位都++,加完之后C位1,G位2,T位3,由于A是最小的不用清算A位能形成的逆序对
?
?
下一个数是T,不用++,但需要清算T位能形成的逆序对,count要加上T位的3,此时count位4
?
?
下一个数是A,C位,G位,T位都++,但由于没有下一位了,不用清算,这时候count返回4
?
?
?
?