除了上一次介紹的希爾排序,堆排序,快速排序,也是經常用到的排序方式,其中快速排序可以說是一種性能十分優秀的排序。
1 堆排序:
針對堆排序,對於其代碼實現不作闡述,因為太過於復雜,主要是堆處理的復雜。
在此,對其算法的核心步驟作一定描述:
堆排序,本質上,分為兩步:
1 建立堆:
1 廣度優先初始化堆
2大數上浮,使滿足堆屬性
2 堆排序:
1 取走樹根的值
2 將樹根的值與 最下層 最右側(注意這個描述所代表的位置)的葉子節點交換
3 去掉經過步驟2交換后的最下層,最右側的葉子節點(也就是原根節點)
4 將最大數上浮至根
5 重復1、2、3、4
堆排序使用了堆這種數據結構,對數據進行排序,盡管上述過程看起來並不復雜,但在實際寫程序的時候,也並不是一個簡單的過程。
2 快速排序:
快速排序是一種十分優秀的排序方式,甚至說是一種最優的排序都不為過。
快速排序采用分治遞歸的思想完成了對數組的排序,快速排序的核心思想是:給基准數據(也就是邊界)找其正確索引位置。
什么叫:給基准數據找其正確索引位置呢? 舉個例子:
對於原始數據: 8,2,6,我們知道其從小到大的順序是:2,6,8;那么對於數據8而言,正確的位置是:3,;對於數據2而言,正確的位置是1。那么給基准數據找其正確索引的位置被理解成:假如現在選定基准數據6,我們要能把數據6 放到位置 2 ,就完成了 給基准數據(也就是邊界)找其正確索引位置。那么如何做到這一點呢?也就是:我們怎么樣才知道一個數據被放到了正確的位置呢? 其實核心在於:這個數左邊的數據都比這個數小,這個數右邊的數據都比這個數大,那么這個數就被放到了一個正確位置。這是一種十分朴素但是又十分重要的思想。而快速排序證實基於這種思想完成了快速的排序。下面我們看看,快速排序如何做到這一點:
假如對於一組數據[a1,a2,....,an]假設數據的個數為n,大小順序任意。我們先選擇這組數據 (1+n)/2位置的數據作為基准數據,也就是中間位置的數據,將這個數左邊作為一個子數組v1,這個數右邊作為另外一個子數組v2。也就是整個大的待排序的數組被分成了2個小數組。這個時候,我們需要清楚:我們到底要干什么?
沒錯,我們需要將這個位置處於 (1+n)/2的數據放在正確的位置。這個時候,我們就要用到上面的思想了:這個數左邊的數據都比這個數小,這個數右邊的數據都比這個數大,那么這個數就被放到了一個正確位置。更具體而言:
這個數左邊的子數組v1的每個數據都與基准數據進行比較,也就是與位置(1+n)/2的數據進行比較,如果比基准數據大,就把這個這個數據交換到右邊數組對稱的位置去。同樣的,對於右側數組,也采用相同的操作。最終,使得基准數據的左側數據都比 基准數據(邊界)小,使得基准數據的右側側數據都比 基准數據(邊界)大。
但是這僅僅將一個數據放在了正確的位置,那么剩下的數據該怎么辦呢?,同樣的,對於左側的子數組v1,右側的子數組v2;再次選取各自的中間位置的數據作為邊界,將v1分為v1-1,v1-2,將v2分解成v2-1,v2-2。這個時候對於從v1中選取的基准數據,從v2中選取的基准數據,將他們放在正確的位置.....一直這樣做下去,最后整個大的數組被分解成n個小數組,即每個數組只有一個元素,排序結束,得到正確的排序結果。
這就是快速排序,怎么樣,十分精彩吧,這種分治遞歸,為數據找正確位置的思想簡直爆炸精彩好嘛23333.。
說了這么多,先上個代碼吧:
1 //快速排序 2 void FastSort(vector<int> & sort_a) 3 { 4 int a_size; 5 a_size = sort_a.size();//得到數組大小 6 if (a_size < 2) 7 return; 8 int MaxIndex = 0; 9 for (int count = 1; count < a_size;count++) 10 { 11 if (sort_a[count]>sort_a[MaxIndex]) 12 { 13 MaxIndex = count; 14 } 15 } 16 swap(sort_a[a_size - 1], sort_a[MaxIndex]);//這里的swap用的是vector中的函數 17 FastSort(sort_a, 0, a_size-2); 18 } 19 ///快速排序 20 void FastSort(vector<int> & sort_array, int first, int last) 21 { 22 int lower = first + 1; 23 int upper = last; 24 swap(sort_array[first], sort_array[(first + last) / 2]); 25 int bound = sort_array[first];//將中間位置元素作為bound,並將其放在最開始的位置 26 while (lower <= upper) 27 { 28 while (sort_array[lower] < bound) 29 lower++; 30 while (sort_array[upper] > bound) 31 upper--; 32 if (lower < upper) 33 swap(sort_array[lower++], sort_array[upper--]); 34 else 35 lower++; 36 } 37 swap(sort_array[first], sort_array[upper]); 38 if (first < upper-1) 39 FastSort(sort_array, first, upper - 1); 40 if (last > lower) 41 FastSort(sort_array, lower, last); 42 /*if (last > upper+1) 43 FastSort(sort_array, upper + 1, last);*/ 44 }
上述過程完美的呈現了快速排序的優雅。在此對這個程序不再作闡述,有興趣參考《c++數據結構與算法》這本書。只是這個程序中,有一個讓我不安的地方:將待排序數組的最大數據放到了數據最右邊,最大數據不參與排序。。。原因在於:
如果我們不將最大數放在最后,恰巧最大數據作基准數據了,這個時候,會發生什么???
這個時候,28,29行的代碼會一直執行,一直執行到lower =n+1,顯然這個時候,28行的while循環sort_array[lower] 對數組的訪問已經越界了!!!!!。造成程序非正常中斷。
我們再來關注一下:上文中有一句:我們先選擇這組數據 (1+n)/2位置的數據作為基准數據。可能,每個人都會問,為什么選這個地方的數據,這樣做一定好嗎,什么樣才是好的呢?
先說結論:快速排序最復雜的地方在於邊界位置的選取,我們選取的這個地方的邊界不一定是最好的。那么什么樣的叫好的邊界呢?
我們知道:快速排序的過程,數組一生二,二生四,四生八......如果我們選的邊界,使得每次划分的子數組彼此間數據個數大致相等,那么這種邊界選取就是一種優秀的方案。遺憾的是,並不存在一個理論的划分方式,這和數據的序列有關。這就導致了快速排序並不是一種穩定的算法(盡管如此,仍然很優秀)
那為何我們通常會選擇中間位置的數據作為邊界呢?那是因為:在實際上,數組的排序基本排序好了(注意是“基本”),這樣選擇中間的數據,其左右數據交換后,個數大致也是相等的)。
最后,再給上一個我認為講解快排比較好的網站:http://www.sohu.com/a/246785807_684445 (講的十分清楚)
歸並排序
歸並排序仍然采用了分治遞歸的做法,其核心思想是: 遞歸的將 排好序 的子數組 組合成 排好序的 大數組。
也就是:我們將一個數組拆成若干組數組(如果是拆成2組就是2路歸並),子數組再拆成2組,子數組再拆成兩組,最后每個組只有一個元素為止。然后反過來2個排序、4個排序、8個排序....那個排序.這就是歸並的過程。
更為精彩的講解,可以見簡書上這位大神的描述:https://www.jianshu.com/p/33cffa1ce613’
需要注意的是:歸並排序存在一個最大的問題:需要產生一個與原數組同樣大小的數組空間來緩存中間結果!!!,造成空間復雜度為O(n)。
當然,我們考慮采用 鏈表,部分插入排序,這些方式來提高歸並排序的效率。
再說快速排序算法與歸並排序:
快排和歸並排序都體現了一種思想:分治,但是有所不同。
歸並是嚴格的1分為2,2分為4......這樣的對半分的思想;
而快速排序並沒有采用這種對半分的思想,而是選擇一個邊界,小於等於這個邊界的數放在其前面,大於這個數的數放到其后面。最后該數就放在了正確的位置。 那么對於快排而言:怎么選這個邊界就成為一個嚴重影響排序效率的事情。
因此,對於歸並排序,其划分的過程實質是一個平衡二叉樹的過程,那么對於快速排序而言,是否是平衡樹,則要打上大大的???
現在考慮一種情況:對於一個幾乎排好序的數組,如果我們每次選擇最左側的數作為邊界,會發生什么???對,樹結構直接退化成鏈結構,logn層結構直接退化成 n層結構,這樣算法的復雜度直接由nlogn退化成n^2。這也表明了排序算法的不穩定性。那么我們應該如何改變這一點呢?
那就是我們采用隨機數的方式,每次隨機從序列中選擇一個數作為邊界條件。
那么也許看似不平衡性問題被解決了,但是還沒有,假如現在有10000個數,但是這些數據都是在[0,10]之前的整數。那么會面臨什么樣的問題?對,會存在大量等於邊界的數,這樣的話如果把等於歸於小於等於,那么必然“左重右輕”,如果歸於大於等於,則會“左輕右重”。即,此時仍然面臨着不平衡的問題。則解決這種問題的思路有兩種:將數據划分為小於等於和大於等於,也就是使得等於邊界的數在兩側都有,另一種比較明顯的思想是:分三路:小於 ,等於,大於。
最后,想表明:快速排序確實是一種不穩定的排序算法,而其不穩定性是歸因為其分治策略,因此領悟一個算法的本質對於掌握衡量,甚至算法設計有着至關重要的作用。