《數據結構與算法分析》讀書筆記——排序

穩定的

冒泡排序bubble sort — O(n^2

雞尾酒排序(Cocktail sort,雙向的冒泡排序)— O(n^2

插入排序insertion sort— O(n^2)

桶排序bucket sort— O(n); 需要 O(k) 額外空間

計數排序(counting sort) — O(n+k); 需要 O(n+k) 額外空間

並排序merge sort— O(nlog n); 需要 O(n) 額外空間

原地合併排序— O(n^2)

二叉排序樹排序Binary tree sort — O(nlog n)期望時間; O(n^2)最壞時間;需要 O(n) 額外空間

鴿巢排序(Pigeonhole sort) — O(n+k); 需要 O(k) 額外空間

基數排序radix sort— O(n·k); 需要 O(n) 額外空間

Gnome 排序— O(n^2)

圖書館排序— O(nlog n) with high probability,需要1+ε)n額外空間

不穩定的

選擇排序selection sort— O(n^2)

希爾排序shell sort— O(nlog n)如果使用最佳的現在版本???

組合排序— O(nlog n)

堆排序heapsort— O(nlog n)

平滑排序— O(nlog n)

快速排序quicksort— O(nlog n)期望時間,O(n^2) 最壞情況;對於大的、亂數列表一般相信是最快的已知排序

Introsort— O(nlog n)

Patiencesorting— O(nlog nk) 最壞情況時間,需要額外的 O(nk)空間,也需要找到最長的遞增子串行(longest increasing subsequence

不實用的排序算法

Bogo排序— O(n× n!) 期望時間,無窮的最壞情況。

Stupid sort— O(n^3);遞歸版本需要 O(n^2) 額外存儲器

珠排序Bead sort — O(n) or On),但需要特別的硬件

Pancake sorting—O(n),但需要特別的硬件

stooge sort——On^2.7)很漂亮但是很耗時

 

(直接)插入排序(穩定的)

//*********此實現方法是百度百科上的,估計他人總結的。但需要較多(線性)額外空間,數據結構與算法分析的書上的插入排序不是這種描述,而是和wiki百科上的描述一樣。

插入排序是這樣實現的:

1、首先新建一個空列表,用於保存已排序的有序數列(我們稱之爲"有序列表")。

2、從原數列中取出一個數,將其插入"有序列表"中,使其仍舊保持有序狀態。

3、重複2號步驟,直至原數列爲空。

插入排序的平均時間複雜度爲平方級的,效率不高,但是容易實現。它藉助了"逐步擴大成果"的思想,使有序列表的長度逐漸增加,直至其長度等於原列表的長度。

//*********以下是wiki百科上的描述,和書上一樣。

一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:

1.   從第一個元素開始,該元素可以認爲已經被排序

2.   取出下一個元素,在已經排序的元素序列中從後向前掃描

3.   如果該元素(已排序)大於新元素,將該元素移到下一位置

4.   重複步驟3,直到找到已排序的元素小於或者等於新元素的位置

5.   將新元素插入到該位置後

6.   重複步驟2~5

如果比較操作的代價比交換操作大的話,可以採用二分查找法來減少比較操作的數目。該算法可以認爲是插入排序的一個變種,稱爲二分查找排序

冒泡排序(穩定的)

冒泡排序是這樣實現的:

1、從列表的第一個數字到倒數第二個數字,逐個檢查:若某一位上的數字大於他的下一位,則將它與它的下一位交換。

2、重複1號步驟(每次截至到下沉點,下沉點及其後面是有序的),直至再也不能交換。

冒泡排序平均時間複雜度插入排序相同,也是平方級的,但冒泡排序是原地排序的,也就是說它不需要額外的存儲空間。

選擇排序(不穩定的)

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

選擇排序是這樣實現的:

1、設數組內存放了n個待排數字,數組下標從1開始,到n結束。

2、初始化i=1

3、從數組的第i個元素開始到第n個元素,尋找最小的元素。

