既然這是一篇主題思想爲優化快排的文章,天然就不討論關於快排的一些定義和基礎性的問題,只說快排應該怎麼優化。html
首先快排的平均時間複雜度優於不少排序,可是時間複雜度也有和他同樣的,也就是堆排序,但爲何實際應用中快排要好於堆排呢?算法
緣由主要有三個:數組
答案是確定的,能夠說在多數狀況下,基本的快排速度是優於其餘排序的,可是凡事都有侷限性,快速排序對於數據不平衡的數組,重複數組,小數組等狀況速度是比較不理想的,這個時候天然就須要優化,來儘量的減少侷限性。緩存
咱們知道基本的快速排序選取第一個或最後一個元素做爲樞軸,可是,這一直很很差的處理方法。對於這個問題通常有兩種處理方法:隨機選取樞軸、三數取中(median-of-three)。網絡
選三個數(或更多,通常爲左中右)做爲樣本,取其中位數做爲樞軸點,這樣劃分能夠儘量的使樞軸在中間,使兩邊數據更均衡。ide
/*函數做用:取待排序序列中low、mid、high三個位置上數據,選取他們中間的那個數據做爲樞軸*/
int PickMiddle(int arr[],int low,int high) {
int mid = (high+low) / 2;//計算數組中間的元素的下標
//使用三數取中法選擇樞軸
if (arr[low] > arr[high])
sawp(arr,low,high); //目標: arr[low] <= arr[high]
if (arr[mid] > arr[high])
sawp(arr,mid,high); //目標: arr[mid] <= arr[high]
//以上兩步保證把最大的移到最右端
if (arr[mid] > arr[low])
sawp(arr,mid,low); //目標: arr[low] >= arr[mid]
//此時,arr[mid] <= arr[low] <= arr[high]
return arr[low];
//low的位置上保存這三個位置中間的值
//分割時能夠直接使用low位置的元素做爲樞軸,而不用改變分割函數了
}
複製代碼
對於很小和部分有序的數組,快排不如插排好。當待排序序列分割到必定長度後,繼續分割的效率比插入排序要差,此時可使用插排而不是快排。函數
if(high-low < 7)
{
InsertSort(L); //進行插入排序
return;
}//else進行正常的快排
複製代碼
快速排序對於元素重複率特別高的數組,效率顯得很是低下。舉個例子,假如在排序過程當中一個子數組已所有爲重複元素,則對於此數組排序就應該中止了,但快排算法依然會將其切分爲更小的數組。性能
一個簡單的改進想法就是將數組分爲三部分:小於當前切分元素的部分,等於當前切分元素的部分,大於當前切分元素的部分。用一張圖說明就是這樣:學習
int QSort(int arr[],int low,int high) {
if(low < high)
{
int lt = low; //low爲樞軸位置
int gt = high;
int i=low+1; //low位置的元素爲樞軸元素,因此用於比較的元素從low+1開始
int temp = arr[low]; //將樞軸的元素儲存到temp中
while(i <= gt)
{
if(arr[i] < temp) //小於樞軸元素的放在lt左邊
sawp(arr,lt++,i++); //即交換lt和i位置的元素,此時樞紐位置(lt)右移一位,i也所以右移
else if(arr[i] > temp) //大於樞軸元素的放在gt右邊
sawp(arr,i,gt--); //交換i和gt位置的元素,gt須要左移,i因爲變爲gt位置元素,因此不須要移動
else //相等時,無需交換,只需把i右移一位
i++;
}
//lt-gt的元素已經排定,只需對it左邊和gt右邊的元素進行遞歸求解
QSort(arr,low,lt-1);
QSort(arr,gt+1,high);
}
}
複製代碼
咱們知道快排是一個遞歸算法,而遞歸的問題是若是遞歸太深容易棧溢出。因此針對快排的優化,還有一個角度是對遞歸的優化。網上不少文章中都提到一個叫作尾遞歸優化的東西。優化
爲了更好的理解,得先了解了一下什麼叫尾遞歸。
若是一個函數中全部遞歸形式的調用都出如今函數的末尾,咱們稱這個遞歸函數是尾遞歸的。當遞歸調用是整個函數體中最後執行的語句且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸。尾遞歸函數的特色是在迴歸過程當中不用作任何操做,這個特性很重要,由於大多數現代的編譯器會利用這種特色自動生成優化的代碼。
具體用法看這篇文章吧,我就不復述了。 尾調用優化 - 阮一峯的網絡日誌
瞭解完尾遞歸後,再來看看網上流行的快排尾遞歸優化方法。
關鍵代碼以下
void QSort(int arr[],int low,int high) {
int pivot; //樞軸
while(low<high)//直到把右半邊數組劃分紅最多隻有一個元素爲止,就排完了!
{
pivot=Partition(arr,low,high);
QSort(arr,low,pivot-1); //對低子表遞歸排序
low=pivot+1;
}
}
複製代碼
首先上面代碼中遞歸調用並非最後一步,甚至最後一行都不是,和咱們上面看到的尾遞歸的定義不太同樣,我查閱了網上不少資料,其中算法導論中7-4章也提到了這個問題。
算法導論中的題目:
中文版本:
原書中的問題是這樣的:
The QUICKSORT algorithm of Section 7.1 contains two recursive calls to itself. After the call to PARTITION, the left subarray is recursively sorted and then the right subarray is recursively sorted. The second recursive call in QUICKSORT is not really necessary; it can be avoided by using an iterative control structure. This technique, called tail recursion, is provided automatically by good compilers. Consider the following version of quicksort, which simulates tail recursion.
這個問題也提到尾遞歸技術,但在後續的描述中更像是描述尾遞歸的思想,which simulates tail recursion這是書中的原話,意思也很明顯,就是模擬尾遞歸技術,而非真正的尾遞歸。
那麼用這樣的方式到底能不能下降遞歸深度呢?
答案是能,不妨來畫一下這兩種方法的遞歸樹:
首先設置模擬數組爲(1,2,3,4,5,6,7,8,9,10,11),樞軸選取採用三數取中方式
普通雙遞歸的遞歸樹爲:
而若是採用模擬尾遞歸的方法,因爲去掉了高子表的遞歸,而採用直接調用快排處理函數;不難發現其實本質是至關於把右節點擦掉,把右節點的子節點直接鏈接到當前節點。這樣右邊的葉深度,也就是最大棧深度就降低了。這個是不須要依賴編譯器優化的。
不過這種優化我以爲沒什麼必要,畢竟這只是把遞歸算法轉化爲了迭代算法,而全部的遞歸均可以轉化爲迭代,爲什麼又不把另外一個遞歸寫成迭代呢;更況且通過優化後快排的平均遞歸深度基本爲,雖然可能爲n,但這種狀況在使用了各類優化後,幾乎不可能出現了,因此我以爲優化算法,最重要的應該是結合具體狀況對算法自己優化,而不是粗暴的把遞歸改爲非遞歸。
參考文獻: