第五講 經典演算法之排序演算法

2021-09-02 17:41:12 字數 3725 閱讀 2022

3. 最後說幾句

顧名思義,排序演算法就是將陣列中雜亂無章的數按照從小到大或者從大到小之類的順序進行排列的**程式。一般常見的排序演算法有氣泡排序、選擇排序、快速排序、歸併排序、堆排序、計數排序、桶排序、希爾排序等。

例如:

54 12 32 55 4 10 1 3 89 12

按從小到大(公升序)的順序進行排序後將變成:

1 3 4 10 12 12 32 54 55 89

那麼如何編寫這樣的乙個程式便是本次內容的重點。

氣泡排序與選擇排序在思路上其實差不多,都是通過乙個雙重迴圈語句,每次將從陣列的當前排序部分中挑出最大或者最小的那個元素,移動至當前待排序部分的最邊位置,**如下:

//氣泡排序

void

bubblesort

(int a,

int n)

//選擇排序

void

selectionsort

(int a,

int n)

不難分析得到,這兩種排序演算法的時間複雜度都是o(n2),都是很容易理解且最基礎的排序演算法。

快速排序算是最好用且最常用的排序演算法了,它的思想有點像二分思想,首先隨便挑選乙個數作為參考數,然後遍歷一邊陣列將比參考數小的所有數排在一邊,比參考數大的所有數排在另一邊,這樣就分成了兩個未排序的陣列,又遞迴進行這樣的操作。舉例而言,對於未排序的陣列a

54 12 32 55 4 10 1 3 89 12

記參考數為a[k]=54,演算法初始情況下k=0。記搜尋變數i=0(指向首元素下標),j=9(指向末元素下標)。

首先利用搜尋變數j從後往前搜尋,找到第乙個比參考數小的元素,然後交換它們的位置。不難發現j=9, a[j]=12<54,滿足條件,故交換a[9]a[k] (k=0),同時j = j-1 = 8,陣列變為如下情形:

12 12 32 55 4 10 1 3 89 54

此刻i=0, j=8, k=9

接著利用搜尋變數i從前往後搜尋,找到第乙個比參考數大的元素,然後同樣交換它們的位置。不難發現i=3, a[i]=55>54,滿足條件,故交換a[3]a[k] (k=9),同時i = i+1 = 4,陣列變為如下情形:

12 12 32 54 4 10 1 3 89 55

此刻i=4, j=8, k=3
12 12 32 3 4 10 1 54 89 55

此刻i=4, j=6, k=7

void

quicksort

(int a,

int n,

int s,

int e)

}for

(; i}quicksort

(a,n,s,k)

,quicksort

(a,n,k+

1,e)

;}

不難分析得到,在每一層的所有遞迴呼叫所花費的時間複雜度為o(n),總共有o(logn)層,故時間複雜度為o(nlogn)

快速排序無疑是一種很巧妙的排序演算法,也是實際運用最為廣泛的排序演算法,如果有興趣深究的話,會發現快速排序其實是一種不穩定的排序演算法,極端情況上,如果待排序陣列本就有序,那麼時間複雜度將會達到o(n2),因此在實際運用中,很多封裝好的函式在演算法的實現上都運用了一點隨機化的小技巧,就是隨機化選擇參考數,即隨機初始化k,這樣的好處是,不會存在總是最壞時間複雜度的情況。

設想一種情況,如果待排序陣列全是一定範圍內的非負整數,那麼我們便可以通過計數陣列來統計每個數出現的次數,然後對計數陣列做乙個累加求和,這個累加和便是相應元素在排序後陣列中的下標,這裡不詳細說思路了,有興趣的可以上網自行了解,下面是**:

#include

#define maxn 100000

int k =

1000

;int a[k+1]

, c[maxn]

=, ranked[maxn]

;int

main()

for(

int i =

1; i < k;

++i)

c[i]

+= c[i-1]

;for

(int i = n-

1; i >=0;

--i)

ranked[

--c[a[i]]]

= a[i]

;for

(int i =

0; i < n;

++i)

printf

(" %d"

, ranked[i]);

printf

("\n");

return0;

}

這種排序演算法其實比較好理解,可以簡單理解成遍歷了一遍陣列,然後記錄了每個數出現了多少個,然後從小到大或者從大到小把這些數再放回去,這樣得到的結果陣列便是有序的。而且不難發現,這個演算法是時間複雜度是線性的,沒有迴圈的巢狀,t(n) = o(n + k),其中k表示陣列中元素的範圍大小。

這是一種典型的犧牲空間換時間的演算法,所以缺點也很明顯,一方面,k很可能會很大很大,以至於比nlogn都大,亦或者使得無法申請陣列a[k];另一方面,演算法需要以下標與不同元素對應起來然後計數,而下標只能是非負整數,所以該演算法對於元素有複數或者浮點數的情況,是無法應付的,故計數排序適用範圍較小,儘管它在理論上十分優美。

事實上,為了應對k過大,或者浮點負數等情況,之後又有人提出了桶排序以及基數排序,有興趣的可以自行了解。

除了以上提到的排序之外,還有希爾排序、堆排序、歸併排序等排序演算法。事實上,排序演算法可以分為比較排序和非比較排序,除了計數排序、基數排序屬於非比較排序演算法這一類外,其餘的都屬於比較排序演算法(桶排序根據桶內排序演算法的實現既可以屬於比較排序也可以屬於非比較排序)。比較排序演算法的時間複雜度下限理論上是o(nlogn),其中堆排序、歸併排序的時間複雜度和快速排序一樣,都是o(nlogn)級別的。

排序演算法是程式設計中乙個極其基礎且常用的演算法,無論是程式設計競賽還是面試中,排序演算法的出現率都很高,而快速排序作為排序演算法中最常用的乙個更是十分重要。雖然在比賽中直接呼叫標頭檔案中的庫函式sort(…)便可直接使用封裝好的快速排序,但作為一名學者,了解其實現原理與思想是十分重要且必要的,也是程式設計師的內功之所在。

第六講 經典演算法之遞迴與分治

遞迴與分治,顧名思義,就是既有遞迴又有分治。遞迴指函式呼叫自身,分治是指乙個大的問題被分成了幾個小問題,分而治之。總得來說,按我的理解,就是將乙個具體的問題抽象成一類問題,在解決該具體問題時,將原問題逐個的分解成更小的問題,然後分別遞迴呼叫同乙個函式來解決。回顧一下我們熟悉的斐波那契數列的計算 f ...

排序演算法 經典排序演算法之氣泡排序

氣泡排序很經典了,有人比喻過像是排序演算法中的hello world,很貼切。演算法的基本思想是每次都需要兩兩比較大小。氣泡排序演算法的過程如下 從前往後 1 比較相鄰的元素。如果第乙個比第二個大,就交換他們兩個。2 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應...

排序演算法 經典排序演算法之選擇排序

演算法的基本思想,是從無序中選擇最小的元素,然後交換。1 從陣列的第乙個元素開始,將待排序陣列分為有序和無序兩個區間。2 從無序陣列中選取最小的元素和有序陣列的最後乙個元素交換。3 重複第2步,直到無序陣列沒有元素為止。時間複雜度為o n n 空間複雜度為o 1 選擇排序是給每個位置選擇當前元素最小...