归并排序之Kendall tau距离 (三)

717 阅读2分钟

在上一篇文章中,我们介绍了基准排列,并介绍了从基准排列到最终排列的调整过程中会产生逆序对. 然后我们基于最终排列的编号序列计算逆序对.总的来说这个过程分成三步:

  1. 生成基准排列(A)的映射关系,我们用数组索引表示编号
  2. 根据映射关系生成最终排列(B)的编号序列
  3. 求B的编码序列的逆序对

我们把上述过程转变为java代码

  1. 生成基准排列(A)的映射关系
  private Map<Comparable,Integer> setupMapping(Comparable[] a){
    int len = a.length;
    Map<Comparable,Integer> map = new HashMap<>(len);
    for(int i=0;i<len;i++)
      map.put(a[i],i);
    return map;
  }
  1. 根据映射关系生成最终排列(B)的编号序列
  private int[] getCodeSequence(Comparable[] a,Comparable[] b){
    Map<Comparable,Integer> map = setupMapping(a);
    int[] codes = new int[a.length];
    for(int i=0;i<b.length;i++){
      codes[i] = map.get(b[i]);
    }
    return codes;
  }
  1. 求B的编码序列的逆序对

如何求逆序对呢?最直接的方式就是遍历编码序列的每个元素,然后跟它后面的所有元素进行比较,如果后者小于前者,那么计数器加一. 完全没问题,但是时间复杂度是O(n^2). 看到n平方的复杂度,我们应该下意识的做出生理反应,所以merge sort来了.

  //基于编码序列查找逆序对
  private int getReversedPairs(int[] codeSequence){
    int[] aux = new int[codeSequence.length];
    return divide(codeSequence,aux,0,codeSequence.length-1);
  }
  
  //利用归并排序查找
  private int divide(int[] codeSequence,int[] aux, int lo,int hi){
    if(hi <= lo) return 0;
    int inversions = 0;
    int mid = lo + (hi-lo)/2;
    inversions += divide(codeSequence,aux,lo,mid);
    inversions += divide(codeSequence,aux,mid+1,hi);
    inversions += merge(codeSequence,aux,lo,mid,hi);
    return inversions;
  }

  private int merge(int[] a,int[] aux, int lo,int mid,int hi){
    int pair = 0;
    int i=lo;
    int j=mid+1;
    for(int k=lo;k<=hi;k++)
      aux[k] = a[k];

    for(int k=lo;k<=hi;k++){
      if(i>mid) a[k] = aux[j++];
      else if(j>hi)a[k] = aux[i++];
      else if(aux[j]<(aux[i])){
        StdOut.printf("%d - %d \n",aux[j],aux[i]);
        pair += mid-i+1;
        a[k] = aux[j++];
      }
      else a[k] = aux[i++];
    }
    return pair;
  }
  
  //入口函数,接受两个排列
  public int getInversions(Comparable[] a,Comparable[] b){
    int[] codes = getCodeSequence(a,b);
    return getReversedPairs(codes);
  }

写个ut测试一把

  private static void test1(){
    Integer[] a = {0,3,1,6,2,5,4};
    Integer[] b = {1,0,3,6,4,2,5};
    P19<Integer> merge = new P19<>();
    int count = merge.getInversions(a,b);
    StdOut.println("total numbers " + count);
  }

  public static void main(String[] args) {
    test1();
  }

逆序编号输出:

0 - 2 
1 - 2 
4 - 6 
5 - 6 
total numbers 4

注意 上面输出的逆序元素的编号,如果要得到最终的逆序元素,可以通过最终排列B的映射关系找出.还有一种简单的办法,在上面的分析中我们知道,编号其实就是基准数组的索引.所以可以直接把这些逆序编号作为索引来访问A元素,那么这些元素就是逆序元素. 大家可以当作练习把上面的代码修改一下.

逆序元素输出

0-1
3-1
2-4
5-4

总结

我们从求Kentall tau距离作为切入点,引出了如何求逆序对这个问题,然后展示了如何用归并排序来解决逆序对.通过这个例子,我们会发现,这些基本算法的用途非常广,对于我们来说如何熟练掌握这些基本算法,并且合理的应用到对应的场景是关键.这条路没有捷径,只能多刷题,如果你想成为算法大牛,无他,唯手熟尔.

参考

en.wikipedia.org/wiki/Kendal… www.iteye.com/blog/shmily… algs4.cs.princeton.edu/25applicati…