天天看點

排序算法(2)

255 406 134 592 657 745 683

對該序列進行3次掃描後會發現,第3此掃描中最後一次交換的一對紀錄是l[4]和l[5]:

50 67 255 134 | 406 483 592 657 683 745 888

顯 然,第3次掃描(i=3)結束後l[5]以後的序列都已經排好序了,是以下一次掃描不必到達last(l)-i=11-4=7,即第2行的for 循環j不必到達7,隻要到達4-1=3就可以了。按照這種思路,可以來回地進行掃描,即先從頭掃到尾,再從尾掃到頭。這樣就得到雙向冒泡排序算法:

procedure bi-directional_bubble_sort(var l:list);

var

low,up,t,i:position;

begin

1 low:=first(l);up:=last(l);

2 while up>low do

3 t:=low;

4 for i:=low to up-1 do

5 if l[i]>l[i+1] then

6 swap(l[i],l[i+1]);

7 t:=i;

end;

8 up:=t;

9 for i:=up downto low+1 do

10 if l[i]< l[i-1] then

11 swap(l[i],l[i-1]);

12 t:=i;

13 low:=t;

算法利用兩個變量low和up記錄排序的區域l[low..up],用變量t 記錄最近一次交換紀錄的位置,4-7行從前向後掃描,9-12行從後向前掃描,每次掃描以後利用t所記錄的最後一次交換記錄的位置,并不斷地縮小需要排序的區間,直到該區間隻剩下一個元素。

直覺上來看,雙向冒泡法先讓重的氣泡沉到底下,然後讓輕的氣泡浮上來,然後再讓較大氣泡沉下去,讓較輕氣泡浮上來,依次反複,直到排序結束。

雙向冒泡排序法的性能分析比較複雜,目前暫缺,那位朋友知道請告訴我。

冒泡排序法和雙向冒泡排序法是原地置換排序法,也是穩定排序法,如果算法bubble_sort中第3行的比較條件l[j]>l[j+1]改為l[j]>= l[j+1],則不再是穩定排序法。

選擇排序 selection sort

選擇排序的基本思想是對待排序的記錄序列進行n-1遍的處理,第i遍處理是将l[i..n]中最小者與l[i]交換位置。這樣,經過i遍處理之後,前i個記錄的位置已經是正确的了。

選擇排序算法可實作如下。

procedure selection_sort(var l:list);

i,j,s:position;

1 for i:=first(l) to last(l)-1 do

2 s:=i;

3 for j:=i+1 to last(l) do

4 if l[j]< l[s] then

5 s:=j; //記錄l[i..n]中最小元素的位置

6 swap(l[i],l[s]); //交換l[i],l[s]

算法selection_sort中裡面的一個for循環需要進行n-i次比較,是以整個算法需要

次比較。

顯而易見,算法selection_sort中共調用了n-1次swap過程。選擇排序法是一個原地置換排序法,也是穩定排序法。

插入排序 insertion sort

插 入排序的基本思想是,經過i-1遍處理後,l[1..i-1]己排好序。第i遍處理僅将l[i]插入l[1..i-1]的适當位置,使得l[1..i]又 是排好序的序列。要達到這個目的,我們可以用順序比較的方法。首先比較l[i]和l[i-1],如果l[i-1]≤ l[i],則l[1..i]已排好序,第i遍處理就結束了;否則交換l[i]與l[i-1]的位置,繼續比較l[i-1]和l[i-2],直到找到某一個 位置j(1≤j≤i-1),使得l[j] ≤l[j+1]時為止。圖1示範了對4個元素進行插入排序的過程,共需要(a),(b),(c)三次插入。

圖1 對4個元素進行插入排序

在 下面的插入排序算法中,為了寫程式友善我們可以引入一個哨兵元素l[0],它小于l[1..n]中任一記錄。是以,我們設元素的類型 elementtype中有一個常量-∞,它比可能出現的任何記錄都小。如果常量-∞不好事先确定,就必須在決定l[i]是否向前移動之前檢查目前位置是 否為1,若目前位置已經為1時就應結束第i遍的處理。另一個辦法是在第i遍處理開始時,就将l[i]放入l[0]中,這樣也可以保證在适當的時候結束第i 遍處理。下面的算法中将對目前位置進行判斷。

插入排序算法如下:

i,j:position;

v:elementtype;

