1、算法思想
快排算法的基本思想是,選取待排序數組中的任意一個元素作爲基準值,遍歷數組中的元素。把小於基準值的元素放到基準值的左邊,大於基準值的元素放到基準值的右邊,基準值放到兩者之間,此時,基準值到達最終的位置。然後對基準值左邊的子數組和右邊的子數組採用同樣的方式處理,直到子數組區間縮小爲1時,說明數組有序。
快排的遞歸實現:
template<typename T>
void quickSort_OneWay(T* array,int left, int right)
{
if (left == right){
return;
}
index = partition(T, left, right);
quickSort_OneWay(T, left, index - 1);
quickSort_OneWay(T, index + 1, right);
}
單路快排、隨機快排、雙路快排、三路快排的主要區別在於partion部分,下面分別介紹一下這幾類partition的具體細節。
2、單路快排
在單路快排中的partion算法中,每次選取的基準值是待排序數組中最左邊的那個元素,並且是把數組劃分成大於基準值和小於等於基準值的兩部分(注意下圖,右邊這部分是>=v)
如上所示,
(1)每次我們選取待排序數組的第一個元素作爲基準值;
(2)從第1個元素開始向後遍歷,i指向當前正在遍歷的元素;
(3)[l+1,j]區間的元素小於V,[j+1,i-1]區間的元素大於等於V。
(4)每當i碰到比基準值小的元素,就將i位置的元素與j+1位置的元素進行交換,橙色區域就擴大一個長度,如下圖:
(5)最後只需將位置i的元素和位置j的元素交換即可。
單路快排partition算法:
template<typename T>
int partition(T* arrry, int left, int right){
int base = array[left];
int j = left;
for (int i = left+1; i <= right; i++){
if (array[i] < base){
swap(array, j + 1, i);
j += 1;
}
}
swap(array, left, j);
return j;
}
這段代碼存在以下兩個問題:
問題1:近乎有序:當待排序數組近乎有序時,由於默認選擇待排序數組的第一個元素作爲基準值,會導致根據基準值劃分的兩個子數組嚴重不均衡(不均衡就會導致遞歸的深度變得很深),極可能出現一個數組只有一個元素,而另一個數組n-1個元素的情況,此時遞歸調用的深度爲n,快排會退化成時間複雜度爲的排序算法。
問題2:大量重複:當待排序數組含有大量重複元素時,如果剛好選擇了重複元素作爲基準值,由於代碼會將等於基準值的元素劃分到右邊這個子數組中,使得根據基準值劃分的左右子數組嚴重不均衡,快排同樣會退化成時間複雜度爲
3、隨機快排
爲了解決問題1,基準值不再默認選擇待排序數組的第一個元素,而是從待排序數組中隨機選取一個元素作爲基準值,因此有了隨機快排,隨機快排的partion算法如下:
#include<cstdlib>
#include<ctime>
#define random(a,b) ((a)+rand()%((b)+1-(a)))
template<typename T>
int partition_random(T* arrry, int left, int right){
int base_index = random(left,right);
swap(array,left,base_index);
int base = array[left];
int j = left;
for (int i = left+1; i <= right; i++){
if (array[i] < base){
swap(array, j + 1, i);
j += 1;
}
}
swap(array, left, j);
return j;
}
4、雙路快排
爲了解決問題2,一種思路是將重複的元素均勻地分佈在兩邊的數組中,雙路快排可以實現這個想法,雙路快排的過程如下所示:
(1)i索引不斷向後掃描,當i的元素小於v時,i++;
(2)j索引不斷向前掃描,當j的元素大於v時,j- - ;
(3)當i碰到一個>= v的元素以及j碰到一個<= v的元素,交換i與j的元素,i++,j- -;
雙路快排實現代碼:
template<typename T>
int partition_TwoWay(T* arrry, int left, int right){
int base_index = random(left, right);
swap(array, left, base_index);
int base = array[left];
i = left + 1;
j = right;
while (true){
while (i <= right && array[i] < base) i++;
while (j >= left + 1 && array[j]>base)j--;
if (i > j) break;
swap(array, i,j);
i++;
j--;
}
swap(array, left, j);
return j;
}
注意: 在判定條件中,邊界情況只能是 < 或 >,而不是 <= 或 >=。
- 對於arr[i]< v和arr[j]>v的方式,第一次partition得到的分點是數組中間;
- 對於arr[i]<=v和arr[j]>=v的方式,第一次partition得到的分點是數組的倒數第二個。
因爲我們的目的就是要讓重複的均勻在兩邊的數組中,而第二種方式還是會將連續出現相等的值歸爲一方,這樣還是會導致兩顆子樹的不平衡,還是會出現導致O(n^2)的情況出現。
5、三路快排
二路排序只是提升了一下效率,當有大量重複值排序的時候,還是會淪爲O(n^2)的排序算法,之前的二路,將數組分成兩部分,小於v,大於v,兩部分是都含有等於v的,只是說盡可能的均勻分佈,當存在大量的重複可能效率還是不好,而三路快排則是多加了一部分等於v。
三路排序的過程如下所示:
(1)[left+1,lt]維持小於v的元素,[lt+1,i-1]維持等於v的元素,[gt,r]維持大於1的元素;
(2)當i小於gt時,重複(3)-(5)
(3)如果i索引元素小於v,則將位置i和位置lt+1的元素互換,i++,lt++;
(4)如果i索引元素大於v,則將位置i和位置gt-1的元素互換,gt–;
(5)如果i索引元素等於v,則i++;
(6)最後將位置left和位置lt的元素進行互換。
三路快排代碼實現:
template<typename T>
int* partition_ThreeWay(T* arrry, int left, int right){
int base_index = random(left, right);
swap(array, left, base_index);
int base = array[left];
int lt = left;
int i = left+1;
int gt = right + 1;
while (i < gt){
if (array[i] < base){
swap(array, i, lt + 1);
i++;
lt++;
}
else if(array[i] > base){
swap(array, i, gt - 1);
gt--;
}
else{
i++;
}
}
swap(array, left, lt);
int* reslut = new int[2];
reslut[0] = lt;
result[1] = gt;
return result;
}
6、快排算法分析
-
時間複雜度: 最好,最壞。
-
空間複雜度:遞歸調用本質在不斷壓棧,因此快排的空間複雜度與遞歸的深度有關,最好的情況是 ,最壞。
-
穩定性:不穩定
-
使用情況:數據規模較大和數組無序。
7、排序算法的穩定性
(1)什麼是排序算法的穩定性?
排序算法的穩定性大家應該都知道,通俗地講就是能保證排序前2個相等的數其在序列的前後位置順序和排序後它們兩個的前後位置順序相同。在簡單形式化一下,如果Ai = Aj,Ai原來在位置前,排序後Ai還是要在Aj位置前。
(2)穩定性的好處
其次,說一下穩定性的好處。排序算法如果是穩定的,那麼從一個鍵上排序,然後再從另一個鍵上排序,第一個鍵排序的結果可以爲第二個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位相同的元素其順序再高位也相同時是不會改變的。另外,如果排序算法穩定,對基於比較的排序算法而言,元素交換的次數可能會少一些(個人感覺,沒有證實)。