W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
「快速排序 quick sort」是一種基于分治策略的排序算法,運行高效,應(yīng)用廣泛。
快速排序的核心操作是“哨兵劃分”,其目標(biāo)是:選擇數(shù)組中的某個元素作為“基準(zhǔn)數(shù)”,將所有小于基準(zhǔn)數(shù)的元素移到其左側(cè),而大于基準(zhǔn)數(shù)的元素移到其右側(cè)。具體來說,哨兵劃分的流程如圖 11-8 所示。
圖 11-8 哨兵劃分步驟
哨兵劃分完成后,原數(shù)組被劃分成三部分:左子數(shù)組、基準(zhǔn)數(shù)、右子數(shù)組,且滿足“左子數(shù)組任意元素
快速排序的分治策略
哨兵劃分的實質(zhì)是將一個較長數(shù)組的排序問題簡化為兩個較短數(shù)組的排序問題。
quick_sort.cpp
/* 元素交換 */
void swap(vector<int> &nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/* 哨兵劃分 */
int partition(vector<int> &nums, int left, int right) {
// 以 nums[left] 作為基準(zhǔn)數(shù)
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 從右向左找首個小于基準(zhǔn)數(shù)的元素
while (i < j && nums[i] <= nums[left])
i++; // 從左向右找首個大于基準(zhǔn)數(shù)的元素
swap(nums, i, j); // 交換這兩個元素
}
swap(nums, i, left); // 將基準(zhǔn)數(shù)交換至兩子數(shù)組的分界線
return i; // 返回基準(zhǔn)數(shù)的索引
}
快速排序的整體流程如圖 11-9 所示。
quick_sort.cpp
/* 快速排序 */
void quickSort(vector<int> &nums, int left, int right) {
// 子數(shù)組長度為 1 時終止遞歸
if (left >= right)
return;
// 哨兵劃分
int pivot = partition(nums, left, right);
// 遞歸左子數(shù)組、右子數(shù)組
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
從名稱上就能看出,快速排序在效率方面應(yīng)該具有一定的優(yōu)勢。盡管快速排序的平均時間復(fù)雜度與“歸并排序”和“堆排序”相同,但通??焖倥判虻男矢撸饕幸韵略?。
快速排序在某些輸入下的時間效率可能降低。舉一個極端例子,假設(shè)輸入數(shù)組是完全倒序的,由于我們選擇最左端元素作為基準(zhǔn)數(shù),那么在哨兵劃分完成后,基準(zhǔn)數(shù)被交換至數(shù)組最右端,導(dǎo)致左子數(shù)組長度為 n?1、右子數(shù)組長度為 0 。如此遞歸下去,每輪哨兵劃分后的右子數(shù)組長度都為 0 ,分治策略失效,快速排序退化為“冒泡排序”。
為了盡量避免這種情況發(fā)生,我們可以優(yōu)化哨兵劃分中的基準(zhǔn)數(shù)的選取策略。例如,我們可以隨機選取一個元素作為基準(zhǔn)數(shù)。然而,如果運氣不佳,每次都選到不理想的基準(zhǔn)數(shù),效率仍然不盡如人意。
需要注意的是,編程語言通常生成的是“偽隨機數(shù)”。如果我們針對偽隨機數(shù)序列構(gòu)建一個特定的測試樣例,那么快速排序的效率仍然可能劣化。
為了進一步改進,我們可以在數(shù)組中選取三個候選元素(通常為數(shù)組的首、尾、中點元素),并將這三個候選元素的中位數(shù)作為基準(zhǔn)數(shù)。這樣一來,基準(zhǔn)數(shù)“既不太小也不太大”的概率將大幅提升。當(dāng)然,我們還可以選取更多候選元素,以進一步提高算法的穩(wěn)健性。采用這種方法后,時間復(fù)雜度劣化至 O(n2) 的概率大大降低。
quick_sort.cpp
/* 選取三個元素的中位數(shù) */
int medianThree(vector<int> &nums, int left, int mid, int right) {
// 此處使用異或運算來簡化代碼
// 異或規(guī)則為 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1
if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right]))
return left;
else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right]))
return mid;
else
return right;
}
/* 哨兵劃分(三數(shù)取中值) */
int partition(vector<int> &nums, int left, int right) {
// 選取三個候選元素的中位數(shù)
int med = medianThree(nums, left, (left + right) / 2, right);
// 將中位數(shù)交換至數(shù)組最左端
swap(nums, left, med);
// 以 nums[left] 作為基準(zhǔn)數(shù)
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 從右向左找首個小于基準(zhǔn)數(shù)的元素
while (i < j && nums[i] <= nums[left])
i++; // 從左向右找首個大于基準(zhǔn)數(shù)的元素
swap(nums, i, j); // 交換這兩個元素
}
swap(nums, i, left); // 將基準(zhǔn)數(shù)交換至兩子數(shù)組的分界線
return i; // 返回基準(zhǔn)數(shù)的索引
}
在某些輸入下,快速排序可能占用空間較多。以完全倒序的輸入數(shù)組為例,由于每輪哨兵劃分后右子數(shù)組長度為 0 ,遞歸樹的高度會達到 n?1 ,此時需要占用 O(n) 大小的棧幀空間。
為了防止棧幀空間的累積,我們可以在每輪哨兵排序完成后,比較兩個子數(shù)組的長度,僅對較短的子數(shù)組進行遞歸。由于較短子數(shù)組的長度不會超過 n/2 ,因此這種方法能確保遞歸深度不超過 log?n ,從而將最差空間復(fù)雜度優(yōu)化至 O(log?n) 。
quick_sort.cpp
/* 快速排序(尾遞歸優(yōu)化) */
void quickSort(vector<int> &nums, int left, int right) {
// 子數(shù)組長度為 1 時終止
while (left < right) {
// 哨兵劃分操作
int pivot = partition(nums, left, right);
// 對兩個子數(shù)組中較短的那個執(zhí)行快排
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 遞歸排序左子數(shù)組
left = pivot + 1; // 剩余未排序區(qū)間為 [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 遞歸排序右子數(shù)組
right = pivot - 1; // 剩余未排序區(qū)間為 [left, pivot - 1]
}
}
}
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: