歸併排序及序列逆序數

2022-05-29 22:30:24 字數 3690 閱讀 5763

歸併排序(merge sort)是又一類不同的排序方法,合併的含義就是將兩個或兩個以上的有序資料序列合併成乙個新的有序資料序列,因此它又叫歸併演算法。它的基本思想就是假設陣列a有n個元素,那麼可以看成陣列a是又n個有序的子串行組成,每個子串行的長度為1,然後再兩兩合併,得到了乙個 n/2   個長度為2或1的有序子串行,再兩兩合併,如此重複,值得得到乙個長度為n的有序資料序列為止,這種排序方法稱為2路合併排序。

例如陣列a有7個資料,分別是: 49   38   65 97   76   13   27,那麼採用歸併排序演算法的操作過程如圖所示:

初始值                 [49]   [38]   [65]   [97]   [76]   [13]   [27]

看成由長度為1的7個子序列組成

第一次合併之後         [38     49]    [65    97]   [13     76]   [27]

看成由長度為1或2的4個子序列組成

第二次合併之後         [38     49      65    97]   [13     27     76]

看成由長度為4或3的2個子序列組成

第三次合併之後         [13     27      38    49     65      76    97]

歸併排序演算法過程圖

合併演算法的核心操作就是將一維陣列中前後相鄰的兩個兩個有序序列合併成乙個有序序列。合併演算法也可以採用遞迴演算法來實現,形式上較為簡單,但實用性很差。

序列逆序數:

定義1由1,2,…,n 組成的乙個有序陣列稱為乙個n級排列。在乙個排列中如果一對數的前後位置與大小順

序相反,即大數排在小數的前面,則稱它們為乙個逆序。乙個排列中所有逆序的總和稱為該排列的逆序數。

那麼,如何求解乙個序列的逆序數呢?暴力法是問題解的最簡單的方法,下面給出核心**:

int inversion number(int *a,int n)

counter=0

for(i from 1 to n)

for(j from i+1 to n)

if(a[i]>a[j])  counter=counter+1

return counter;

直接根據定義暴力求解固然簡單,但是往往效率不高,o(n^2)的複雜度在資料規模較大的時候,是不很樂觀的。

《演算法導論》(第二版p24)有這樣乙個問題:給出乙個演算法,它能在o(n*lgn)的最壞情況執行時間,確定n個

歸併求逆序簡單原理:

歸併排序是分治的思想,具體原理自己去看書吧。利用歸併求逆序是指在對子序列 s1和s2在歸併時,若s1[i]>s2[j](逆序狀況),則逆序數加上s1.length-i,因為s1中i後面的數字對於s2[j]都是逆序的。

void merge(long a,int p,int q,intr)}

void mergesort(long a,int p,intr)}

pku 1007:

求逆序數,然後排序輸出就行了。

pku 1804, pku 2299:

是最簡單的關於逆序對的題目,題目大意是給出乙個序列,求最少移動多少步可能使它順序,規定只能相鄰移動。

相鄰移動的話,假設a b 相鄰,若ab時,交換會減少逆序,使序列更順序,所以做交換。

由上可知,所謂的移動只有一種情況,即a>b,且一次移動的結果是逆序減1。假設初始逆序是n,每次移動減1,那麼就需要n次移動時序列變為順序。所以題目轉化為直接求序列的逆序便可以了。

zju 1481:

這題和本次預選賽的f略有相似,不過要簡單得多。題意是給定序列s,然後依次將序列首項移至序列尾,這樣共有n-1次操作便回到了原序列(操作類似於迴圈左移)。問這n-1次操作和原序列,他們的逆序數最小的一次是多少?

有模板在手,直觀地可以想到是,對於這n次都求逆序數,然後輸出最小的一次就可以了,但這樣做的複雜度有o(n*nlogn),太過複雜。

如果只求初始序列的逆序數的話,只要後面的n-1次操作的逆序數能夠在o(1)的演算法下求得,就能保證總體o(nlogn)的複雜度了。事實上,對於每次操作確實可以用o(1)的演算法求得逆序數。將序列中ai移到aj的後面,就是ai做j-i次與右鄰的交換,而每次交換有三個結果:逆序+1、逆序-1、逆序不變。由於題目中說明序列中無相同項,所以逆序不變可以忽略。逆序的加減是看ai與aj間(包括aj)的數字大小關係,所以求出ai與aj間大於ai的數字個數和小於ai的數字個數然後取差,就是ai移動到aj後面所導致的逆序值變化了。

