快速排序(三種演算法實現和非遞迴實現)
快速排序(Quick Sort)是對氣泡排序的一種改進,基本思想是選取一個記錄作為樞軸,經過一趟排序,將整段序列分為兩個部分,其中一部分的值都小於樞軸,另一部分都大於樞軸。然後繼續對這兩部分繼續進行排序,從而使整個序列達到有序。
遞迴實現:
void QuickSort(int* array,int left,int right)
{
assert(array);
if(left >= right)//表示已經完成一個組
{
return;
}
int index = PartSort(array,left,right);//樞軸的位置
QuickSort(array,left,index - 1);
QuickSort(array,index + 1,right);
}
PartSort()函式是進行一次快排的演算法。
對於快速排序的一次排序,有很多種演算法,我這裡列舉三種。
左右指標法
- 選取一個關鍵字(key)作為樞軸,一般取整組記錄的第一個數/最後一個,這裡採用選取序列最後一個數為樞軸。
- 設定兩個變數left = 0;right = N - 1;
- 從left一直向後走,直到找到一個大於key的值,right從後至前,直至找到一個小於key的值,然後交換這兩個數。
- 重複第三步,一直往後找,直到left和right相遇,這時將key放置left的位置即可。
當left >= right時,一趟快速排序就完成了,這時將Key和array[left]的值進行一次交換。
一次快排的結果:4 1 3 0 2 5 9 8 6 7
基於這種思想,可以寫出程式碼:
int PartSort(int* array,int left,int right)
{
int& key = array[right];
while(left < right)
{
while(left < right && array[left] <= key)
{
++left ;
}
while(left < right && array[right] >= key)
{
--right;
}
swap(array[left],array[right]);
}
swap(array[left],key);
return left;
}
問題:下面的程式碼為什麼還要判斷left < right?
while(left < right && array[left] <= key)
key是整段序列最後一個,right是key前一個位置,如果array[right]這個位置的值和key相等,滿足array[left] <= key,然後++left,這時候left會走到key的下標處。
挖坑法
- 選取一個關鍵字(key)作為樞軸,一般取整組記錄的第一個數/最後一個,這裡採用選取序列最後一個數為樞軸,也是初始的坑位。
- 設定兩個變數left = 0;right = N - 1;
- 從left一直向後走,直到找到一個大於key的值,然後將該數放入坑中,坑位變成了array[left]。
- right一直向前走,直到找到一個小於key的值,然後將該數放入坑中,坑位變成了array[right]。
- 重複3和4的步驟,直到left和right相遇,然後將key放入最後一個坑位。
當left >= right時,將key放入最後一個坑,就完成了一次排序。
注意,left走的時候right是不動的,反之亦然。因為left先走,所有最後一個坑肯定在array[right]。
寫出程式碼:
int PartSort(int* array,int left,int right)
{
int key = array[right];
while(left < right)
{
while(left < right && array[left] <= key)
{
++left;
}
array[right] = array[left];
while(left < right && array[right] >= key)
{
--right;
}
array[left] = array[right];
}
array[right] = key;
return right;
}
前後指標法
- 定義變數cur指向序列的開頭,定義變數pre指向cur的前一個位置。
- 當array[cur] < key時,cur和pre同時往後走,如果array[cur]>key,cur往後走,pre留在大於key的數值前一個位置。
- 當array[cur]再次 < key時,交換array[cur]和array[pre]。
通俗一點就是,在沒找到大於key值前,pre永遠緊跟cur,遇到大的兩者之間機會拉開差距,中間差的肯定是連續的大於key的值,當再次遇到小於key的值時,交換兩個下標對應的值就好了。
帶著這種思想,看著圖示應該就能理解了。
下面是實現程式碼:
int PartSort(int* array,int left,int right)
{
if(left < right){
int key = array[right];
int cur = left;
int pre = cur - 1;
while(cur < right)
{
while(array[cur] < key && ++pre != cur)//如果找到小於key的值,並且cur和pre之間有距離時則進行交換。注意兩個條件的先後位置不能更換,可以參照評論中的解釋
{
swap(array[cur],array[pre]);
}
++cur;
}
swap(array[++pre],array[right]);
return pre;
}
return -1;
}
最後的前後指標法思路有點繞,多思考一下就好了。它最大的特點就是,左右指標法和挖坑法只能針對順序序列進行排序,如果是對一個連結串列進行排序, 就無用武之地了。
所以記住了,前後指標這個特點!
快速排序的優化
首先快排的思想是找一個樞軸,然後以樞軸為中介線,一遍都小於它,另一邊都大於它,然後對兩段區間繼續劃分,那麼樞軸的選取就很關鍵。
1、三數取中法
上面的程式碼思想都是直接拿序列的最後一個值作為樞軸,如果最後這個值剛好是整段序列最大或者最小的值,那麼這次劃分就是沒意義的。
所以當序列是正序或者逆序時,每次選到的樞軸都是沒有起到劃分的作用。快排的效率會極速退化。
所以可以每次在選樞軸時,在序列的第一,中間,最後三個值裡面選一箇中間值出來作為樞軸,保證每次劃分接近均等。
2、直接插入
由於是遞迴程式,每一次遞迴都要開闢棧幀,當遞迴到序列裡的值不是很多時,我們可以採用直接插入排序來完成,從而避免這些棧幀的消耗。
整個程式碼:
//三數取中
int GetMid(int* array,int left,int right)
{
assert(array);
int mid = left + ((right - left)>>1);
if(array[left] <= array[right])
{
if(array[mid] < array[left])
return left;
else if(array[mid] > array[right])
return right;
else
return mid;
}
else
{
if(array[mid] < array[right])
return right;
else if(array[mid] > array[left])
return left;
else
return mid;
}
}
//左右指標法
int PartSort1(int* array,int left,int right)
{
assert(array);
int mid = GetMid(array,left,right);
swap(array[mid],array[right]);
int& key = array[right];
while(left < right)
{
while(left < right && array[left] <= key)//因為有可能有相同的值,防止越界,所以加上left < right
++left;
while(left < right && array[right] >= key)
--right;
swap(array[left],array[right]);
}
swap(array[left],key);
return left;
}
//挖坑法
int PartSort2(int* array,int left,int right)
{
assert(array);
int mid = GetMid(array,left,right);
swap(array[mid],array[right]);
int key = array[right];
while(left < right)
{
while(left < right && array[left] <= key)
++left;
array[right] = array[left];
while(left < right && array[right] >= key)
--right;
array[left] = array[right];
}
array[right] = key;
return right;
}
//前後指標法
int PartSort3(int* array,int left,int right)
{
assert(array);
int mid = GetMid(array,left,right);
swap(array[mid],array[right]);
if(left < right){
int key = array[right];
int cur = left;
int pre = left - 1;
while(cur < right)
{
while(array[cur] < key && ++pre != cur)
{
swap(array[cur],array[pre]);
}
++cur;
}
swap(array[++pre],array[right]);
return pre;
}
return -1;
}
void QuickSort(int* array,int left,int right)
{
assert(array);
if(left >= right)
return;
//當序列較短時,採用直接插入
if((right - left) <= 5)
InsertSort(array,right-left+1);
int index = PartSort3(array,left,right);
QuickSort(array,left,index-1);
QuickSort(array,index+1,right);
}
int main()
{
int array[] = {4,1,7,6,9,2,8,0,3,5};
QuickSort(array,0,sizeof(array)/sizeof(array[0]) -1);//因為傳的是區間,所以這裡要 - 1;
}
非遞迴實現
遞迴的演算法主要是在劃分子區間,如果要非遞迴實現快排,只要使用一個棧來儲存區間就可以了。
一般將遞迴程式改成非遞迴首先想到的就是使用棧,因為遞迴本身就是一個壓棧的過程。
void QuickSortNotR(int* array,int left,int right)
{
assert(array);
stack<int> s;
s.push(left);
s.push(right);//後入的right,所以要先拿right
while(!s.empty)//棧不為空
{
int right = s.top();
s.pop();
int left = s.top();
s.pop();
int index = PartSort(array,left,right);
if((index - 1) > left)//左子序列
{
s.push(left);
s.push(index - 1);
}
if((index) + 1) < right)//右子序列
{
s.push(index + 1);
s.push(right);
}
}
}
上面就是關於快速排序的一些知識點,如果哪裡有錯誤,還望指出。