快速排序的優化
三種快速排序以及快速排序的優化:
一:快速排序的基本思想
快排使用分治的思想: | 通過一趟排序將待排序序列分割成兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小。之後分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。 |
二:快速排序的三個步驟
1.選擇基準 | 在待排序列中,按照某種方式挑出一個元素,作為“基準”(pivot) |
2.分割操作 | 以該基準在序列中的位置,把序列分成兩個子序列,此時基準左邊的比基準小,基準右邊的都比基準大 |
3.遞迴操作 |
遞迴地對兩個序列進行快速排序,直到序列為空或者只有一個元素。 |
三:選擇基準的方式
對於分治演算法,當每次劃分時,演算法若能都劃分成兩個子序列時,那麼分治演算法效率會達到最大。也就是說,基準的選擇將會決定演算法的效率。選擇基準的方式決定了兩個分割後子序列的長度,進而對整個演算法的效率產生決定性的影響。
最理想的方法:選擇基準恰好能把待排序序列分成兩個等長的子序列。
下面我們將介紹3種選擇基準的方法:
方法(1):固定位置(基礎)
①思想:取序列的第一個或最後一個元素作為基準。基本快速排序
int SelectPivot(int arr[] , int low , int high) { return arr[low];//選擇序列的第一個元素作為基準。 //return arr[high]; } //注意:基本快排選取第一個或者最後一個元素作為基準。
②測試資料:
③測試資料分析:
如果輸入的序列是隨機的,處理時間可以接受。
但如果陣列已經有序了,此時的分割方法是一個非常不好的分割,因為每次分割只能使待排序序列減一,此時為最壞的情況,導致快速排序淪為氣泡排序。時間複雜度為O(n^2)。
因此,使用第一個元素作為樞紐元素是非常糟糕的,為了避免這種情況,就引入了下面兩個獲取基準的方法。
方法(2):隨機選取基準
①思想:隨機取帶排序元素中的元素作為基準。
/*隨機選擇樞軸的位置,區間在low和high之間*/ int SelectPivotRandom(int arr[],int low , int high) { srand((unsigned)time(NULL)); int pivotPos = rand()%(high - low) + low; //把樞軸位置的元素和low位置的元素互換,此時可以和普通的快排一樣呼叫換分函式 swap(arr[pivotPos],arr[low]); return arr[low]; }
②測試資料:
③:測試資料分析
這是一種相對安全的策略。由於樞軸的位置是隨機的,那麼產生的分割也不會總是出現劣質分割。
但是在整個陣列數字全相等時,認識最壞的情況,時間複雜度為O(n^2)。實際上:隨機化快排得到的理論上最壞的情況可能性僅僅為
1/(2^n)。所以隨機化快速排序可以對於絕大多數輸入資料達到O(nlogn)的期望時間複雜度。
一位前輩做出了一個精闢的總結:“隨機化快速排序可以滿足一個人一輩子的人品需求。”
方法(3):三數取中(median-of-three)(優化有序的資料)
引入的原因:雖然隨機選取樞軸時,減少出現不好分割的機率,但是最壞的情況下還是O(n^2),要緩解這種情況,就引入了三數取中的選取樞軸。
①:具體思想
對待排序序列中low,mid,high三個位置上資料進行排序,取他們中間的那個資料作為樞軸,並且用0下標元素儲存樞軸。
即:三數取中,並且0下標元素儲存樞軸。
/*函式作用:取待排序序列中low,mid,high三個位置上資料,選取他們中間的那個資料作為樞軸*/
int SelectPivotMedianOfThree( int arr[],int low,int high)
{
int mid = low + ((low + high)>>1);//計算陣列中間元素的下標。
if(arr[mid] > arr[high])//目標:arr[mid] <= arr[high]
{
swap(arr[mid] , arr[high]);
}
if(arr[low] > arr[high])
{
swap(arr[low] , arr[high]);
}
if(arr[mid] > arr[low])
{
swap(arr[mid] , arr[low]);
}
//此時,arr[mid]<=arr[low]<=arr[high]
return arr[low];
//low位置上儲存這三個位置中間的值,分割時可以直接使用low位置的元素作為樞軸,而不改用分割函數了
}
②:測試資料
③:測試資料分析:使用三數取中選擇樞軸的優勢還是很明顯的,但是還是處理不了重複陣列
優化:
優化1:對於很小和部分有序的陣列。快排不如插入排序好。當待排序序列的長度分割到一定大小之後,繼續分割的效率比插入排序要差。此時可以使用插排而不是快排。
截至範圍:待排序序列長度N = 10.雖然在2 ~ 20之間任意截至範圍都有可能產生類似大的結果,這種做法也避免了一些有害的退化情形。摘自《資料結構與演算法分析》Mark Allen Weiness著。
if(high - low + 1 < 10)
{
insertSort(arr,low,high);
return ;
}
//else正常執行快排
②:測試資料
③:測試資料分析
針對隨即陣列,使用三數取中選擇樞軸+插排,效率還是可以提高一點。
但是針對已排序陣列,是沒有作用的。因為待排序序列是已經有序的,那麼每次劃分只能使得待排序序列減一。此時插入排序是起不了任何作用的,所以這裡看不到任何的時間減少。
同時該方法對於重複陣列還是沒有任何的辦法。
優化2:再一次分割結束後,可以把與key相等的元素聚在一起。繼續下次分割時,不再用對於key相等元素分割。
①:具體的處理過程
第一步: | 再劃分過程中,把與key相等元素放入陣列的兩端。 |
第二步: | 劃分結束後,把與key相等的元素移到樞軸周圍。 |
舉例:
待排序序列:1 4 6 7 6 6 7 6 8 6 |
三數取中選取樞軸:下標為4 的數 6 |
轉化後待分割序列:6 4 6 7 1 6 7 6 8 6 樞軸key: 6 |
第一步:再劃分過程中,把與key相同的元素放入陣列的兩端 結果為: 6 4 1 6(樞軸) 7 8 7 6 6 6 。此時與6相等的元素全部放入兩端 |
第二步:劃分結束後,把與key相等的元素移到樞軸周圍。結果:1 4 6 6(樞軸) 6 6 6 7 8 7.此時與6相等的元素全移到樞軸周圍了。 |
之後,在1 4 和 7 8 7兩個子序列中進行快排。 |
void QSort(int arr[],int low,int high)
{
int first = low;
int lase = high;
int left = low;
int right = high;
int leftlen = 0;
int rightlen = 0;
if(high - low +1 < 10)
{
InsertSort(arr,low,high);
return;
}
//一次分割;
int key = SelectPivotMedianOfThree(arr,low,high);//使用三數取中選擇樞軸
while(low < high)
{
while(high < low && arr[high] >= key)
{
if(arr[high] == key)//處理相等元素
{
swap(arr[right],arr[high]);
right--;
rightlen++;
}
high--;
}
arr[low] = arr[high];
while(high > low && arr[low] <= key)
{
if(arr[low] == key)
{
swap(arr[left],arr[low]);
left++;
leftlen++;
}
low++
}
arr[high] = arr[low];
}
arr[low] = ley;
//一次排序結束
//把與樞軸key相同的元素移到樞軸最終位置周圍
int i = low - 1;
int j = first;
while(j < left && arr[i] != key)
{
swap(arr[i],arr[j]);
i--;
j++;
}
i = low +1;
j = last;
while(j > right && arr[i] != key)
{
swap(arr[i],arr[j]);
i++;
j--;
}
QSort(arr,first,low-1-leftlen);
QSort(arr,low + 1 + rightlen,last);
}
②:測試資料
③:測試資料分析:三數取中選擇樞軸+插排+聚集相等元素的組合竟然效果好的 出奇。
原因:在陣列中,如果有相等的元素,那麼減少不少冗餘的劃分。這點再重複陣列中的體現特別明顯。(這裡的插排的作用還是不明顯)。
優化3:優化遞迴操作
①:思想:快排函式在函式末尾有兩次遞迴操作,我們可以對其使用尾遞迴優化。
②優點:如果待排序的序列劃分的極端不平衡,遞迴的深度將趨近於n,而棧的大小有限,每次遞迴呼叫都會耗費一定 的佔空間,函式引數越多,每次遞迴耗用的空間也就越多。優化後,可以縮減堆疊深度,由原來的O(n)縮減為O(log n)
void Qsrot(int arr[] , int low , int high)
{
int pivotPos = -1;
if(high - low + 1 < 10)
{
InsertSort(arr , low , high);
return;
}
while(low < high)
{
pivotPos = Partition(arr,low,high);
QSort(arr , low , pivot - 1);
low = pivot + 1;
}
}
注意:第一次遞迴後,low就沒用了;此時第二次遞迴可以使用迴圈代替。
②測試資料
③:測試分析:其實這種優化編譯器會自己優化,相比於不用該優化方法,時間幾乎沒少。
優化4:使用並行或多執行緒處理子程式(不詳細解釋)
所有的資料測試:
概括:這裡效率最好的快排組合 是:三數取中 + 快排 + 聚合相等元素。他和STL中的Sort函式的效率差不多。