1 for i:=first(l)+1 to last(l) do

2 v:=l[i];

3 j:=i;

4 while (j<>first(l))and(l[j-1]< v) do //循環找到插入點

5 l[j]:=l[j-1]; //移動元素

6 j:=j-1;

7 l[j]:=v; //插入元素

下 面考慮算法insertion_sort的複雜性。對于确定的i,内while循環的次數為o(i),是以整個循環體内執行了∑o(i)=o(∑i),其 中i從2到n。即比較次數為o(n2)。如果輸入序列是從大到小排列的,那麼内while循環次數為i-1次,是以整個循環體執行了∑(i-1)=n(n -1)/2次。由此可知,最壞情況下,insertion_sort要比較Ω(n2)次。

如果元素類型是一個很大的紀錄,則算法第5行要消耗大量的時間,是以有必要分析移動元素的次數。經過分析可知,平均情況下第5行要執行n(n-1)/4次,分析方法與冒泡排序的分析相同。

如果移動元素要消耗大量的時間,則可以用連結清單來實作線性表,這樣insertion_sort可以改寫如下(當然前一個算法同樣也适用于連結清單,隻不過沒下面這個好,但是下面算法這個比較複雜):

注意:在下面的算法中連結清單l增加了一個哨兵單元,其中的元素為-∞,即線性表l的第一個元素是l^.next^

procedure selection_sort_ii(var l:plist);

i,j,tmp:position;

1 if l^.next=nil then exit; //如果連結清單l為空則直接退出

2 i:=l^.next; //i指向l的第一個元素,注意,l有一個哨兵元素,是以l^.next^才是l的第一個元素

3 while i^.next<>nil do

4 tmp:=i^.next; //tmp指向l[i]的下一個位置

5 j:=l;

6 while (j<>i)and(tmp^.data>=j^.next^.data) do //從前向後找到tmp的位置,tmp應該插在j後面

7 j:=j^.next;

8 if j<>i then //j=i說明不需要改變tmp的位置

9 i^.next:=tmp^.next; //将tmp從i後面摘除

10 tmp^.next:=j^.next; //在j後面插入tmp

11 j^.next:=tmp;

end

12 else i:=i^.next; //否則i指向下一個元素

上述改進算法主要是利用連結清單删除和插入元素友善的特性,對于數組則不适用。

插入排序法是一個原地置換排序法,也是一個穩定排序法。插入法雖然在最壞情況下複雜性為θ(n2),但是對于小規模輸入來說,插入排序法是一個快速的原地置換排序法。許多複雜的排序法,在規模較小的情況下,都使用插入排序法來進行排序,比如快速排序和桶排序。

下面是一個java applet制作的插入排序示範程式。

快速排序 quick sort

我們已經知道,在決策樹計算模型下,任何一個基于比較來确定兩個元素相對位置的排序算法需要Ω(nlogn)計算時間。如果我們能設計一個需要o(n1ogn)時間的排序算法,則在漸近的意義上,這個排序算法就是最優的。許多排序算法都是追求這個目标。

下面介紹快速排序算法,它在平均情況下需要o(nlogn)時間。這個算法是由c.a.r.hoare發明的。

算法的基本思想

快速排序的基本思想是基于分治政策的。對于輸入的子序列l[p..r],如果規模足夠小則直接進行排序,否則分三步處理:

分解(divide):将輸入的序列l[p..r]劃分成兩個非空子序列l[p..q]和l[q+1..r],使l[p..q]中任一進制素的值不大于l[q+1..r]中任一進制素的值。

遞歸求解(conquer):通過遞歸調用快速排序算法分别對l[p..q]和l[q+1..r]進行排序。

合并(merge):由于對分解出的兩個子序列的排序是就地進行的,是以在l[p..q]和l[q+1..r]都排好序後不需要執行任何計算l[p..r]就已排好序。

這個解決流程是符合分治法的基本步驟的。是以,快速排序法是分治法的經典應用執行個體之一。

算法的實作

算法quick_sort的實作:

注意:下面的記号l[p..r]代表線性表l從位置p到位置r的元素的集合,但是l并不一定要用數組來實作,可以是用任何一種實作方法(比如說連結清單),這裡l[p..r]隻是一種記号。

procedure quick_sort(p,r:position;var l:list);

const

e=12;

q:position;

1 if r-p< 

繼續閱讀