4、將上一步找到的最小元素和第i位元素交換。

5i++,直到i=n1算法結束,否則回到第3

選擇排序平均時間複雜度也是O(n^2)的。

不穩定性:在一趟選擇,如果當前元素比一個元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中25的相對前後順序就被破壞了。

希爾排序(Shellsort,不穩定的)

實現:通過比較相距一定間隔的元素來工作;各趟比較所用的距離隨着算法的進行而減小,直到只比較相鄰元素的最後一趟排序爲止。也叫縮小增量排序(diminishingincrement sort)。

增量序列(increment sequence): h1, h2,~~hth1=1

希爾排序的重要性質:一個hk-排序的文件保持它的hk-排序性。保證前面的各趟排序不被後面的各趟排序打亂。

一趟hk-排序的作用就是對hk 個獨立的子數組執行一次插入排序

增量序列的一種流行但是不好的選擇是使用Shell建議的序列(希爾增量):ht=N/2 hk= hk+1/2。最壞情形運行時間爲Θ(N2)

希爾增量的問題:增量對未必互素,因此較小的增量可能影響很小。

Hibbard增量:137~~2k-1。最壞運行時間Θ(N3/2)。相鄰的增量沒有公因子。

Sedgewick增量序列:{151941109~~}

希爾排序的優劣:

不需要大量的輔助空間,和歸併排序一樣容易實現。希爾排序是基於插入排序的一種算法,提高了效率。希爾排序的時間複雜度與增量序列的選取有關,例如希爾增量時間複雜度爲O(n²),而Hibbard增量的希爾排序的時間複雜度爲O(),但是現今仍然沒有人能找出希爾排序的精確下界。希爾排序沒有快速排序算法 O(n(logn)),因此中等大小規模表現良好,對規模非常大的數據排序不是最優選擇。但是比O()複雜度的算法快得多。並且希爾排序非常容易實現,算法代碼短而簡單。此外,希爾算法在最壞的情況下和平均情況下執行效率相差不是很多,與此同時快速排序在最壞的情況下執行的效率會非常差。專家們提倡,幾乎任何排序工作在開始時都可以用希爾排序在實際使用中證明它不夠快再改成快速排序這樣更高級的排序算法. 插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位。本質上講,希爾排序算法是直接插入排序算法的一種改進,減少了其複製的次數,速度要快很多。原因是,當n值很大時數據項每一趟排序需要的個數很少,但數據項的距離很長。當n值減小時每一趟需要移動的數據增多,此時已經接近於它們排序後的最終位置。正是這兩種情況的結合才使希爾排序效率比插入排序高很多。

堆排序(Heapsort,不穩定的)

原地堆排序

假設我們已經讀入一系列數據並創建了一個堆,一個最直觀的算法就是反覆的調用del_max()函數,因爲該函數總是能夠返回堆中最大的值,然後把它從堆中刪除,從而對這一系列返回值的輸出就得到了該串行的降序排列。真正的原地堆排序使用了另外一個小技巧。堆排序的過程是:

1.  創建一個堆H[0..n-1]

2.  把堆首(最大值)和堆尾互換

3.  把堆的尺寸縮小1,並調用shift_down(0),目的是把新的數組頂端數據調整到相應位置

4.  重複步驟2,直到堆的尺寸爲1

堆排序的平均時間複雜度空間複雜度

歸併排序(Merge sort,穩定的)