依據上面的道理,因為題目有要求ai是移動到最後乙個數,而ai又必定是頭項,所以只要計算大於ai的個數和小於ai的個數之差就行了。然後每次對於前一次的逆序數加上這個差,就是經過這次操作後的逆序數值了。

pku 2086:

這題不是求逆序對,而是知道逆序數k來製造乙個序列。要求序列最小,兩個序列比較大小是自左向右依次比較項,擁有較大項的序列大。 

其實造序列並不難,由1804可知,只要對相鄰數做調整就能做到某個逆序數了。難點是在求最小的序列。舉例 1 2 3 4 5,要求逆序1的最小序列是交換4 5,如果交換其他任意相鄰數都無法保證最小。由此可以想到,要保證序列最小,前部分序列可以不動(因為他們已經是最小的了),只改動後半部分。而我們知道n個數的最大逆序數是n*(n-1)/2,所以可以求乙個最小的p,使得 k

考慮k=7,n=6的情況,求得p=5,即前部分1不動,後面5個數字調整。4個數的最大逆序是5 4 3 2,逆序數是6,5個數是6 5 4 3 2,逆序數是10。可以猜想到,保證5中4個數的逆序不動,調整另乙個數的位置就可以增加或減少逆序數,這樣就能調整出6-10間的任意逆序。為了保證最小,我們可以取盡量小的數前移到最左的位置就行了。2前移後逆序調整4,3前移後調整了3,4調整2,5調整1,不動是調整0,可以通過這樣調整得到出6-10,所以規律就是找到需要調整的數,剩下的部分就逆序輸出。需要調整的數可以通過總逆序k-(p-1)*(p-2)/2+(n-p)求得。

pku 1455:

這是一道比較難的關於逆序數推理的題目,題目要求是n人組成乙個環,求做相鄰交換的操作最少多少次可以使每個人左右的鄰居互換,即原先左邊的到右邊去,原右邊的去左邊。容易想到的是給n個人編號,從1..n,那麼初始態是1..n然後n右邊是1,目標態是n..1,n左邊是1。

初步看上去好象結果就是求下逆序(n*(n-1)/2 ?),但是難點是此題的序列是乙個環。在環的情況下,可以減少許多次移動。先從非環的情況思考,原1-n的序列要轉化成n-1的序列,就是做n(n-1)/2次操作。因為是環,所以(k)..1,n..k+1也可以算是目標態。例如:1 2 3 4 5 6的目標可以是 6 5 4 3 2 1,也可以是 4 3 2 1 6 5。所以,問題可以轉化為求形如(k)..1,n..k+1的目標態中k取何值時,逆序數最小。

經過上面的步驟,問題已經和zju1481類似的。但其實,還是有規律可循的。對於某k,他的逆序數是左邊的逆序數+右邊的逆序數,也就是(k*(k-1)/2)+((n-k)*(n-k-1)/2) (k>=1 && k<=n)。展開一下,可以求得k等於n/2時逆序數最小為((n*n-n)/2),現在把k代入進去就可以得到解了。

要注意的是k是整數,n/2不一定是整數,所以公式還有修改的餘地,可以通用地改為(n/2)*(n-1)/2。

pku 2893:

用到了求逆序數的思想,但針對題目還有優化,可見m*n puzzle的優化。

pku 1077:

比較經典的搜尋題,但在判斷無解的情況下,逆序數幫了大忙,可見八數碼實驗報告。

歸併排序及序列逆序數

歸併排序 merge sort 是又一類不同的排序方法,合併的含義就是將兩個或兩個以上的有序資料序列合併成乙個新的有序資料序列,因此它又叫歸併演算法。它的基本思想就是假設陣列a有n個元素,那麼可以看成陣列a是又n個有序的子串行組成,每個子串行的長度為1,然後再兩兩合併,得到了乙個 n 2 個長度為2...

歸併排序 逆序數

對於數列a,將其二分地拆分為b,c 先將b,c分別排序好,再合併b,c即為總的排序,不過在合併的過程中我們可以算出逆序數哦。其原理網上很多,我這裡不再贅述,只給出實現 include include define ll long long using namespace std ll mergeso...

逆序數(歸併排序)

分而治之 分 每次從中間劃分開,直到有序為止,即乙個整數 void merge int s,int left,int right 治重新定義乙個a陣列,儲存排序完的合併陣列,void sort int s,int left,int mid,int right while i mid a k s i ...