文章目錄
排序及分類
排序:將一組雜亂無章的數據按一定規律順次排列起來。
即,將無序序列排成一個有序序列(由小到大或由大到小)的運算。
- 如果參加排序的數據結點包含多個數據域,那麼排序往往是針對其中某個域而言。
排序方法的分類
-
按數據存儲介質:內部排序和外部排序
按比較器個數: 串行排序和並行排序
按主要操作: 比較排序和基數排序
按輔助空間 :原地排序和非原地排序
按穩定性: 穩定排序和非穩定排序
按自然性: 自然排序和非自然排序 -
按存儲介質可分爲:
- 內部排序:數據量不大、數據在內存,無需內外存交換數據
- 外部排序:數據量較大、數據在外村(文件排序)
-
按比較器個數可分爲:
- 串行排序:單處理機(同一時刻比較一對元素)
- 並行排序:多處理機(同一時刻比較多對元素)
-
按主要操作可分爲:
- 比較排序:用比較的方法
插入排序、交換排序、選擇排序、歸併排序 - 基數排序: 不比較元素的大小、僅僅根據元素本身的取值確定其有序位置。
- 比較排序:用比較的方法
-
按輔助空間可分爲:
- 原地排序:輔助空間用量爲O(1)的排序方法。
-
按穩定性可分爲:
- 穩定排序:能夠使任何數值相等的元素,排序以後相對次序不變。
- 非穩定性排序:不是穩定排序的方法。
存儲結構——記錄序列以順序表存儲
#define MAXSIZE 20 //設記錄不超過20個
typedef int KeyType; //設關鍵字爲整型量(int型)
typedef struct{ //定義每個記錄(數據元素 )
KeyType key; //關鍵字
InfoType otherinfo; //其他數據項
}RedType; //Recond Type
typedef struct{ //定義順序表的結構
RedType r[MAXSIZE +1]; //存儲順序表的向量
//r[0]一般作哨兵或緩衝區
int length; //順序表的長度
}SqList;
插入排序
基本思想:
每步將一個待排序的對象,按其關鍵碼大小,插入到前面已經排好序的一組對象的適當位置上,直到對象全部插入位置。
基本操作:有序插入
- 在有序序列中插入一個元素,保持序列有序,有序長度不斷增加。
- 起初,a[0]是長度爲1的子序列。然後,逐一將a[1]至a[n-1]插入到有序子序列中。
- 在插入a[1]前,數組a的前半段(a[0] ~ a[i-1])是有序段,後半段是a[i] ~ a[n-1]) 是停留於輸入次序的 “無序段”。
- 插入a[i]使a[0]~a[i-1]有序,也就是 要爲a[i]找到有序位置j(0 ≤ j ≤ i),將a[i]插入在a[j]的位置上。
a.插在中間
b.插在最前面
c.插在最後面
插入排序的種類:
直接插入排序
- 採用順序查找法查找插入位置
- 直接插入排序,使用“哨兵”
1.複製爲哨兵(將零號位置a[0]設置爲爲哨兵)
2.記錄後移,查找插入位置
3.插入到正確位置
//直接插入排序算法
void InsertSort(SqList &L){
int i,j;
for(i=2;i<=L.length;++i){
if(L.r[i].key < L.r[i-1].key){ //若"<",需將L.r[i]插入有序子表
L.r[0] = L.r[i]; //複製爲哨兵
for(j=i-1;L.r[0].key < L.r[j].key; --j){
L.r[j+1] = L.r[j]; //記錄後移
}
L.r[j+1] = L.r[0]; //插入到正確位置
}
}
}
-
直接插入排序——性能分析
實現排序的基本操作有兩個:
1.“比較”序列中兩個關鍵字的大小;
2.“移動”記錄。最好的情況(關鍵字在記錄序列中順序有序):
1 3 11 23 34 55 65 79 90
最壞的情況(關鍵字在記錄序列中逆序有序):
90 79 65 55 34 23 11 3 1
-
原始數據越接近有序,排序速度越快
-
最壞情況下(輸入數據是逆有序的) Tw(n) = O(n²)
-
平均情況下,耗時差不多是最壞情況下的一半 Te(n) = O(n²)
-
要提高查找速度
- 減少元素的比較次數
- 減少篇元素的移動次數
折半插入排序
- 查找插入位置時採用折半查找法,其他的還是用哨兵存待插入的數據。
直接上代碼片段:
//折半插入排序算法
void BlnsertSort (SqList &L){
for(i=2; i <= L.length;++i){ //依次插入第2~第n個元素
L.r[0] = L.r[i]; //當前插入元素存到“哨兵”位置
low = 1;high = i-1; //採用二分法查找法查找插入位置
while( low <= high){
mid = (low + high) /2;
if(L.r[0].key < L.r[mid].key)
high = mid - 1;
else
low = mid + 1;
} //循環結束,high+1則爲插入位置
for(j = i-1; j>=high +1;--j)
L.r[j+1] = L.r[j]; //移動元素
L.r[high+1] = L.r[0]; //插入到正確位置
}
} //BlnsertSort
- 折半查找比順序查找快,所以折半插入排序就平均性能來說比直接插入排序要快;
- 它所需要的關鍵碼比較次數與待排序對象序列的初始排序無關,僅依賴於對象個數。在插入第i個對象時,需要經過【log2 i】+ 1 次關鍵碼比較,才能確定它應插入的位置。
- 當n較大時,總關鍵碼比較次數比直接插入排序的最壞情況要好得多,但比其最好情況要差;
- 在對象的初始排列已經按關鍵碼派好序或接近有序時,直接插入排序比折半插入排序的關鍵碼比較次數要少;
- 折半插入查找法——算法分析
折半插入排序的對象移動次數於直接插入排序相同,依賴於對象的初始化排列- 減少了比較次數,但沒有減少移動次數
- 平均性能優於直接插入排序
時間複雜度爲O(n²)
空間複雜度爲O(1) (只需要一個哨兵位置)
是一種穩定的排序方法
- 減少了比較次數,但沒有減少移動次數
希爾排序
在直接插入排序的提高版, 我們知道直接插入排序在直接插入排序在基本有序和待排序的記錄個數較少時,效率較高。
基本思想:
先將整個待排序紀錄序列分割成若干子序列,分別進行直接插入排序,待整個序列中的紀錄“基本有序”時,再對全體記錄進行一次直接插入排序。
- 希爾排序算法,特點:
1)縮小增量
2)多遍插入序列 - 希爾排序思路
1.定義增量序列Dk,從大到小選擇序列。
2.對每個增量序列Dk,進行“Dk-間隔”插入序列(k=M,M-1,…1) - 希爾排序特點
- 一次移動,移動位置較大,跳躍式的接近排序後的最終位置
- 最後一次只需要少量移動
- 增量序列必須是遞減的,最後一個必須是1
- 增量序列應該是互質的
//希爾排序
void ShellSort(Sqlist &L, int dlat[] ,int t){ //dlat[]增量序列
// 按增量序列dlta[0...t-1]對順序表L作希爾排序
for(k=0; k<t;++k) //t趟
ShellInsert(L,Dlta[k]); //一趟增量爲dlta[k]的插入排序
}//ShellSort
void ShellInsert(SqList &L, int dk){
for(i=dk+1;i <= L.length; ++i){
if(r[i].key < r[i-dk].key){
r[0] = r[i];
for(j=i-dk; j>0 &&(r[0].key < r[j].key); j = j-dk)
r[j+dk] = r[j];
r[j+dk] = r[0];
}
}
}
希爾排序算法效率與增量序列的取值有關
時間複雜度是n和d的函數:O(n²)~O(1.6n¹˙²⁵)
空間複雜度爲O(1)
是一種不穩定的排序方法
- 最後一個增值量必須爲1,無除了1之外的公因子
- 不宜在鏈式存儲結構上實現
交換排序
基本思想:
兩兩比較,如果發生逆序則交換,直到所有記錄都排好序爲止。
常見的交換排序方法:
1)冒泡排序;2)快速排序。
冒泡排序及改進——基於簡單交換思想
基本思想:每趟不斷將記錄兩兩比較,並按“前小後大”規則交換
//冒泡排序算法
void bubble_sort(SqList &L){
int m,i,j;
RedType x; //交換時臨時存儲
for(m=1;m<=n-1;m++){ //總共需m趟
for(j=1;j<=n-m;j++){
if(L.r[j].key > L.r[j+1].key){
x = L.r[j];
L.r[j] = L.r[j+1];
L.r[j+1] = x; //交換
} // endif
} //for
}
}
優點:每趟結束時,不僅能擠出一個最大值到最後面位置,還能同時部分理順其他元素;
如何提高效率?一旦某一趟比較時不出現記錄交換,說明已排好序了,就可以結束本算法。
//改進地冒泡排序
void bubble_sort (SqList &L){
int m,i,j,flag=1; //flag作爲是否有交換地比標記
RedType x;
for(m=1; m <= n-1 && flag == 1; m++){
flag = 0;
for(j=1; j<=m;j++){
iif(L.r[j].key > L.r[j+1].key){ //發生逆序
flag = 1; //發生交換,flag置爲1,若本趟沒發生交換,flag保持爲0
x = L.r[j];
L.r[j] = L.r[j+1];
L.r[j+1] = x; //交換
}
}//endif
}//for
}
- 冒泡排序最好時間複雜度是O(n)
- 冒泡排序最壞時間複雜度是O(n²)
- 冒泡排序平均時間複雜度是O(n²)
- 冒泡排序算法中增加了一個輔助空間temp,輔助空間爲S(n)=O(1)
- 冒泡排序是穩定的
快速排序——改進的交換排序
基本思想:
- 任取一個元素爲中心 (pivot:樞軸、中心點)
- 所有比它小的元素一律前放,比它大的元素一律後放
形成左右兩個子表; - 對各子表重新選擇中心元素並依次規則調整;(遞歸思想)
- 直到每個子表的元素只剩一個
通過一趟排序,將待排序記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄進行排序,以達到整個序列有序
具體實現:選定一箇中間數作爲參考,所有元素與之比較,小的調到其左邊,大的調到其右邊。(樞軸)中間數:可以是第一個數、最後一個數、最中間一個數、任選一個數等。
①每一趟的子表的形成是採用從兩頭向中間交替式逼近法;
②由於每趟中對各子表的操作都相似,可採用遞歸算法
void main(){
SqList L;
QSort (L,1,L.length);
}
void QSort(SqList &L,int low,int high){ //對順序表L快速排序
if(low < high){ //長度大於1
pivotloc = Partition(L,low,high); //中心點的位置
//將L.r[low...high] 一分爲二,pivotloc爲樞軸元素排好序的位置
QSort(L,low,pivotloc-1); //對低子表遞歸排序
QSort(L,pivotloc+1,high); //對高子表遞歸排序
}//endif
}//QSort
int Partition (SqList &L,int low,int high){
L.r[0] = L.r[low];
pivotkey = L.r[low].key;
while(low < high){
while(low < high && L.r[high].key >= pivotkey)
--high;
L.r[low] = L.r[high];
while(low < high&&L.r[low].key <= pivotkey)
++low;
L.r[high] = L.r[low];
}
L.r[low] = L.r[0];
return low ;
}
快速算法分析
- 時間複雜度
可以證明,平均計算時間是O(n㏒₂n)- Qsort(): O(log₂n)
- Partition(): O(n)
- 實驗結果表明:就平均計算時間而言,快速排序是我們所有討論的所有內排序方法中最好的一個。
- 空間複雜度
快速排序不是原地排序
由於程序中使用了遞歸,需要遞歸調用棧的支持,而棧的長度取決於遞歸調用的深度。(即使不用遞歸,也需要用用戶棧)- 在平均情況下:需要O(logn)的棧空間
- 最壞情況下:棧空間可達O(n)。
- 穩定性
快速排序是一種不穩定的排序方法。
快速排序不適用於對原本有序或基本有序的記錄序列進行排序。
例如:將序列(90,85,79,74,58,50,43)進行快速排序爲增序;將(43,50,58,74,79,85,90)進行排序爲降序。我們會發現由於每次樞軸記錄的關鍵字都是大於其他所有記錄的關鍵字,致使一次劃分之後得到的子序列(1)的長度爲0,這時已經退化成爲沒有改進措施的冒泡排序。
- 劃分元素的選取是影響時間性能的關鍵
- 輸入數據次序越亂,所選劃分元素值的隨機性越好,排序速度越快,快速排序不是自然排序方法。
- 改變劃分元素的選取方法,至多隻能改變算法平均情況的時間性能,無法改變最壞情況下的時間性能。即最壞情況下,快速排序的時間複雜性總是O(n²)
選擇排序
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理是:第一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然後再從剩餘的未排序元素中尋找到最小(大)元素,然後放到已排序的序列的末尾。以此類推,直到全部待排序的數據元素的個數爲零。選擇排序是不穩定的排序方法。
簡單選擇排序
基本思想:在待排序的數據中選出最大(小)的元素放在其最終的位置。
基本操作:
1.首先通過n-1次關鍵字比較,從n個記錄中找出關鍵字最小的記錄,將它與第一個記錄交換
2.再通過n-2次比較,從剩餘的n-1個記錄中找出關鍵字小的記錄,將它與第二個記錄交換。
3.重複上述操作,共進行n-1趟排序後,排序結束
//簡單選擇排序
void SelectSort(SqList &L){
int k,i,j;
for (i=1; i<L.length;++i){
k = i;
for(j=i+1;j<= L.length; j++)
if(L.r[j].key < L.r[k].key)
k = j;
if(k!=i){
SqList temp = L.r[i];
L.r[i] = L.r[k];
L.r[k] = temp;
}
}
}
時間複雜度O(n)
- 記錄移動次數
最好情況:0
最壞情況:3(n-1) - 比較次數:無論待排序列處於什麼狀態,選擇排序所需進行的“比較”次數都相同。
算法穩定性:
- 簡單選擇排序是不穩定排序
空間複雜度O(1)
堆排序
堆的定義:
若n個元素的序列{a₁ a₂ … an}滿足
則分別稱該序列{ a₁ a₂ … an }爲小根堆和大根堆。
從堆的定義可以看出,堆實質是滿足如下性質的完全二叉樹:二叉數中任一非葉子結點均小於(大於)它的孩子節點
若在輸出堆頂的最小值(最大值)後,使得剩餘n-1個元素的序列重又建成一個堆,則得到n個元素的次小值(次大值)…如此反覆,便能得到一個有序序列,這個過程稱之爲堆排序。
堆調整
如何在輸出堆頂元素後,調整剩餘元素爲一個新的 堆?
小根堆:
1.輸出堆頂元素之後,以堆中最後一個元素代替之;
2.然後將根節點值與左、右子數得根結點值進行比較,並與其中小者進行交換;
3.重複上述操作,直至葉子結點,將得到新的堆,稱這個從堆頂至葉子的調整過程爲“篩選”。
大根堆:
1.輸出堆頂元素之後,以堆中最後一個元素代替之;
2.然後將根節點值與左、右子數得根結點值進行比較,並與其中大者進行交換;
3.重複上述操作,直至葉子結點,將得到新的堆,稱這個從堆頂至葉子的調整過程爲“篩選”。
void HeapAdjust (elem R[],int s,int m){ //堆調整
/* 已知R[s...m]中記錄的關鍵字除R[s]之外均滿足堆得定義,本函數調整R[s]
的關鍵字,是R[s ... m] 成爲一個大根堆 */
rc = R[s];
for(j=2*s; j <= m ;j *= 2){ //沿key較大的孩子結點向下篩選
if(j < m && R[j] < R[j+1])
++j; //j爲key較大的記錄的下標
if(rc >= R[j])
break;
R[s] = R[j];
s = j; //rc應插入在位置s上
} //for
R[s] = rc;
} //HeapAdjust
可以看出:
對於一個無序序列反覆“篩選”就可以得到一個堆;
即:從一個無序序列建堆的過程就是一個反覆“篩選”的過程。
堆建立
我們知道:單結點的二叉樹是堆;
在完全二叉樹中所有以葉子結點(序號i>n/2)爲根的子樹是堆。
這樣,我們只需要依次將以序列爲n/2,n/2 - 1, …,1的結點爲根的子樹均調整爲堆即可。
即,對應由n個元素組成的無序序列,“篩選”只需從第n/2個元素開始。
由於堆實質上是一個線性表,我們可以順序存儲一個堆。
例如,有關鍵字爲49,38,65,97,76,13,27,49的一組記錄,將其按關鍵字調整爲一個小根堆。
首先,我們按順序先排列下來,如圖
從最後一個非葉子結點開始,以此向前調整:
①調整從第n/2個元素開始,將以該元素爲根的二叉樹調整爲堆。(n/2即該例中第4個即97,將97與其子結點進行比較,若大於其子結點則交換順序,如圖)
②將以序號爲n/2 - 1的結點爲根的二叉樹調整爲堆;(調整完後,該排第3號元素,即65,將其與左右孩子結點比較排序,如圖)
③再將以序號爲n/2 - 2的結點爲根的二叉樹調整爲堆;(繼續將第3號元素與其左右孩子結點比較排序)
④再將以序號爲n/2 - 3的結點爲根的二叉樹調整爲堆;(繼續將第2號元素與其左右孩子節點進行比較)
將初始無序的R[1]到R[n]建成一個小根堆,可用以下語句實現:
for(i = n/2;i >= 1; i-- )
HeapAdjust (R, i, n);
由以上分析可知:
若對一個無序序列建堆,然後輸出根;重複該過程就可以由一個無需序列輸出有序序列。
實質上,堆排序就是利用完全二叉樹中父結點與孩子結點之間的內在關係來排序的。
整體代碼如下:
//堆排序
void HeapAdjust (elem R[],int s,int m){ //堆調整
/* 已知R[s...m]中記錄的關鍵字除R[s]之外均滿足堆得定義,本函數調整R[s]
的關鍵字,是R[s ... m] 成爲一個大根堆 */
rc = R[s];
for(j=2*s; j <= m ;j *= 2){ //沿key較大的孩子結點向下篩選
if(j < m && R[j] < R[j+1])
++j; //j爲key較大的記錄的下標
if(rc >= R[j])
break;
R[s] = R[j];
s = j; //rc應插入在位置s上
} //for
R[s] = rc;
} //HeapAdjust
void HeapSort(elem R[]){ //對R[1]到R[n]進行堆排序
int i;
for(i = n/2;i >= 1;i--)
HeapAdjust( R, i ,n); //建初始堆
for(i = n;i > 1; i--){ //進行n-1趟排序
Swao (R[1],R[i]); //根與最後一個元素交換
HeapAdjust(R, 1,i-1); //對R[1]到R[i-1]重新建堆
}
}//HeapSort
-
初始化所需時間不超過O(n)
-
排序階段(不含初始堆化)
- 一次重新堆化所需時間不超過O(logn)
- n-1次循環所需時間不超過O(nlogn)
Tw(n) = O(n) + O(nlogn) = O(nlogn)
-
堆排序的時間主要耗費在建初始堆和調整建新堆時進行的反覆“篩選”上。堆排序在最壞情況下,其時間複雜度也爲O(nlog₂n),這是堆排序的最大優點。無論待排序中的記錄是正序還是逆序排列,都不會使堆排序處於“最好”或“最壞”的狀態。
-
另外,堆排序僅需一個記錄大小供交換用的輔助存儲空間。
-
堆排序是一種不穩定的排序方法,它不適用於待排序記錄個數n較少的情況,但對於n較大的文件還是很有效的。
堆排序的空間複雜度爲O(1)
歸併排序
基本思想:將兩個或兩個以上的有序子序列“歸併”爲一個有序序列。
- 在內部排序中,通常採用的是2-路歸併排序
- 即:將兩個位置相鄰的有序子序列R[l…m]和R[m+1…n]歸併爲一個有序序列R[l…n]
例:兩個相鄰的排序。
整個歸併排序僅需[log₂n]趟
關鍵問題:如何將兩個有序序列合成一個有序序列?
我們可以參考在數據結構所學的線性表中將兩個線性表合併的算法。
詳情請看我的另一個博客線性表的使用
在瞭解過線性表的基本操作後,我們看相鄰的兩個有序序列怎麼操作:
將 i 和 j 依次比較,
若SR[i].key <= SR[j].key,則TR[k] =RS[i]; k++;i++;
否則,TR[k] = SR[j];k++;j++;
- 時間效率:O(nlog₂n)
- 空間效率:O(n)
因爲需要一個與原始序列同樣大小的輔助序列(R1)。這正是次算法的缺點。 - 穩定性:穩定
基數排序
基本思想:分配+收集
也叫桶排序或箱排序:設置若干個箱子,將關鍵字爲k的記錄放入第k個箱子,然後在按序號將非空的鏈接。
基數排序:數字是有範圍的,均由0-9這十個數字組成,則只需設置十個箱子,相繼按個、十、百…進行排序。
基數排序算法分析
- 時間效率:O(k*(n+m))
k:關鍵字個數;
m:關鍵字取值範圍爲m個值 - 空間複雜度:O(n+m)
- 是穩定的
各種排序方法的綜合比較
一、時間性能
1.按平均的時間性能來分,有三類排序方法:
- 時間複雜度爲O(nlogn)的方法有:
- 快速排序、堆排序和歸併排序,其中以快速排序最好;
- 時間複雜度爲O(n²)的有:
- 直接插入排序、冒泡排序和簡單選擇排序,其中以直接插入爲最好,特別是對那些對關鍵字近似有序的記錄序列尤爲如此;
- 時間複雜度爲O(n)的排序方法只有:基數排序。
2.當待排記錄序列按關鍵字順序有序時,直接插入排序和冒泡排序能達到O(n)的時間複雜度;而對於快速排序而言,這是最不好的情況,此時的時間性能退化爲O(n²),因此是應該儘量避免的情況。
3.簡單選擇排序、堆排序和歸併排序的時間性能不隨記錄序列中關鍵字的分佈而改變。
二、空間性能
指的是排序過程中所需的輔助空間大小
1.所有的簡單排序方法(包括:直接插入、冒泡和簡單排序)和堆排序的空間複雜度爲O(1)
2.快速排序爲O(logn),爲棧所需的輔助空間
3.歸併排序所需輔助空間最多,其空間複雜度爲O(n)
4.鏈式基數排序需附設隊列首位指針,則空間複雜度爲O(rd)
三、排序方法的穩定性
- 穩定的排序方法指的是,對於兩個關鍵字相等的記錄,它們在序列中的相對位置,在排序之前和經過排序之後,沒有改變。
- 當對多關鍵字的記錄序列進行LSD方法排序時,必須採用穩定的排序方法。
- 對於不穩定的排序方法,只要能舉出一個實例說明即可。(相同的數在排完序後位置相對改變了)
- 快速排序和堆排序是不穩定的排序方法。
四、關於“排序方法的時間複雜度的下限”(即最壞情況)
- 我們討論的各種排序方法,除基數排序外,其它方法都是基於“比較關鍵字”進行排序的排序方法,可以證明,這類排序法可能達到的最快的時間複雜度爲O(nlogn)。
(基數排序不是基於“比較關鍵字”的排序方法,所以它不受這個限制) - 可以用一顆判定樹來描述這類基於“比較關鍵字”進行排序的排序方法。
聲明:此文章完全是博主個人用來鞏固數據結構知識點,所學習的筆記,其內容借鑑於青島大學王卓老師的網課資料。網課鏈接
在此非常感謝老師分享的內容,老師講的非常細緻到位。感謝🙇