快速排序用的恰到好處時,它是迄今為止所有內排序演算法中最快的一種。快速排序演算法(qsa)本身在不斷對小陣列進行排序,是分治法的副產品,它是不穩定的。qsa應用廣泛,典型應用是unix系統呼叫庫函式例程中的qsort函式。qsa有時會由於最差時間代價的效能而在某些應用中無法採用。相對於用二叉樹進行排序來說,qsa以一種更有效的方式實現了「分治法」的思想。
二叉樹排序,是將所有的節點放到乙個二叉查詢樹中,然後再按中序方法遍歷,結果得到乙個有序陣列。快速排序演算法缺點:存一棵二叉樹要占用大量節點空間;把結點插入二叉樹中需要花很多時間。
優點:二叉查詢樹隱含地實現了分治法(divide and conquer);二叉查詢樹的根結點將樹分為兩部分,所有比它小的記錄結點都在左子樹,所有比它大的記錄結點都位於其右子樹。(對其左右子樹分別進行處理)
首先選擇乙個軸值:find pivot;
然後進行陣列分割:partition;
qsa再對軸值的左右子陣列分別進行類似操作。
選擇軸值的方法
最簡單的是使用第乙個記錄的關鍵碼。這種方法的缺點在於如果輸入的陣列是正序的或者逆序的,就會將所有節點分到軸值的一邊。較好的是隨機選取軸值,優點是減少原始輸入對排序的影響,缺點是隨機選取軸值,開銷太大。可以用選取陣列中間點的方法代替。
函式partition
由於事先並不知道有多少關鍵碼比中心點(軸值)小,我們可以用一種較為巧妙的方法分割:從陣列的兩端移動下標,必要時交換記錄,直到陣列兩端的下標相遇為止。
假如事先知道有多少個節點比軸值小,partition只需將key比軸值小的 k 個節點放到陣列的 k 個位置上,關鍵碼比軸值大的元素放到最後即可。
陣列的分割
假設輸入的陣列中有 k-1 個小於軸值的節點,於是這些節點被放在陣列的最左邊的 k-1 個位置上,而大於軸值的節點被放在陣列最右邊的n-k個位置上。
在給定分割中的節點不必被排序,只要求所有節點都放在了正確的分組位置中。而軸值的位置就是下標k。
partition程式解析
partition函式的具體**如下,
int partition(int arr, int i, int r, int npivot)
while (m < r);
swap(arr[m], arr[r]);
return m;
}
分析:快速排序中需要注意的是,呼叫 partition 函式之前,軸值已被放在陣列的最後乙個位置上。函式partition 將返回值 k,這是分割後的右半部分的起始位置。函式分割一定不能影響到陣列中 j 所指的記錄,然後軸值被放到下標為 k 的位置上,這就是它在最終排序好的陣列中的位置。m = i - 1; 保證了從陣列 arr[i] 開始處理。
arr[--r]保證了 arr[j] 沒有被處理。開始時邊界引數 m 和 r 在陣列的實際邊界之外,每一輪外層do迴圈,都將它們向陣列中間移動,直到它們相遇為止。
每層內層while迴圈,邊界下標都先移動,之後再與軸值比較。保證了每個while迴圈都有所進展,即使當最後一次do迴圈中兩個被交換的值都等於軸值時也同樣被處理。
第二個while迴圈中保持 r >= i,保證了當軸值所分割出來的左半部分的長度為 0 時,r 不至於會超出陣列的下界(下溢位)。
函式返回右半部分的第乙個下標值,因此我們可以確定遞迴呼叫quicksort的子陣列的邊界。
要做到上面這一點,必須保證在遞迴呼叫quicksort函式的過程中軸值不再移動。即使是在最差的情況下選擇了乙個不好的軸值,導致分割出了乙個空子陣列,而另乙個子陣列起碼有n-1個記錄。這種情況逆序輸出可好
前面所述,演算法中選擇最右邊的元素位置存放軸值,再把資料分成兩個部分後,再和右半部分最左邊的值交換,從而把軸值交換到中間位置。也可選擇最左邊的一元素位置作為軸值,並暫存軸值,空出此位置給高階不合適的值搬移到此處,然後高階又會空出乙個位置...如此迴圈,直到高低端指標相遇,空出的位置恰好放回軸值。
**示例如下:
#includeusing namespace std;
void swap(int &a, int &b)
int findpivot(int i, int j)
int partition(int arr, int i, int r, int npivot)
while (m < r);
swap(arr[m], arr[r]);
return m;
}void quicksort(int arr, int i, int j)
int main()
cout << "after sorting:";
quicksort(arr, 0, 9);
for (int i = 0; i < 10; i++)
cout << endl;
system("pause");
return 0;
}
後一種策略是在不斷的變動軸值的位置,直到軸值到合適的位置(高低指標相遇)。**示例如下,
#includeusing namespace std;
int partition(int arr, int low, int high)
arr[low] = arr[0];
return low;
}void quicksort(int arr, int low, int high)
}int main()
cout << "after sorting:";
quicksort(arr, 1, 8);
for (int i = 1; i <= 8; i++)
cout << endl;
system("pause");
return 0;
}
分析快速排序的函式過程
首先看對長度為k的子陣列進行 findpivot 和 partition 操作的例子。知道了 findpivot 和 partition 的時間,就可以分析快速排序的時間複雜度。
最差情況:出現在軸值未能很好的分割陣列,即乙個子陣列中無節點,而另乙個陣列中有 n-1 個節點。下一次處理的子陣列只比原陣列小1。如果上述這種情況發生在每一次分割過程中,那總時間代價為1 + 2 + 3 + ... + n = o(n*n)。這種情況僅在每個軸值都未能將陣列分割好時出現,並沒多大可能發生。所以這種最差情況並不影響快排的工作。
最佳情況:每個軸值都將陣列分成相等的兩部分,此時要分割 log2n 次,最上層原始待排序陣列中有 n 個記錄,第二層分割的陣列是2個長度各為n/2的子陣列,第三層分割的陣列是4個長度各為n/4的子陣列,以此類推,所以每層所有分割步驟之和為n。時間複雜度是o(nlog2n)。
平均情況:軸值將陣列分成長度為0和n-1、1和n-2、...,以此類推。這些分組的概率是相等的,o(nlog2n)
快速排序的改進
改變常數因子
1.尋找函式findpivot實際上,我們沒有必要儲存子陣列的拷貝,只需將子陣列的邊界存起來。如果注意調整快排的遞迴呼叫順序,堆疊的深度可以保持較小。可將函式findpivot和partition**直接內嵌到演算法中,直接編碼,減少函式呼叫。三者取中法
random
看當前子陣列中第乙個,中間乙個及最後乙個位置的陣列
2.事實上,當 n 很小時,快排很慢。
用處理小陣列較快的方法來替換快排,如插入排序和選擇排序。但有一種更有效更簡單的優化方法:
當快排的子陣列小於某個閾值時,什麼也不做。儘管那些子陣列中的數值是無序的,但此時左邊陣列key都小於右邊,所以雖然快排只是大致將排序碼移到了接近正確的位置,不過已經基本有序,這樣的待排序陣列適用插入排序,最後一步僅是呼叫一下插入排序過程將整個陣列排序。最好的組合方式是當子陣列的長度小於9時就選用插入排序。
3.縮短執行時間與遞迴呼叫有關
由於每個快排操作都要對2個子序列排序,所以無法使用一種簡單方法轉換為等價的迴圈演算法。但當需要儲存的資訊不是很多時,可以使用棧模擬遞迴呼叫,實現快排。
排序 快速排序
快速排序時實踐中最快的一直排序,平均時間是0 nlogn 最壞的情況是o n2 但是很容易將這種情況避免 空間複雜度 o n lgn 不穩定。快速排序時基於分治模式處理的,對乙個典型子陣列a p.r 排序的分治過程為三個步驟 1.分解 a p.r 被劃分為倆個 可能空 的子陣列a p q 1 和a ...
排序 快速排序
定義 在快速排序演算法中,使用了分治策略,將要排序的序列分成兩個子串行,然後遞迴地對子序列進行排序,直到整個序列排序完畢。步驟 1.在序列中選擇乙個關鍵元素作為軸 2.對序列進行重新排序,將比軸小的元素移到軸的前邊,比軸大的元素移動到軸的後面。在進行劃分之後,軸便在它最終的位置上 3.遞迴地對兩個子...
排序 快速排序
時間複雜度 快速排序每次將待排序陣列分為兩個部分 1 在理想狀況下,每一次都將待排序陣列劃分成等長兩個部分,則需要logn次劃分。2 在最壞情況下,即陣列已經有序或大致有序的情況下,每次劃分只能減少乙個元素,快速排序將不幸退化為氣泡排序,最壞情況為o n 2 快速排序的平均時間複雜度為o nlogn...