归并排序(逆序数问题)详解

微信公众号:bigsaijava

前言

在排序中,咱们可能大部分更熟悉冒泡排序、快排之类。对归并排序可能比较陌生。然而事实上归并排序也是一种稳定的排序,时间复杂度为O(nlogn).算法

归并排序是基于分治进行归并的,有二路归并和多路归并.咱们这里只讲二路归并而且平常用的基本是二路归并。而且归并排序的实现方式递归形式非递归形式。要注意其中的区分(思想上没有大的区别,只是划分上会有区分后面会对比)。编程

而且归并排序很重要的一个应用是求序列中的逆序数个数。固然逆序数也能够用树状数组完成,这里就不介绍了。数组

归并排序(merge sort)

归并和快排都是基于分治算法的。分治算法其实应用挺多的,不少分治会用到递归,也有不少递归实现的算法是分治,但事实上分治和递归是两把事。分治就是分而治之。由于面对排序,若是不采用合理策略。每多一个数就会对整个总体带来巨大的影响。而分治就是将整个问题能够分解成类似的子问题。子问题的解决要远远高效于整个问题的解决,而且子问题的合并并不占用太大资源。微信

至于归并的思想是这样的:spa

  • 第一次:整串先进行划分红1个一个单独,第一次是一一(12 34 56---)归并成若干对,分红若干2个区间.归并完(xx xx xx xx----)这样局部有序的序列。
  • 第二次就是两两归并成若干四个(1234 5678 ----)每一个小局部是有序的
  • 就这样一直到最后这个串串只剩一个,然而这个耗费的总次数logn。每次操做的时间复杂的又是O(n)。因此总共的时间复杂度为O(nlogn).

对于分治过程你可能了解了,可是这个两两merge的过程实际上是很重要的。首先咱们直到的两个序列都是有序的。其实思想也很简单,假设两个串串为 3 5 7 82 6 9 10进行归并操做。咱们须要借助一个额外的数组team[8]将两个串串有序存进去就行。而流程是这样的:3d

在这里插入图片描述在这里插入图片描述code

非递归的归并
正常归并的代码实现都是借助递归的。可是也有不借助递归的。大部分课本或者考试若是让你列归并的序列,那么默认就是非递归的,好比一个序列9,2,6,3,8,1,7,4,10,5序列的划分也是这样的。blog

第一次结束: {2,9}{3,6}{1,8}{4,7}{5,10}
第二次结束:{2,3,6,9}{1,4,7,8}{5,10}
第三次结束:{1,2,3,4,6,7,8,9}{5,10}
第四次结束:{1,2,3,4,5,6,7,8,9,10}

递归的归并
在代码实现上的归并可能大部分都是递归的归并。而且递归和分治整在一块儿真的是很容易理解。递归能够将问题分解成子问题,而这偏偏是分治所须要的手段。而递归的一来一回过程的来(分治)回(归并),一切都刚恰好。排序

而递归的思想和上面非递归确定不一样的,你能够想一想非递归:我要考虑当前几个进行归并,每一个开始的头坐标该怎么表示,还要考虑是否越界等等问题哈,写起来略麻烦

而非递归它的过程就是局部—>总体的过程,而递归是总体—>局部—>总体的过程。
而递归实现的归并的思想:

 void mergesort(int[] array, int left, int right) {
        int mid=(left+right)/2;//找到中间节点
        if(left<right)//若是不是一个节点就往下递归分治
        {
            mergesort(array, left, mid);//左区间(包过mid)进行归并排序
            mergesort(array, mid+1, right);//右区间进行归并排序
            merge(array, left,mid, right);//左右已经有序了,进行合并
        }
    }

一样是9,2,6,3,8,1,7,4,10,5这么一串序列,它的递归实现的顺序是这样的(可能部分有点问题,可是仍是有助于理解的):

在这里插入图片描述在这里插入图片描述

因此实现一个归并排序的代码为:

