快速排序總結(QuickSort)
一.什麼是快速排序
1.快排的本質
快速排序是Koare在1962年提出的一種二叉樹結構
的交換
排序,它實際上是一種對於氣泡排序改進
的一種方法。
2.快排的思想
在待排序序列中任意取一個元素
作為基準元素,按照該基準元素將待排序序列分為兩個子序列
,左邊子序列的值都小於
基準值,右邊子序列的值都大於
基準值。然後把左右子序列當做一個子問題
,以同樣的方法處理左右子序列,直到所有的元素都排列在相對應的位置上為止。快排是一個遞迴
問題,它是按照二叉遞迴樹的前序路線
去劃分的。
3.程式碼實現
void QuickSort(int* num, int left, int right)
{
if (num == NULL )
return;
//遞迴出口
if (left >= right)
return;
//按照基準值將待排序區間劃分為兩個子區間
int div = PartSort(num, left, right);
//子問題排序左子區間
QuickSort(num, left, div - 1);
//子問題排序右子區間
QuickSort(num, div + 1, right);
}
上邊快排的程式碼只剩下partsort
沒有實現,這個函式也是快排的核心,下面我們實現這個函式。
二.一次快排(partsort)常見實現方式
1.hoare版本
int PartSort1(int* num, int begin, int end)
{
int key = num[end];
int last = end;
while (begin < end)
{
//1.左邊找到大於基準值的元素
while ((begin < end) && (num[begin] <= key))
begin++;
//2.右邊找到小於基準值的元素
while ((begin < end) && (num[end] >= key))
end--;
//3.交換兩個值
Swap(&num[begin], &num[end]);
}
//兩個下標走到一塊的時候,把基準值交換過來
Swap(&num[begin], &num[last]);
//返回基準值的位置
return begin;
}
2.挖坑法
//挖坑法
int PartSort2(int* num, int begin, int end)
{
int key = num[end];//把基準值拿出來(挖一個坑)
while (begin < end)
{
//1.左邊找到大於基準值的元素,並放入坑裡
while ((begin < end) && (num[begin] <= key))
begin++;
num[end] = num[begin];
//2.右邊找到小於基準值的元素,並放入坑裡
while ((begin < end) && (num[end] >= key))
end--;
num[begin] = num[end];
}
//3.把拿出來基準值放入坑裡
num[begin] = key;
//返回基準值的位置
return begin;
}
3.前後指標法
//前後指標法
int PartSort3(int* num, int begin, int end)
{
int prev = begin - 1;
int cur = begin;
int key = num[end];
while (cur < end)
{
//++prev=cur說明這個元素時第一個元素或者這是一段連續的小於基準的序列
if ((num[cur] < key) && (++prev != cur))
Swap(&num[cur], &num[prev]);
cur++;
}
//把基準元素放在所有小於基準元素的緊鄰後邊,可以達到基準的左邊小於基準,右邊大於基準
Swap(&num[++prev], &num[end]);
return prev;
}
三.快排常見的幾道面試題
1.快排的最壞和最優場景?時間複雜度分別為多少?
- 最壞場景:待排序列是有序的。既每次選的基準元素將待排序列劃分為左右子序列,而左右子序列中必定有一個為空,它的二叉遞迴樹深度是
N
,所以時間複雜度為O(N*N)
。 - 最好場景:每次選的基準都是待排序列最中間的元素,二叉遞迴樹深度為O
(logN)
,所以時間複雜度為O(N*logN)
。
2.快排的空間複雜度?
快排是一個遞迴的過程,每次函式呼叫只使用了常數的空間,所以它的空間複雜度就是它遞迴的深度。
- 最好場景:
O(logN)
- 最壞場景:
O(N)
3.快排的穩定性
(1).什麼是排序的穩定性
排序的穩定性是指,在對待排序列排序後,是否改變相同關鍵字的前後順序
(既相對位置
)。例如:對【2,3,1(第一個),1(第二個),5,6】
序列排序,如果排序結果為【1(第一個),1(第二個),2,3,5,6】
那麼這個排序演算法是穩定
的;如果排序結果為【1(第二個),1(第一個),2,3,5,6】
,那麼這個排序演算法是不穩定
的,因為關鍵字1
的相對位置變化了。
(2).快排是否穩定
以一個例子分析快排是否穩定:
(3).排序穩定性的應用場景
分析一個排序演算法的穩定性到底有什麼用呢?,下邊用一個場景分析:
假設在一次考試中,有兩個同學的考試成績是相同的,那麼我們到底把哪一個同學排在前面呢?這時,年級主任說把學號排在前面的同學的成績放在前面,因為學號是唯一的,我們可以先用學號把所有同學的成績排序,在用一個穩定的排序演算法在對總成績排序一次,這時兩個成績相同的同學一定是學號在前的他就排在前面
四.快排的使用場景
不同條件下,排序方法的選擇:(n指的是待排關鍵字的個數)
- 當
n較小
,可採用直接插入或直接選擇排序
。當記錄規模較小並且基本有序時,直接插入排序較好;否則因為直接選擇移動的記錄數少於直接插人,應選直接選擇排序。 - 當待排序列初始狀態
基本有序
,則應選用直接插人、冒泡或隨機的快速排序
; - 當
n較大
,則應採用時間複雜度為O(nlgn)的排序方法:快速排序、堆排序或歸併排序
。 - 快速排序是目前基於比較的
內部排序
中被認為是最好的方法,當待排序的關鍵字是隨機分佈
時,快速排序的平均時間最短
;堆排序所需的輔助空間少於快速排序
,並且不會出現快速排序可能出現的最壞情況
。這兩種排序都是不穩定
的。若要求排序穩定,則可選用歸併排序
。優先順序佇列
通常用堆排序
來實現
五.快排的優化
1. 優化一:三數取中
- 思想:這裡的優化主要針對
選擇基準元素
的優化。在選擇基準的時候,當時最壞場景
的時候,左邊是最小,右邊是最大,我們可以先根據最小和最大元素的下標確定中間的元素下標
,然後在和要選擇的基準元素交換位置
;如果是最優場景
,三數取中也可以優化,三個數中取一箇中間值
,必然要比隨機選擇基準好一些。 - 程式碼實現
//三數取中優化法(找三個值中間大的那個)
int GetMidKey(int* num, int begin, int end)
{
assert(num);
int mid = begin + (end - begin) / 2;
if (num[begin] < num[mid])
{
if (num[mid] < num[end])
return mid;
else
{
if (num[begin]>num[end])
return begin;
else
return end;
}
}
else
{
if (num[begin] < num[end])
return begin;
else
{
if (num[mid]>num[end])
return mid;
else
return end;
}
}
}
//hoare版本(左邊找大於基準,右邊找小於基準,交換)
int PartSort1(int* num, int begin, int end)
{
//三數取中優化
int index = GetMidKey(num, begin, end);
//和要選的基準交換
Swap(&num[index], &num[end]);
int key = num[end];
int last = end;
while (begin < end)
{
//1.左邊找到大於基準值的元素
while ((begin < end) && (num[begin] <= key))
begin++;
//2.右邊找到小於基準值的元素
while ((begin < end) && (num[end] >= key))
end--;
//3.交換兩個值
Swap(&num[begin], &num[end]);
}
//兩個下標走到一塊的時候,把基準值交換過來
Swap(&num[begin], &num[last]);
//返回基準值的位置
return begin;
}
2.優化二:小區間優化(把底層的遞迴替換掉)
快排對n較大的待排序列排序是很快的,但是對於n較小的序列時間複雜度和之間插入排序是差不多
的,而且快排的遞迴演算法還存在函式呼叫和返回的開銷
,所以我們可以考慮將快排遞迴演算法的底層遞迴用直接插入排序給替換
掉。
void QuickSort1(int* num, int left, int right)
{
if (num == NULL)
return;
//遞迴出口
if (left >= right)
return;
//小區間優化(替換掉後邊幾層的遞迴)
if (right - left + 1 < 10)
InsertSort(num, right - left + 1);
//按照基準值將待排序區間劃分為兩個子區間
int div = PartSort1(num, left, right);
//子問題排序左子區間
QuickSort1(num, left, div - 1);
//子問題排序右子區間
QuickSort1(num, div + 1, right);
}
六.將遞迴快排轉換為迴圈快排
1.演算法思想
遞迴演算法是對快排的遞迴二叉樹按照前序的路線來排列的,那麼我們要把遞迴演算法轉換為迴圈演算法,就要利用到棧的後進先出
的特性,從最內層開始處理。
2.程式碼實現
//快排非遞迴(按照遞迴樹的前序路線走)
void QuickSortNonR(int* num, int left, int right)
{
if (num == NULL || right <= left)
return;
Stack st;
StackInit(&st);
//先將整個區間壓棧
StackPush(&st, left);
StackPush(&st, right);
while (StackEmpty(&st) != 0)
{
//取棧頂並且出棧
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
//先劃分主區間,固定好一個基準
int div = PartSort1(num, begin, end);
//如果左子序列還有大於1個元素,繼續壓棧
if (begin < div - 1)
{
StackPush(&st, begin);
StackPush(&st, div - 1);
}
//如果右子序列還有大於1個元素,繼續壓棧
if (div + 1 < end)
{
StackPush(&st, div + 1);
StackPush(&st, end);
}
}
StackDestroy(&st);
}