歸併排序(Merge sort,臺灣譯作:合併排序)是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法Divide and Conquer)的一個非常典型的應用。時間複雜度O(NlogN),但主要問題是需要線性附加內存(歸併算法的空間複雜度爲:Θ (n),因此很難用於內存排序。

歸併操作的過程如下:

1.  申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列

2.  設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置

3.  比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置

4.  重複步驟3直到某一指針到達序列尾

5.  將另一序列剩下的所有元素直接複製到合併序列尾

快速排序(quiksort,不穩定的)

快速排序,在平均狀況下,排序 n 個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n)算法更快,因爲它的非常精煉和高度優化的內部循環(inner loop可以在大部分的架構上很有效率地被實現出來。

和歸併排序一樣,快速排序也是一種分治的遞歸算法。

步驟爲:

1.   如果S中元素個數是01,則返回。

2.   從數列S中挑出一個元素,稱爲 "樞紐元"pivot),(三數中值分割法Median-of-Three Partitioning,使用左端、右端和中心位置上的三個元素的中值作爲樞紐元)

3.   重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition操作。

4.   遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。

分割策略:

1. 將樞紐元與最後的元素交換使得樞紐元離開要被分割的數據段。i從第一個元素開始,j從倒數第二個元素開始(實際上,如書中的程序描述,i從第二個元素開始比較,因爲第一個元素在選樞紐元已經比較過)。

2. ij的左邊時,將i右移,移過那些小於樞紐元的元素,並將j左移,移過那些大於樞紐元的元素。當ij停止時,i指向一個大元素而j指向一個小元素。如果ij的左邊,那麼將這兩個元素互換。重複直到ij彼此交錯爲止。

3. 分割的最後一步是將樞紐元與i所指的元素交換。

4. 如果ij遇到等於樞紐元的關鍵字,那麼就讓ij都停止。

修改快速排序以解決選擇問題(selection problemk個最大/小值),叫做快速選擇(quickselect)。

桶排序(bucket sort,穩定的)

桶排序 (Bucket sort)或所謂的箱排序,工作的原理是將數組分到有限數量的桶子裏。每個桶子再個別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種歸納結果。當要被排序的數組內的數值是均勻分配的時候,桶排序使用線性時間(Θn))。但桶排序並不是 比較排序,他不受到 O(n log n) 下限的影響。

桶排序以下列程序進行:

1.   設置一個定量的數組當作空桶子。

2.   尋訪串行,並且把項目一個一個放到對應的桶子去。

3.   對每個不是空的桶子進行排序。

  1. 從不是空的桶子裏把項目再放回原來的串行中。

以下來自百度百科:

要求:數據的長度必須完全一樣;原理:桶排序利用函數的映射關係。

桶排序有其侷限性,適合元素值(鍵值)集合並不大的情況。

桶排序利用函數的映射關係,減少了幾乎所有的比較工作。把大量數據分割成了基本有序的數據塊()。然後只需要對桶中的少量數據做先進的比較排序即可。

N關鍵字進行桶排序的時間複雜度分爲兩個部分:

(1) 循環計算每個關鍵字的桶映射函數,這個時間複雜度O(N)

(2) 利用先進的比較排序算法對每個桶內的所有數據進行排序,其時間複雜度 ∑ O(Ni*logNi) 。其中Ni 爲第i個桶的數據量。

很顯然,第(2)部分是桶排序性能好壞的決定因素。儘量減少桶內數據的數量是提高效率的唯一辦法(因爲基於比較排序的最好平均時間複雜度只能達到O(N*logN))。因此,我們需要儘量做到下面兩點:

(1) 映射函數f(k)能夠將N個數據平均的分配到M個桶中,這樣每個桶就有[N/M]個數據量。

(2) 儘量的增大桶的數量。極限情況下每個桶只能得到一個數據,這樣就完全避開了桶內數據的比較排序操作。當然,做到這一點很不容易,數據量巨大的情況下,f(k)函數會使得桶集合的數量巨大,空間浪費嚴重。這就是一個時間代價和空間代價的權衡問題了。

對於N個待排數據,M個桶,平均每個桶[N/M]個數據的桶排序平均時間複雜度爲:

O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)

桶排序應用:海量數據

一年的全國高考考生人數爲500 萬,分數使用標準分,最低100 ,最高900 ,沒有小數,你把這500 萬元素的數組排個序。

分析:對500W數據排序,如果基於比較的先進排序,平均比較次數爲O(5000000*log5000000)≈1.112億。但是我們發現,這些數據都有特殊的條件: 100=<score<=900。那麼我們就可以考慮桶排序這樣一個投機取巧的辦法、讓其在毫秒級別就完成500萬排序。

方法:創建801(900-100)個桶。將每個考生的分數丟進f(score)=score-100的桶中。這個過程從頭到尾遍歷一遍數據只需要500W次。然後根據桶號大小依次將桶中數值輸出,即可以得到一個有序的序列。而且可以很容易的得到100分有***人,501分有***人。

實際上,桶排序對數據的條件有特殊要求,如果上面的分數不是從100-900,而是從0-2億,那麼分配2億個桶顯然是不可能的。所以桶排序有其侷限性,適合元素值集合並不大的情況。

典型

在一個文件中有10G個整數,亂序排列,要求找出中位數。內存限制爲2G。只寫出思路即可(內存限制爲2G意思是可以使用2G空間來運行程序,而不考慮本機上其他軟件內存佔用情況。)關於中位數:數據排序後,位置在最中間的數值。即將數據分成兩部分,一部分大於該數值,一部分小於該數值。中位數的位置:當樣本數爲奇數時,中位數=(N+1)/2 ; 當樣本數爲偶數時,中位數爲N/21+N/2的均值(那麼10G個數的中位數,就第5G大的數與第5G+1大的數的均值了)。

分析:既然要找中位數,很簡單就是排序的想法。那麼基於字節的桶排序是一個可行的方法。

思想:將整型的每1byte作爲一個關鍵字,也就是說一個整形可以拆成4keys,而且最高位的keys越大,整數越大。如果高位keys相同,則比較次高位的keys。整個比較過程類似於字符串的字典序

第一步:10G整數每2G讀入一次內存,然後一次遍歷這536,870,912即(1024*1024*1024*2 /4個數據。每個數據用位運算">>"取出最高8(31-24)。這8bits(0-255)最多表示256個桶,那麼可以根據8bit的值來確定丟入第幾個桶。最後把每個桶寫入一個磁盤文件中,同時在內存中統計每個桶內數據的數量NUM[256]

代價:(1) 10G數據依次讀入內存的IO代價(這個是無法避免的,CPU不能直接在磁盤上運算)(2)在內存中遍歷536,870,912個數據,這是一個O(n)的線性時間複雜度(3)256個桶寫回到256個磁盤文件空間中,這個代價是額外的,也就是多付出一倍的10G數據轉移的時間。

第二步:根據內存中256個桶內的數量NUM[256],計算中位數在第幾個桶中。很顯然,2,684,354,560個數中位數是第1,342,177,280個。假設前127個桶的數據量相加,發現少於1,342,177,280,把第128個桶數據量加上,大於1,342,177,280。說明,中位數必在磁盤的第128個桶中。而且在這個桶的第1,342,177,280-N(0-127)個數位上。N(0-127)表示前127個桶的數據量之和。然後把第128個文件中的整數讀入內存。(平均而言,每個文件的大小估計在10G/128=80M左右,當然也不一定,但是超過2G的可能性很小)。注意,變態的情況下,這個需要讀入的第128號文件仍然大於2G,那麼整個讀入仍然可以按照第一步分批來進行讀取。

代價:(1)循環計算255個桶中的數據量累加,需要O(M)的代價,其中m<255(2)讀入一個大概80M左右文件大小的IO代價。

第三步:繼續以內存中的某個桶內整數的次高8bit(他們的最高8bit是一樣的)進行桶排序(23-16)。過程和第一步相同,也是256個桶。

第四步:一直下去,直到最低字節(7-0bit)的桶排序結束。我相信這個時候完全可以在內存中使用一次快排就可以了。

整個過程的時間複雜度O(n)的線性級別上(沒有任何循環嵌套)。但主要時間消耗在第一步的第二次內存-磁盤數據交換上,即10G數據分255個文件寫回磁盤上。一般而言,如果第二步過後,內存可以容納下存在中位數的某一個文件的話,直接快排就可以了

 

基數排序(radix sort,穩定的)

基數排序Radix sort,也叫卡式排序card sort)是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。

它是這樣實現的:將所有待比較數值(正整數)統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。(每次可以是1位,也可以是多位)。

基數排序的方式可以採用LSDLeastsignificant digital)或MSDMostsignificant digital),LSD的排序方式由鍵值的最右邊開始,而MSD則相反,由鍵值的最左邊開始。基數排序的策略是多趟桶式排序。

基數排序的時間複雜度是 O(k·n),其中n是排序元素個數,k是排序趟數。這個時間複雜度不一定優於O(n·log(n))k的大小取決於數字位的選擇(比如比特位數),和待排序數據所屬數據類型的全集的大小;k決定了進行多少輪處理,而n是每輪處理的操作數目。

空間複雜度O(k·n)

計數排序(Counting sort,穩定的)

計數排序的基本思想:對於給定的輸入序列中的每一個元素x,確定該序列中值小於x的元素的個數。一旦有了這個信息,就可以將x直接存放到最終的輸出序列的正確位置上。

當輸入的元素是 n 0 k 之間的整數時,它的運行時間是Θ(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。

由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序0100之間的數字的最好的算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的算法來排序數據範圍很大的數組。

通俗地理解,例如有10個年齡不同的人,統計出有8個人的年齡比A小,那A的年齡就排在第9,用這個方法可以得到其他每個人的位置,也就排好了序。當然,年齡有重複時需要特殊處理(保證穩定性),這就是爲什麼最後要反向填充目標數組,以及將每個數字的統計減去1的原因。算法的步驟如下:

1. 找出待排序的數組中最大和最小的元素

2. 統計數組中每個值爲i的元素出現的次數,存入數組C的第i

3. 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)

4. 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1

外部排序(external sorting)

外部排序指的是大文件的排序,即待排序的記錄存儲在外存儲器上,待排序的文件無法一次裝入內存,需要在內存和外部存儲器之間進行多次數據交換,以達到排序整個文件的目的。

外部排序最常用的算法是多路歸併排序,即將原文件分解成多個能夠一次性裝入內存的部分,分別把每一部分調入內存完成排序。然後,對已經排序的子文件進行歸併排序

一般來說外排序分爲兩個步驟:預處理和合並排序。即首先根據內存的大小,將有n個記錄的磁盤文件分批讀入內存,採用有效的內存排序方法進行排序,將其預處理爲若干個有序的子文件,這些有序子文件就是初始順串,然後採用合併的方法將這些初始順串逐趟合併成一個有序文件。

多路合併:在初始順串構造之後,使用k-路合併所需要的趟數爲logkN/M)。(k-路合併方法使用2k盤磁帶)。

多相合並:(k-路合併方法使用k+1盤磁帶)。順串的個數是一個斐波那契數Fn,分配這些順串的最好方式是把他們分裂成兩個斐波那契數Fn-1Fn-2

順串的構造:多路合併中最初的順串含有M個記錄,另一種順串的構造方法是替換選擇(replacement selection)。M個記錄讀入內存並建立堆。執行一次DeleteMin,把該最小記錄寫到輸出磁帶,再從輸入磁帶讀入下一個記錄。如果它比剛剛寫出的記錄大,則放入堆中堆根位置,調整堆序;否則,將該新元素存入堆的死區(dead space),用於下一順串。繼續執行DeleteMin,直到堆大小爲零,此時順串構建完成,死區元素(等於堆大小)作爲一個新堆,重複。

替換選擇產生平均長度爲2M的順串,順串數約爲一半。替換選擇的特殊價值:輸入數據常常從排序或幾乎排序開始,此時替換選擇產生少數非常長的順串。

 

發佈了28 篇原創文章 · 獲贊 7 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章