private static void mergesort(int[] array, int left, int right) {
        int mid=(left+right)/2;
        if(left<right)
        {
            mergesort(array, left, mid);
            mergesort(array, mid+1, right);
            merge(array, left,mid, right);
        }
    }

    private static void merge(int[] array, int l, int mid, int r) {
        int lindex=l;int rindex=mid+1;
        int team[]=new int[r-l+1];
        int teamindex=0;
        while (lindex<=mid&&rindex<=r) {//先左右比较合并
            if(array[lindex]<=array[rindex])
            {
                team[teamindex++]=array[lindex++];
            }
            else {              
                team[teamindex++]=array[rindex++];
            }
        }
        while(lindex<=mid)//当一个越界后剩余按序列添加便可
          {
              team[teamindex++]=array[lindex++];

          }
        while(rindex<=r)
          {
              team[teamindex++]=array[rindex++];
          } 
        for(int i=0;i<teamindex;i++)
        {
            array[l+i]=team[i];
        }

    }

逆序数

首先得了解什么是逆序数:

在数组中的两个数字,若是前面一个数字大于后面的数字,则这两个数字组成一个逆序对

也就是好比3 2 1.看3 ,有2 1在后面,看2 有1在后面有3个逆序数。
而好比1 2 3的逆序数为0.

在数组中,暴力确实能够求出逆序数,可是暴力之法太复杂,不可取!而有什么好的方法能解决这个问题呢? 当前序列我可能不知道有多少序列。可是咱们直到若是这个序列若是有序那么逆序数就为0.

在看个序列 abcd 3 2 1 efg编程abcd 1 2 3 efg整个序列逆序数减小3个。由于若是无论abcd仍是efg和123三个数相对位置没有变。因此咱们是能够经过某种方法肯定逆序数对的。

咱们就但愿能不能有个过程,动态改变若是逆序数发生变化可以记录下来?!好比动那么一下可以知道有没有改变的。而且这个动不能瞎动,最好是局部的,有序的动。归并排序就是很适合的一个结构。由于确定要选个小于O(n^2^)的复杂度算法,而归并排序知足,而且每次只和邻居进行归并,归并后该部分有序。

纵观归并的每一个单过程例如两个有序序列:假设序列2 3 6 8 9和序列1 4 7 10 50这个相邻区域进行归并。

在这里插入图片描述在这里插入图片描述
而纵观整个归并排序。变化过程只须要注意一些相对变化便可也就是把每一个归并的过程逆序数发生变化进行累加,那么最终有序的那个序列为止获得的就是整个序列的逆序数!
在这里插入图片描述在这里插入图片描述

至于规律,你能够发现每次归并过程当中,当且仅当右侧的数提早放到左侧,而左侧还未放置的个数就是该元素减小的逆序个数! 这个须要消化一下,而在代码实现中,须要这样进行便可!

int value;
------
-----
------
private static void merge(int[] array, int l, int mid, int r) {
        int lindex=l;int rindex=mid+1;
        int team[]=new int[r-l+1];
        int teamindex=0;
        while (lindex<=mid&&rindex<=r) {
            if(array[lindex]<=array[rindex])
            {
                team[teamindex++]=array[lindex++];
            }
            else {              
                team[teamindex++]=array[rindex++];
                value+=mid-lindex+1;//加上左侧还剩余的
            }
        }
        while(lindex<=mid)
          {
              team[teamindex++]=array[lindex++];

          }
        while(rindex<=r)
          {
              team[teamindex++]=array[rindex++];
          } 
        for(int i=0;i<teamindex;i++)
        {
            array[l+i]=team[i];
        }

    }

结语

至于归并排序和逆序数就讲这么多了!我的感受已经尽力讲了,若是有错误或者很差的地方还请各位指正。若是感受能够,还请点赞,关注一波哈。
欢迎关注公众号:bigsai 长期奋战输出!

相关文章
相关标签/搜索