歸併排序(逆序數問題)詳解

微信公衆號: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 長期奮戰輸出!

相關文章
相關標籤/搜索