快速排序初步瞭解:
快速排序是由東尼·霍爾所發展的一種排序算法。在平均狀況下,排序 n 個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。後面將提供一種快排的優化方式可以儘量避免出現Ο(n2)的複雜度。
基本思路
1.先從數據當中找出一個數據作爲參照數
2.然後開始分區操作,大於參照數的就放到參照數右邊,小於參照數的就放到參照數的左邊.
3.然後對左右分區繼續進行該操作,知道區間裏面只有一個數據的是時候,快排完成
排序效果:
一、左右指針法
<1>思路: 給定一個數組,再給定左右區間的下標值left,right(此處我給的是左右閉區間),給定一個基準值爲arr[right],接下來就是循環和遞歸的過程。單趟過程中,先讓begin記錄該區間的第一個下標(就像一個指針指向頭一樣),end記錄最後一個值的下標,基準值爲該區間的最後一個值。循環過程中,begin記錄比基準值大的值,end記錄比基準值小的值,判斷如果begin和end還未相遇,則交換對應的值,再進行循環直至begin和end相遇。相遇後此時單趟排序完成,現在begin和end指向一個元素,該元素左側都是比基準值小的值,右側都是比基準值大的,將begin對應的值與基準值進行交換,開始遞歸左半區間和右半區間,重複上述過程。
<2>圖示單趟過程:
整體過程如下:
1 5 6 2 7 4
1 2 6 5 7 4
1 2 4 5 7 6
1 2 4 5 6 7
<3>實現代碼
void LRPoint(int* arr, int left, int right)
{
if (left >= right)
return ;
assert (arr);
int begin = left;
int end = right;
int key = arr[end ];
while (begin < end )
{
while (begin < end && arr[begin ] <= key)
begin ++;
while (begin < end && arr[end ] >= key)
end --;
if (begin < end )
swap(arr[begin ], arr[end ]);
}
swap(arr[begin ], arr[right]);
LRPoint(arr, left,begin - 1 );
LRPoint(arr, begin +1 ,right);
}
二、挖坑法
<1>思路 :大體上如同左右指針法,但是左右指針法是在begin和end符合條件的值後,將begin和end對應的值進行交換;而挖坑法則是邊找邊替換,在最後再將key填入坑中。圖中橙色元素由於此處的值賦值到了別處,可以形象的看做是一個“坑”。
<2>圖示單趟過程:
整體過程如下:
1 5 6 2 7 4
1 5 6 2 7 5
1 2 6 2 7 5
1 2 6 6 7 5
1 2 4 6 7 5
1 2 4 6 7 6
1 2 4 5 7 6
1 2 4 5 7 7
1 2 4 5 6 7
<3>實現代碼
void PitPoint(int * arr, int left, int right)
{
if (left >= right)
return;
int begin = left;
int end = right;
int key = arr[right];
while (begin < end )
{
while (begin < end && arr[begin ] <= key)
begin ++;
arr[end ] = arr[begin ];
while (begin < end && arr[end ] >= key)
end --;
arr[begin ] = arr[end ];
}
arr[end ] = key;
PitPoint(arr, left, begin - 1 );
PitPoint(arr, begin + 1 , right);
}
三、前後指針法
<1>思路 :一開始使用cur記錄left位置,prev記錄cur的前一個位置,利用cur來尋找比基準值小的值,需要注意的是隻有當arr[cur] < arr[right] 和++prev != cur 兩個條件都滿足才進行交換,此處還涉及如果arr[cur] < arr[right]不滿足則不進行prev++,當條件都滿足時進行交換,出了循環後將基準值放在prev++的位置。看着代碼自己縷一遍就會發現規律了。
<2>圖示單趟過程:
整體過程如下:
1 5 6 2 7 4
1 2 6 5 7 4
1 2 4 5 7 6
1 2 4 5 6 7
<3>實現代碼
void FBPoint(int * arr, int left , int right )
{
if (left >= right )
return;
int cur = left ;
int prev = left - 1 ;
while (cur < right )
{
if (arr[cur] < arr[right ] && ++prev != cur)
swap(arr[cur], arr[prev]);
++cur;
}
swap(arr[++prev], arr[right ]);
FBPoint(arr, left , prev - 1 );
FBPoint(arr, prev + 1 , right );
}
四、非遞歸實現
<1>思路:
用棧來模擬實現遞歸操作,每次從棧中取出對應的左右邊界進行操作;在push邊界的順序與取棧頂數據作爲左右邊界的順序要對應。
<2>實現代碼:
void NRQuickSort(int* arr, int sz)
{
assert (arr);
stack<int> s;
int left = 0 ;
int right = sz;
s.push(right);
s.push(left);
while (!s.empty())
{
left = s.top();
s.pop();
right = s.top();
s.pop();
if (left < right)
{
int begin = left;
int end = right;
int key = arr[right];
while (begin < end )
{
while (begin < end && arr[begin ] <= key)
begin ++;
while (begin < end && arr[end ] >= key)
end --;
if (begin < end )
swap(arr[begin ], arr[end ]);
}
swap(arr[begin ], arr[right]);
s.push(right);
s.push(begin + 1 );
s.push(begin - 1 );
s.push(left);
}
}
}
五、優化
<1>通過三數取中法選取key的下標:
當按照上面的代碼如果每次取key的時候取到了待排序列中最大的或者最小的. 也就是序列有序的時候,快速排序的時間複雜度爲O(N^2),我們可以通過選擇合適的key值來進行優化:也就是所謂的三數取中法(給定左右區間的下標,計算出中間下標,並返回這三個值中不大不小的那個),將該下標所對應的值和區間最右側的值進行交換即可。拿左右指針法優化舉例:
int GetMid(int* a, int left, int right)
{
int mid = left + ((right - left) >> 1 );
if (a[left] < a[mid])
{
if (a[right] < a[left])
return left;
else if (a[right] > a[mid])
return mid;
else
return right;
}
else
{
if (a[right] < a[mid])
return mid;
else if (a[right] < a[left])
return left;
else
return right;
}
}
void LRPoint(int* arr, int left, int right)
{
if (left >= right)
return ;
assert (arr);
int begin = left;
int end = right;
swap(arr[end ], arr[GetMid(arr, left, right)]);
int key = arr[end ];
while (begin < end )
{
while (begin < end && arr[begin ] <= key)
begin ++;
while (begin < end && arr[end ] >= key)
end --;
if (begin < end )
swap(arr[begin ], arr[end ]);
}
swap(arr[begin ], arr[right]);
LRPoint(arr, left,begin - 1 );
LRPoint(arr, begin +1 ,right);
}
<2>小區間優化:
思路: 由於遞歸需要建立棧幀消耗十分大,我們可以判斷如果該區間內的元素值小於n(可以取5~15,視情況而定)的時候直接採取插入排序來處理該區間內的數據,就不必再多建立那麼多棧幀,從而提高效率。具體實現就不給出了,只需要添加一個判斷條件再引入插入排序即可。