前言
在開發中會經常用到排序,經常用到排序比如:冒泡排序,選擇排序,直接插入排序等。
那什麼是排序呢?這個其實都很熟悉了,其實排序還分爲內排序和外排序
內排序:在排序整個過程中,待排序的所有記錄全部被放置在內存中
外排序:由於排序的記錄個數太多,不能同時放置在內存,整個排序過程需要在內外存 之間多次交換數據才能進⾏。
常用的是內排序。接下來聊聊常見的排序算法。
在排序的過程過程中進行比較,然後交換是不可避免的。
所以可以先設計一個公共的交換函數,利用哨兵思想來設計一次數據結構,第0個位置不做數據存儲,作爲哨兵或者臨時遍歷使用,具體代碼如下:
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
// 排序算法數據結構設計
#define MAXSIZE 10000
typedef struct
{
// 用於存儲要排序數組,r[0]用作哨兵或臨時變量
int r[MAXSIZE+1];
// 用於記錄順序表的長度
int length;
}SqList;
// 常用交換函數
// 交換L中數組r的下標爲i和j的值
void swap(SqList *L,int i,int j)
{
int temp = L->r[i];
L->r[i] = L->r[j];
L->r[j] = temp;
}
// 打印
void print(SqList L)
{
int i;
for(i=1;i<L.length;i++)
printf("%d,",L.r[i]);
printf("%d",L.r[i]);
printf("\n");
}
1. 冒泡排序
冒泡排序:是一種交換排序,兩兩比較相鄰記錄的關鍵字,如果反序則交換,直到沒有反序的記錄爲止。
在冒泡排序的實現時,可能會寫成下面的形式:
// 冒泡排序-(冒泡排序初級版本)
void BubbleSort0(SqList *L){
int i,j;
for (i = 1; i < L->length; i++) {
for (j = i+1; j <= L->length; j++) {
if(L->r[i] > L->r[j])
swap(L, i, j);
}
}
}
其實上面的代碼嚴格的來說並不是冒泡排序,是對順序表L進行交換排序,因爲並不滿足兩兩比較,所以對其進行改進,如下:
// 冒泡排序-對順序表L作冒泡排序(正宗冒泡排序算法)
void BubbleSort(SqList *L){
int i,j;
for (i = 1; i < L->length; i++) {
// ✅ j是從後面往前循環
for (j = L->length-1; j >= i; j--) {
// 若前者大於後者(注意與上一個算法區別所在)
if(L->r[j]>L->r[j+1])
//交換L->r[j]與L->r[j+1]的值;
swap(L, j, j+1);
}
}
}
其實,還可以對冒泡排序進行優化,如果這個數據交換一次時,是有序的,那麼後面的比較是重複無意義的。我們可以用一個值來標記是否有序。
// 冒泡排序-對順序表L冒泡排序進行優化
void BubbleSort2(SqList *L){
int i,j;
// flag用作標記
Status flag = TRUE;
// i從[1,L->length) 遍歷;
// 如果flag爲False退出循環. 表示已經出現過一次j從L->Length-1 到 i的過程,都沒有交換的狀態;
for (i = 1; i < L->length && flag; i++) {
// flag 每次都初始化爲FALSE
flag = FALSE;
for (j = L->length-1; j>=i; j--) {
if(L->r[j] > L->r[j+1]){
//交換L->r[j]和L->r[j+1]值;
swap(L, j, j+1);
//如果有任何數據的交換動作,則將flag改爲true;
flag=TRUE;
}
}
}
}
2. 簡單選擇排序
簡單排序算法:就是通過n-i
次關鍵詞比較,從n - i +
個記錄中找到關鍵字最小的記錄,並和第i(1<i<n)
個記錄進行交換。
如上圖,先從下標(1-9)中找到最小記錄,i=1, min=2
,然後和第一個記錄交換,得到如下:
然後,i=2, min = 9
,和第二個記錄交換:
依次類推,進行比較,最終完成排序。
代碼實現:
// 選擇排序--對順序表L進行簡單選擇排序
void SelectSort(SqList *L){
int i,j,min;
for (i = 1; i < L->length; i++) {
//✅ 1.將當前下標假設爲最小值的下標
min = i;
//✅ 2.循環比較i之後的所有數據
for (j = i+1; j <= L->length; j++) {
//✅ 3.如果有小於當前最小值的關鍵字,將此關鍵字的下標賦值給min
if (L->r[min] > L->r[j]) {
min = j;
}
}
//✅ 4.如果min不等於i,說明找到了最小值,則交換2個位置下的關鍵字
if(i!=min)
swap(L, i, min);
}
}
3. 直接插入排序
直接插入排序:是將一個記錄插入到已經排好序的有序表中,從而得到一個新的記錄數增加1的有序表
如上圖:
- 循環將
i
從第二個元素到最後一個元素作爲待排序元素 - 判斷當前待排序元素是否小於其前一個元素(
i-1
),小於,則參與插入排序 - 使用臨時遍歷變量temp,存儲待排序元素,(在本次循環中
temp = 3
) - 循環遍歷,找到第二個元素之前,能插入的位置,判斷依據是從
i-1到0
這個空間,滿足L->r[j] > temp
, 則將L->r[j+1] = L->r[j]
- 找到元素
5 > temp
, 需要把5往前⾯面移動,覆蓋元素3
6. 此時r[0]
不大於temp
則j層循環結束. 目前 j = 0
7. 此時需要把 3
覆蓋到j=1
的位置,但是由於j
退出循環時等於0
, 所以是r[j+1] = temp
最終完成本次循環,如下:
然後依次i++
,參照上面的步驟,最終完成排序。具體實現如下:
// 直接插入排序算法
void InsertSort(SqList *L){
int i,j;
//L->r[0] 哨兵 可以把temp改爲L->r[0]
int temp=0;
//假設排序的序列集是{0,5,4,3,6,2};
//i從2開始的意思是我們假設5已經放好了. 後面的牌(4,3,6,2)是插入到它的左側或者右側
for(i=2;i<=L->length;i++)
{
//需將L->r[i]插入有序子表
if (L->r[i]<L->r[i-1])
{
//設置哨兵 可以把temp改爲L->r[0]
temp = L->r[i];
for(j=i-1;L->r[j]>temp;j--)
//記錄後移
L->r[j+1]=L->r[j];
//插入到正確位置 可以把temp改爲L->r[0]
L->r[j+1]=temp;
}
}
}
空間複雜度: O(1)
時間複雜度: O(n2)
4. 希爾排序
希爾排序思想:在插入排序之前,將整個序列調整爲基本有序,然後再對全體序列進行一次直接插入排序。
那麼怎麼將序列調整爲基本有序呢?
希爾排序是把記錄按照下標的一定增量分組,對每組直接使用插入排序,
;隨着增量逐漸減少,每組包含的關鍵字越來越多,當增量減爲1時,整個序列被分爲1組,算法終止。
假設,有下面的一組序列,按照希爾排序的原理,對其進行分組:
初始化增量爲increment = Length / 2 = 5
,每組對應不同的顏色,即分爲{8,3},{9,5},{1,4},{7,6},{2,0}
五組,然後對每組進行插入排序,那麼此時3、5、6、0
,這些小元素會被調整到前面。
然後縮小增量(第一次循環增量爲5),increment = increment / 2= 5/2 = 2
,增量爲2
,即數組被分爲兩組:{3,1,0,9,7} {5,6,8,4,2}
然後對這2個序列進行直接插⼊排序,結果爲:{0,1,3,7,9} {2,4,5,6,8}
,最終結果如下:
然後縮小增量(第二次循環增量爲2),increment = increment / 2= 2/2 = 1
,增量爲1
,即數組被分爲一組,對這個序列直接進行插入排序如下,最終完成排序。
思路(僞代碼):
1. 初始化增量爲整個序列的長度
2. 開始循環,對序列根據增量進行分組,每組進行插入排序,當增量大於1時結束循環
3. 增量序列 = 增量序列/3 + 1
4. 循環每個分組,判斷分組中,是否需要交換,需要則按照插入排序交換對應位置的元素。
// 希爾排序
void shellSort(SqList *L){
int i,j;
// ✅ 初始化增量爲整個序列的長度
int increment = L->length;
//0,9,1,5,8,3,7,4,6,2
// ✅ 開始循環,當increment 爲1時,表示希爾排序結束
do{
// ✅ 增量序列
increment = increment/3+1;
// ✅ i的待插入序列數據 [increment+1 , length]
for (i = increment+1; i <= L->length; i++) {
// 如果r[i] 小於它的序列組元素則進行插入排序,例如3和9. 3比9小,所以需要將3與9的位置交換
// ✅ 判斷,然後進行插入排序
if (L->r[i] < L->r[i-increment]) {
// 將需要插入的L->r[i]暫時存儲在L->r[0].和插入排序的temp 是一個概念;
L->r[0] = L->r[i];
// 記錄後移
for (j = i-increment; j > 0 && L->r[0]<L->r[j]; j-=increment) {
L->r[j+increment] = L->r[j];
}
// 將L->r[0]插入到L->r[j+increment]的位置上;
L->r[j+increment] = L->r[0];
}
}
}while (increment > 1);
}
5. 堆排序
堆是具有一下性質的完全二叉樹:
- 每個結點的值都大於或者等於其左右孩子結點的值,稱爲大頂堆
- 每個結點的值都小於或者等於其左右孩子結點的值,稱爲小頂堆
如果按照層尋遍歷的方式給結點從1開始編號,則結點之間滿足以下關係:
堆排序就是利用堆(假設選擇大頂堆)進行排序的算法,其基本思想如下:
- 將待排序的序列構成一個大頂堆,此時,整個序列最大值就的堆頂的根節點,將其有堆數組的末尾元素交換,此時末尾元素爲最大
- 然後將剩餘的
n-1
個序列重新構成一個堆,這樣就會得到n個元素的次大值, 如此重複執行,就能得到⼀個有序列
接下來以序列{4,6,8,5,9}
爲例,詳細的分析一下:
- 構造初始堆,將給定⽆序列構造成一個⼤頂堆(一般升序採⽤大頂堆,降序採用小頂堆)
A. 給的無序序列結構如下:
B. 從最後一個非葉子結點開始(葉子結點不用調整),第一個非葉子結點2
結點2上數據 6 大於左子樹結點數據5,小於其右子樹結點數據9,所以要將9 和 6 互換。
C. 找到第二個非葉子結點4
,從[4,9,8]
中找到最大的進行交換。
D. 因爲4
和9
的交換,導致【4,5,6】
結構混亂,不符合大頂堆條件,需要繼續調整,交換4
和6
。至此,經過上面的調整,我們將無序列 調整成⼀個⼤頂堆結構。
-
將堆頂元素和末尾元素進行交換,使末尾元素最大,然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第⼆大元素。如此反覆進行交換、重建、交換,
A. 將堆頂元素9和末尾元素4交換,此時末尾元素9,將不參與後續排序
B. 重新調整結構,使其繼續滿⾜堆定義 從[ 4, 6 , 8]
中找到最大的,4
與8
進行交換. 經過調整得到大頂堆
C. 再將堆頂元素
8
與末尾元素5
進行交換,得到第⼆大元素8
,然後繼續上面的步驟進行調整交換,最終得到如下的有序序列
堆排序思路
- 將無需序列構建成一個堆,根據升降序,選擇構建大頂堆或者小頂堆(升序,大頂堆,降序,小頂堆)
- 將堆頂元素與末尾元素交換,將最⼤元素或者最小元素“沉”到數組末端
- 重新調整使之滿足堆定義,繼續交換堆頂和當前末尾元素;反覆,直到序列有序
在構建大頂堆時,從最後一個非葉子開始,由於堆是一個完全二叉樹,其結點按層序編號,對任⼀結點i (1 ≤ i ≤ n)
有:
- 如果
i=1
,則結點i
是⼆叉樹的根. 無雙親結點。 如果i > 1
,則其雙親是結點[ i / 2 ]
- 如果
2i > n
,則結點i
⽆左孩子 (結點i
爲葉⼦結點), 否則左孩⼦子是結點2i
- 如果
2i + 1 > n
,則結點i
⽆右孩子; 否則其右孩⼦子是結點2i+1
接下來實現一下大頂堆調整函數:
// 大頂堆調整函數
void HeapAjust(SqList *L,int s,int m){
int temp,j;
//1. 將L->r[s] 存儲到temp ,方便後面的交換過程;
temp = L->r[s];
//2.
//因爲這是顆完全二叉樹,而s也是非葉子根結點. 所以它的左孩子一定是2*s,而右孩子則是2s+1
for (j = 2 * s; j <=m; j*=2) {
//3. ✅判斷j是否是最後一個結點, 並且找到左右孩子中最大的結點;
//如果左孩子小於右孩子,那麼j++; 否則不自增1. 因爲它本身就比右孩子大;
if(j < m && L->r[j] < L->r[j+1])
++j;
//4. ✅比較當前的temp 是不是比較左右孩子大;如果大則表示我們已經構建成大頂堆了,跳出循環
if(temp >= L->r[j]) {
break;
}
//5. ✅小於,則將L->[j] 的值賦值給非葉子根結點
L->r[s] = L->r[j];
//6. ✅將s指向j; 因爲此時L.r[4] = 60, L.r[8]=60. 那我們需要記錄這8的索引信息.等退出循環時,能夠把temp值30 覆蓋到L.r[8] = 30. 這樣才實現了30與60的交換;
s = j;
}
//7. ✅將L->r[s] = temp. 其實就是把L.r[8] = L.r[4] 進行交換;
L->r[s] = temp;
}
堆排序實現:
// 堆排序--對順序表進行堆排序
void HeapSort(SqList *L){
int i;
//✅ 1.將現在待排序的序列構建成一個大頂堆;
//將L構建成一個大頂堆;
//i從length/2.因爲在對大頂堆的調整其實是對非葉子的根結點調整.
for(i=L->length/2; i>0;i--){
HeapAjust(L, i, L->length);
}
//✅ 2.逐步將每個最大的值根結點與末尾元素進行交換,並且再調整成大頂堆
for(i = L->length; i > 1; i--){
//✅ 將堆頂記錄與當前未經排序子序列的最後一個記錄進行交換;
swap(L, 1, i);
//✅ 將L->r[1...i-1]重新調整成大頂堆;
HeapAjust(L, 1, i-1);
}
}
堆排序的時間複雜度爲:O(nlogn)
堆排序是就地排序,空間複雜度爲常數:O(1)
6. 歸併排序
歸併排序是利用歸併的思想實現排序,它的原理是假設初始序列含有n
個記錄,則可以看成n
個有序的子序列,每個子序列的長度爲1
,然後兩兩合併,得 到[n/2]
個長度爲2
或1
的有序子序列。再兩兩歸併,如此重複,直到得到一個長度爲n
的有序列爲此,這種排序方法稱爲2路路歸併排序
如下圖,將序列依次拆分爲長度爲1
的子序列
,然後在兩兩歸併,得到四個長度爲2
的有序序列,然後再兩兩歸併,得到2個長度爲4
的有序序列,再歸併爲一個有序序列。
接下來分析一下歸併排序的執行流程:
假設對下面的一個無序序列進行歸併排序
首先low = 1,hight = 9
,求得mid = (low + hight)/2 = 5
,然後將原序列拆分爲下面兩個序列,
然後對[low-mid]
和[mid+1-hight]
的兩個序列遞歸拆分,最終拆分爲長度爲1
的子序列。
然後開始兩兩合併。
我們來着重分析一下最後兩個子序列的合併:
-
第一次循環,
SR[i] = SR[1] = 10
與SR[j] = SR[6] = 20
進⾏比較。SR[i] < SR[j]
,那麼將TR[k] = SR[i]
;此時i++, k++
,那麼如果是
SR[i] > SR[j]
的話,將SR[j]
存儲到TR[k]
這個數組。就是j++, k++
第一次循環結束:i = 2,m = 5,j = 6,n = 9
-
第二次循環,
SR[i] = SR[2] = 30
與SR[j] = SR[6] = 20
進⾏比較。SR[i] > SR[j]
,那麼將TR[k] = SR[j]
;此時j++, k++
,第二次循環結束:
i = 2,m = 5,j = 7,n = 9
-
第三次循環,
SR[i] = SR[2] = 30
與SR[j] = SR[7] = 40
進⾏比較。SR[i] < SR[j]
,那麼將TR[k] = SR[i]
;此時i++, k++
,第三次循環結束:
i = 3,m = 5,j = 7,n = 9
-
第四次循環,
SR[i] = SR[3] = 50
與SR[j] = SR[7] = 40
進⾏比較。SR[i] > SR[j]
,那麼將TR[k] = SR[j]
;此時j++, k++
,第四次循環結束:
i = 3,m = 5,j = 8,n = 9
-
第五次循環,
SR[i] = SR[3] = 50
與SR[j] = SR[8] = 60
進⾏比較。SR[i] < SR[j]
,那麼將TR[k] = SR[i]
;此時i++, k++
,第五次循環結束:
i = 4,m = 5,j = 8,n = 9
-
第六次循環,
SR[i] = SR[4] = 70
與SR[j] = SR[8] = 60
進⾏比較。SR[i] > SR[j]
,那麼將TR[k] = SR[j]
;此時j++, k++
,第六次循環結束:
i = 4,m = 5,j = 9,n = 9
-
第七次循環,
SR[i] = SR[4] = 70
與SR[j] = SR[9] = 80
進⾏比較。SR[i] < SR[j]
,那麼將TR[k] = SR[i]
;此時i++, k++
,第七次循環結束:
i = 5,m = 5,j = 9,n = 9
-
第八次循環,
SR[i] = SR[5] = 90
與SR[j] = SR[9] = 80
進⾏比較。SR[i] > SR[j]
,那麼將TR[k] = SR[j]
;此時j++, k++
,第八次循環結束:
i = 5,m = 5,j = 10,n = 9
第八次循環結束後,j>n
, 不滿足循環條件,結束循環。
然後判斷,將兩個子序列中剩餘的元素拼到TR
後面,最終合併爲有序序列。
代碼實現如下:
//3 ✅將有序的SR[i..mid]和SR[mid+1..n]歸併爲有序的TR[i..n]
void Merge(int SR[],int TR[],int i,int m,int n)
{
int j,k,l;
//1.✅將SR中記錄由小到大地併入TR
for(j=m+1,k=i;i<=m && j<=n;k++)
{
if (SR[i]<SR[j])
TR[k]=SR[i++];
else
TR[k]=SR[j++];
}
//2.✅將剩餘的SR[i..mid]複製到TR
if(i<=m)
{
for(l=0;l<=m-i;l++)
TR[k+l]=SR[i+l];
}
//3.✅將剩餘的SR[j..mid]複製到TR
if(j<=n)
{
for(l=0;l<=n-j;l++)
TR[k+l]=SR[j+l];
}
}
//2. ✅將SR[s...t] 歸併排序爲 TR1[s...t];
void MSort(int SR[],int TR1[],int low, int hight){
int mid;
int TR2[MAXSIZE+1];
if(low == hight)
TR1[low] = SR[low];
else{
//1.將SR[low...hight] 平分成 SR[low...mid] 和 SR[mid+1,hight];
mid = (low + hight)/2;
//2. 遞歸將SR[low,mid]歸併爲有序的TR2[low,mid];
MSort(SR, TR2, low, mid);
//3. 遞歸將SR[mid+1,hight]歸併爲有序的TR2[mid+1,hight];
MSort(SR, TR2, mid+1, hight);
//4. 將TR2[low,mid] 與 TR2[mid+1,hight], 歸併到TR1[low,hight]中
Merge(TR2, TR1, low, mid, hight);
}
}
//1. ✅對順序表L進行歸併排序
void MergeSort(SqList *L){
MSort(L->r,L->r,1,L->length);
}
歸併排序的非遞歸實現:
//歸併排序(非遞歸)-->對順序表L進行非遞歸排序
//對SR數組中相鄰長度爲s的子序列進行兩兩歸併到TR[]數組中;
void MergePass(int SR[],int TR[],int s,int length){
int i = 1;
int j;
//1. ✅合併數組
//s=1 循環結束位置:8 (9-2*1+1=8)
//s=2 循環結束位置:6 (9-2*2+1=6)
//s=4 循環結束位置:2 (9-2*4+1=2)
//s=8 循環結束位置:-6(9-2*8+1=-6) s = 8時,不會進入到循環;
while (i<= length-2*s+1) {
//兩兩歸併(合併相鄰的2段數據)
Merge(SR, TR, i, i+s-1, i+2*s-1);
i = i+2*s;
/*
s = 1,i = 1,Merge(SR,TR,1,1,2);
s = 1,i = 3,Merge(SR,TR,3,3,4);
s = 1,i = 5,Merge(SR,TR,5,5,6);
s = 1,i = 7,Merge(SR,TR,7,7,8);
s = 1,i = 9,退出循環;
*/
/*
s = 2,i = 1,Merge(SR,TR,1,2,4);
s = 2,i = 5,Merge(SR,TR,5,6,8);
s = 2,i = 9,退出循環;
*/
/*
s = 4,i = 1,Merge(SR,TR,1,4,8);
s = 4,i = 9,退出循環;
*/
}
//2. ✅如果i<length-s+1,表示有2個長度不等的子序列. 其中一個長度爲length,另一個小於length
// 1 < (9-8+1)(2)
//s = 8時, 1 < (9-8+1)
if(i < length-s+1){
//Merge(SR,TR,1,8,9)
Merge(SR, TR, i, i+s-1, length);
}else{
//③只剩下一個子序列;
for (j = i; j <=length; j++) {
TR[j] = SR[j];
}
}
}
void MergeSort2(SqList *L){
int *TR = (int *)malloc(sizeof(int) * L->length);
int k = 1;
//k的拆分變換是 1,2,4,8;
while (k < L->length) {
//將SR數組按照s=2的長度進行拆分合並,結果存儲到TR數組中;
//注意:此時經過第一輪的歸併排序的結果是存儲到TR數組了;
MergePass(L->r, TR, k, L->length);
k = 2*k;
//將剛剛歸併排序後的TR數組,按照s = 2k的長度進行拆分合並. 結果存儲到L->r數組中;
//注意:因爲上一輪的排序的結果是存儲到TR數組,所以這次排序的數據應該是再次對TR數組排序;
MergePass(TR, L->r, k, L->length);
k = 2*k;
}
}
7. 快速排序
快速排序的基本思想:通過一趟排序將待排序記錄分割爲獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可以分別對兩部分記錄繼續進行排序,以達到整個排序有序的目的。
快速排序思路:
- 判斷low是否小於hight
- 求得樞軸,並將數組樞軸左邊的關鍵字都比樞軸對應的關鍵字小,右邊的關鍵字都比樞軸對應的關鍵字大
- 將數組一份爲二,分別對低子表和高子表排序
那麼如何找一個樞軸,怎麼將樞軸變量放在合適的位置,並且使得它的左側關鍵字均⽐它⼩, 右側關鍵字均⽐比它大。
接下來,我們一下面的數組爲例,分析一下快速排序的執行流程。
首先,選擇子表中第1個記錄作爲樞軸變量,pivotkey = 50
。
然後,從表的兩端往中間掃描,開始循環,循環判斷,
- 從高位開始,找到比
pivokey
更小的值的下標位置,即:循環判斷是否滿足low<high
並且r[high] >= pivotkey
,滿足,則遞減high
,不滿足條件,則跳出循環 - 然後交換比樞軸值小的記錄到低端
- 從低位開始,找到比
pivokey
更大的值的下標位置。即:循環判斷是否滿足low<high
並且r[low] <= pivotkey
,滿足,則遞增low
,不滿足條件,則跳出循環 - 然後交換比樞軸值大的記錄到高端
對上圖的序列第一輪循環,判斷條件
low < high
:
- 首先比較
L->r[high] >= pivotkey && low < high
,此時low=1,high=9,L->r[9] < 50
, 則循環退出。然後交換,將比樞軸記錄小的記錄交換到低端位置上,得到下圖:
-
循環判斷
low < high && L->r[low] <= pivotkey
,此時low=1,high=9,L->r[1] < 50
,則low++,low=2
;然後繼續判斷是否滿足
low < high && L->r[low] <= pivotkey
,此時low=2,high=9,L->r[2] < 50
,則low++,low=3
;然後繼續判斷是否滿足
low < high && L->r[low] <= pivotkey
,此時low=3,high=9,L->r[3] > 50
,不滿足條件,跳出循環,然後將比樞軸記錄大的記錄交換到高端位置上,得到下圖:
至此第一輪循環結束,low = 3, high = 9
,滿足循環條件,進入第二輪循環。
對上圖的序列第二輪循環,判斷條件
low < high
(此時low = 3, high = 9
):
-
首先比較
L->r[high] >= pivotkey && low < high
,此時low=3,high=9,L->r[9] > 50
,滿足條件,high--
,得到high=8
;繼續比較
L->r[high] >= pivotkey && low < high
,此時low=3,high=8,L->r[8]=60 > 50
,滿足條件,high--
,得到high=7
;繼續比較
L->r[high] >= pivotkey && low < high
,此時low=3,high=7,L->r[7]=80 > 50
,滿足條件,high--
,得到high=6
;繼續比較
L->r[high] >= pivotkey && low < high
,此時low=3,high=6,L->r[6]=40 < 50
,不滿足條件,跳出循環。然後交換low
和high
的值,將比樞軸記錄小的記錄交換到低端位置上,得到下圖:
-
循環判斷
low < high && L->r[low] <= pivotkey
,此時low=3,high=6,L->r[3] < 50
,則low++,low=4
;循環判斷
low < high && L->r[low] <= pivotkey
,此時low=4,high=6,L->r[4] < 50
,則low++,low=5
;循環判斷
low < high && L->r[low] <= pivotkey
,此時low=5,high=6,L->r[5] > 50
,不滿足條件,跳出循環,然後交換low
和high
的值,將比樞軸記錄大的記錄交換到高端位置上,得到下圖:
至此第二輪循環結束,low = 5, high = 6
,滿足循環條件,進入第三輪循環。
對上圖的序列第三輪循環,判斷條件
low < high
(此時low = 5, high = 6
):
首先比較 L->r[high] >= pivotkey && low < high
,此時 low=5,high=6,L->r[5] < 50
,滿足條件,high--
,得到high=5
;
此時low == high
退出循環! 表示這一次從兩端交替向中間的掃描已經全部完成了。此時返回low=5
。
接下來按照上面的邏輯,對序列的【1,5-1】和【5+1,9】子序列進行操作。最終得到一個有序的序列。
//✅3. 交換順序表L中子表的記錄,使樞軸記錄到位,並返回其所在位置
//此時在它之前(後)的記錄均不大(小)於它
int Partition(SqList *L,int low,int high){
int pivotkey;
//pivokey 保存子表中第1個記錄作爲樞軸記錄;
pivotkey = L->r[low];
//1. 從表的兩端交替地向中間掃描;
while (low < high) {
//2. 比較,從高位開始,找到比pivokey更小的值的下標位置;
while (low < high && L->r[high] >= pivotkey) {
high--;
}
//3. 將比樞軸值小的記錄交換到低端;
swap(L, low, high);
//4. 比較,從低位開始,找到比pivokey更大的值的下標位置;
while (low < high && L->r[low] <= pivotkey) {
low++;
}
//5. 將比樞軸值大的記錄交換到高端;
swap(L, low, high);
}
//返回樞軸pivokey 所在位置;
return low;
}
//✅2. 對順序表L的子序列L->r[low,high]做快速排序;
void QSort(SqList *L,int low,int high){
int pivot;
if(low < high){
//將L->r[low,high]一分爲二,算出中樞軸值 pivot;
pivot = Partition(L, low, high);
printf("pivot = %d L->r[%d] = %d\n",pivot,pivot,L->r[pivot]);
//對低子表遞歸排序;
QSort(L, low, pivot-1);
//對高子表遞歸排序
QSort(L, pivot+1, high);
}
}
//✅ 1. 調用快速排序(爲了保證一致的調用風格)
void QucikSort(SqList *L){
QSort(L, 1, L->length);
}
時間複雜度:最好情況爲O(nlogn)
,最壞情況爲O(n2)
空間複雜度取決於遞歸造成的棧空間,最好情況爲O(logn)
,最壞情況爲O(n),平均情況下時間複雜度爲O(logn)
上面的算法,在求解樞軸的時候,我們比較暴力,直接取第一個元素爲樞軸,這樣可能存在一些問題,比如第一個元素在當前序列中是最大或者最小時,交換後就會出現一些問題。
那麼,我們可以對樞軸的求解進行優化,取當前序列的中間數爲樞軸,儘量避免取到最大或者最小的情況。
在比較時,要頻繁的交換高位和低位的值,我們可以對高低位進行覆蓋,在最後一次(low = high
)時,用樞軸進行賦值。
比如:
- ⽤高位
high
與pivotkey
進⾏比較找到⽐樞軸小的記錄. 交換到低端位置上
替換後爲:
- ⽤低位
low
與pivotkey
進⾏比較找到⽐樞軸大的記錄. 交換到高端位置上
替換後爲:
3. 這樣依次比較替換,最終得到:
然後替換L->r[low] = L->r[0]
,即:將低位的值替換爲樞軸的值。
優化實現:
int Partition2(SqList *L,int low,int high){
int pivotkey;
// ✅ 1.優化選擇樞軸
//✅ 計算數組中間的元素的下標值;
int m = low + (high - low)/2;
//✅ 將數組中的L->r[low] 是整個序列中左中右3個關鍵字的中間值;
//交換左端與右端的數據,保證左端較小;[9,1,5,8,3,7,4,6,2]
if(L->r[low]>L->r[high])
swap(L, low, high);
//交換中間與右端的數據,保證中間較小; [2,1,5,8,3,7,4,6,9];
if(L->r[m]>L->r[high])
swap(L, high, m);
//交換中間與左端,保證左端較小;[2,1,5,8,3,7,4,6,9]
if(L->r[m]>L->r[low])
swap(L, m, low);
//交換後的序列:3,1,5,8,2,7,4,6,9
//此時low = 3; 那麼此時一定比選擇 9,2更合適;
// ✅ 2. 優化不必要的交換
//pivokey 保存子表中第1個記錄作爲樞軸記錄;
pivotkey = L->r[low];
//將樞軸關鍵字備份到L->r[0];
L->r[0] = pivotkey;
// ✅ 3. 從表的兩端交替地向中間掃描;
while (low < high) {
//✅ 比較,從高位開始,找到比pivokey更小的值的下標位置;
while (low < high && L->r[high] >= pivotkey)
high--;
//✅ 將比樞軸值小的記錄交換到低端;
//swap(L, low, high);
//✅ 用替換的方式將比樞軸值小的記錄替換到低端
L->r[low] = L->r[high];
//✅ 比較,從低位開始,找到比pivokey更大的值的下標位置;
while (low < high && L->r[low] <= pivotkey)
low++;
//✅ 將比樞軸值大的記錄交換到高端;
//swap(L, low, high);
//✅ 替換的方式將比樞軸值大的記錄替換到高端
L->r[high] = L->r[low];
}
//將樞軸數值替換會L->r[low]
L->r[low] = L->r[0];
//返回樞軸pivokey 所在位置;
return low;
}
//✅2. 對順序表L的子序列L->r[low,high]做快速排序;
#define MAX_LENGTH_INSERT_SORT 7 //數組長度的閥值
void QSort2(SqList *L,int low,int high){
int pivot;
//✅ 當high-low 大於常數閥值是用快速排序;
if((high-low)>MAX_LENGTH_INSERT_SORT){
//將L->r[low,high]一分爲二,算出中樞軸值 pivot;
pivot = Partition(L, low, high);
printf("pivot = %d L->r[%d] = %d\n",pivot,pivot,L->r[pivot]);
//對低子表遞歸排序;
QSort(L, low, pivot-1);
//對高子表遞歸排序
QSort(L, pivot+1, high);
}else{
// ✅當high-low小於常數閥值是用直接插入排序
}
}
//✅1. 快速排序優化
void QuickSort2(SqList *L)
{
QSort2(L,1,L->length);
}
排序總結: