1. 程式人生 > >歸併排序(逆序數問題)詳解

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

微信公眾號:bigsai

前言

在排序中,我們可能大部分更熟悉氣泡排序、快排之類。對歸併排序可能比較陌生。然而事實上歸併排序也是一種穩定的排序,時間複雜度為O(nlogn).

歸併排序是基於分治進行歸併的,有二路歸併和多路歸併.我們這裡只講二路歸併並且日常用的基本是二路歸併。並且歸併排序的實現方式有遞迴形式非遞迴形式。要注意其中的區分(思想上沒有大的區別,只是劃分上會有區分後面會對比)。

並且歸併排序很重要的一個應用是求序列中的逆序數個數。當然逆序數也可以用樹狀陣列完成,這裡就不介紹了。

歸併排序(merge sort)

歸併和快排都是基於分治演算法的。分治演算法其實應用挺多的,很多分治會用到遞迴,也有很多遞迴實現的演算法是分治,但事實上分治和遞迴是兩把事。分治就是分而治之。因為面對排序,如果不採用合理策略。每多一個數就會對整個整體帶來巨大的影響。而分治就是將整個問題可以分解成相似的子問題。子問題的解決要遠遠高效於整個問題的解決,並且子問題的合併並不佔用太大資源。

至於歸併的思想是這樣的:

  • 第一次:整串先進行劃分成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]將兩個串串有序存進去就行。而流程是這樣的:

在這裡插入圖片描述

非遞迴的歸併
正常歸併的程式碼實現都是藉助遞迴的。但是也有不借助遞迴的。大部分課本或者考試如果讓你列歸併的序列,那麼預設就是非遞迴的,比如一個序列9,2,6,3,8,1,7,4,10,5序列的劃分也是這樣的。

第一次結束: {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 長期奮戰輸出!