資料結構第10章 排序
文章目錄
插入排序
每次將一個待排序的記錄,按其關鍵字大小插入到前面已經排好序的子表中的適當位置,直到全部記錄插入完成為止。
直接插入排序
整個排序過程為n-1趟插入,即先將序列中第1個記錄看成是一個有序子序列,然後從第2個記錄開始,逐個進行插入,直至整個序列有序。
void InsertSort(SqList &L)
{
int i,j;
for(i=2;i<=L.length; i++)
{ //將L.R[i]插入有序子表
if( L.R[i].key<L.R[i-1].key)
{
L.R[0]=L.R[i]; // 複製為哨兵
j = i-1;
do{
L.R[j+1]=L.R[j]; // 記錄後移
j--;
}while(L.R[0].key>=L.R[j].key))
L.R[j+1]=L.R[0]; //插入到正確位置
}
}
}
時空複雜度和分析
最好的情況(關鍵字在記錄序列中正序
):
“比較”的次數:
∑
i
=
1
n
−
1
1
=
n
−
1
\sum_{i=1}^{n-1} 1=n-1
∑i=1n−11=n−1
“移動”的次數:0
最壞的情況(關鍵字在記錄序列中逆序
有序):
“比較”的次數:
∑
i
=
1
n
−
1
i
=
n
(
n
−
1
)
/
2
\sum_{i=1}^{n-1} i=n(n-1)/2
∑i=1n−1i=n(n−1)/2
“移動”的次數:
∑
i
=
1
n
−
1
(
i
+
2
)
=
(
n
+
4
)
(
n
−
1
)
/
2
\sum_{i=1}^{n-1} (i+2)=(n+4)(n-1)/2
∑i=1n−1(i+2)=(n+4)(n−1)/2
- 改進:在插入第 i(i>1)個記錄時,前面的 i-1 個記錄已經排好序,則在尋找插入位置時,可以用二分查詢來代替順序查詢,從而減少比較次數。這就是折半插入排序。
- 直接插入排序在基本有序時,效率較高
- 在待排序的記錄個數較少時,效率較高
希爾(Shell)排序
先將整個待排記錄序列分割成若干子序列,各個子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行一次直接插入排序。
將相距d個位置的記錄分為一組, n 個記錄被分成 d 個子序列,d 稱為增量,增量d的值在排序過程中從大到小逐漸縮小,直至最後一趟排序減為 1。所以希爾排序也稱為縮小增量排序。
時空複雜度和分析
時間複雜度平均情況: O ( n 1.3 ) O(n^{1.3}) O(n1.3)
如何選擇最佳d序列,目前尚未解決;但最後一個增量值必須為1
不宜在鏈式儲存結構上實現
交換排序
氣泡排序
void Bubble-sort(SqList &L)
{
int i, j, swap; // 當swap為0則停止排序
for ( i=1; i<L.length; i++) // i 表示趟數,最多n-1趟
{
swap=0; // 開始時元素未交換
for ( j=1; j<=L.length-i; j++)
if (L.R[j].key>L.R[j+1].key) // 發生逆序
{ L.R[0]=L.R[j]; L.R[j]=L.R[j+1]; L.R[j+1]=L.R[0];
swap=1;
} // 交換,並標記發生了交換
if(swap==0) break;
}
}
時空複雜度和分析
最好的情況(關鍵字在記錄序列中正序
):
“比較”的次數:
∑
i
=
1
n
−
1
1
=
n
−
1
\sum_{i=1}^{n-1} 1=n-1
∑i=1n−11=n−1
“移動”的次數:0
最壞的情況(關鍵字在記錄序列中逆序
有序):
“比較”的次數:
∑
i
=
1
n
−
1
i
=
n
(
n
−
1
)
/
2
\sum_{i=1}^{n-1} i=n(n-1)/2
∑i=1n−1i=n(n−1)/2
“移動”的次數:
∑
i
=
1
n
−
2
3
(
n
−
i
−
1
)
=
3
n
(
n
−
1
)
/
2
\sum_{i=1}^{n-2}3 (n-i-1)=3n(n-1)/2
∑i=1n−23(n−i−1)=3n(n−1)/2
改進:在氣泡排序中,記錄的比較和移動是在相鄰單元中進行的,記錄每次交換隻能上移或下移一個單元,因而總的比較次數和移動次數較多。
快速排序
- 快速排序首先選一個基準值(即比較的基準),每趟使表的第1個元素放入適當位置(歸位),將表一分為二,前一部分記錄的關鍵碼均小於或等於基準值,後一部分記錄的關鍵碼均大於或等於基準值對;
- 子表按遞迴方式繼續這種劃分,直至劃分的子表長為0或1(遞迴出口)。
void Quick_Sort(SqList &L,int s,int t) /* 對R[s]到R[t]的元素進行排序 */
{ if (s<t) //至少有兩個元素
{ int i=Partition(L,s,t);
Quick_Sort(L,s,i-1);
Quick_Sort(L,i+1,t);
}
}
int Partition(SqList &L,int low,int high)
{ L.R[0]= L.R[low]; /* 暫存基準值元素到R[0]中*/
while(low<high) /* 從表的兩端交替地向中間掃描 */
{ while( low<high&&L.R[high].key>=L.R[0].key )high--;
if(low<high) {L.R[low]= L.R[high]; low++; }
while( low<high&&L.R[low].key<L.R[0].key ) low++;
if (low<high) {L.R[high]= L.R[low]; high--; }
}
L.R[low]= L.R[0]; /* 將基準值元素放到其最終位置 */
return low; /* 返回基準值元素所在的位置*/
}
時空複雜度和分析
最好情況時間複雜度為
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n),空間複雜度為
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)。
最壞情況時間複雜度為
O
(
n
2
)
O(n^2)
O(n2),空間複雜度為
O
(
n
)
O(n)
O(n)。
平均時間複雜度為
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n)。
穩定性:不穩定。
選擇排序
簡單選擇排序(或稱直接選擇排序)
每一趟在後面 n-i+1箇中選出關鍵碼最小的物件, 作為有序序列的第 i 個記錄。
時空複雜度和分析
最好情況時間複雜度為
O
(
n
2
)
O(n^2)
O(n2)。
最壞情況時間複雜度為
O
(
n
2
)
O(n^2)
O(n2)。
空間複雜度為
O
(
1
)
O(1)
O(1)。
樹形選擇排序(錦標賽排序)
簡單選擇排序慢的原因?
用直接選擇排序從n個記錄中選出關鍵字值最小的記錄要做n-1次比較,然後從其餘n-1個記錄中選出最小者要作n-2次比較。顯然,相鄰兩趟中某些比較是重複的。
樹形選擇排序:首先對n個關鍵字進行兩兩比較,然後在其⌈?/2⌉個較小者之間再進行兩兩比較,如此重複,直至選出最小關鍵字為止。這個過程可用一棵有n個葉結點的完全二叉樹表示。
堆排序(Heap Sort)
堆的概念
n個元素的序列
{
k
1
,
k
2
,
…
…
,
k
n
}
\{ k_1, k_2 ,…… , k_n \}
{k1,k2,……,kn},當且僅當滿足下面條件(以大根堆為例)稱之為堆。
{
k
i
≥
k
2
i
k
i
≥
k
2
i
+
1
\left\{\begin{array}{l} k _{ i } \geq k _{2 i } \\ k _{ i } \geq k _{2 i +1}\end{array}\right.
{ki≥k2iki≥k2i+1
(
i
=
1
,
2
,
…
,
⌊
n
/
2
⌋
)
(i=1,2, \ldots,\lfloor n / 2\rfloor)
(i=1,2,…,⌊n/2⌋)
若將此關鍵字序列按順序組成一棵完全二叉樹,則堆可以如下定義:
或每個結點的值都大於或等於其左右孩子結點的值(稱為大根堆或大頂堆)。
堆排序流程:
- 將無序序列建成一個堆;
- 輸出堆頂的最小(大)值;
- 使剩餘的n-1個元素又調整成一個堆,返回步驟2;
篩選或調整演算法
對一棵左右子樹均為堆的完全二叉樹,“調整”根結點使整個二叉樹也成為一個堆。
- s指向當前根節點,將根節點存入暫存區tmp;
- i指向s孩子中key較大的;
- 若tmp.key>=i.key,s=tmp,退出;否則s=i,返回步驟2。
void HeapAdjust(SqList &L,int s,int m)
{ //假設R[s+1..m]已經是堆,將R[s..m]調整為以R[s]為根的大根堆
RecType tmp=L.R[s];
for(int i=2*s ; i<=m ; i*=2) //沿key較大的孩子結點向下篩選
{ if(i<m && L.R[i].key<L.R[i+1].key) i++; //i為key較大的記錄的下標
if( tmp.key>=L.R[i].key )
break;//雙親大:不再調整,temp應插在位置s上
L.R[s]=L.R[i];//將R[j]調整到雙親結點位置上
s=i; //修改s值,以便繼續向下篩選
}
L.R[s]=tmp; //插入
}
無序序列建成一個初始堆
for (i=n/2;i>=1;i--)
HeapAdjust(L.R,i,n);
堆排序演算法
void HeapSort(SqList &L)
{ int i; RecType tmp;
for (i=n/2;i>=1;i--) //迴圈建立初始堆
HeapAdjust(L.R,i,n);
for (i=n; i>=2; i--) //進行n-1次迴圈,完成堆排序
{ temp=L.R[1]; //堆頂歸位,R[1] R[i]
L.R[1]=L.R[i];
L.R[i]=tmp;
HeapAdjust(L.R,1,i-1);//調整剩餘記錄,篩選R[1]結點,得到i-1個結點的堆
}
}
時空複雜度和分析
設有n個記錄的初始序列對應的完全二叉樹的深度為
h
=
⌊
log
2
n
⌋
+
1
h =\left\lfloor\log _{2} n\right\rfloor+1
h=⌊log2n⌋+1,每個非終端結點都要自上而下進行“篩選”。由於第i層上的結點數小於等於
2
i
−
1
2^{i-1}
2i−1,且第i層結點最大下移的深度為h-i,每下移一層要做兩次比較,所以建初堆時關鍵字總的比較次數為
∑
i
=
h
−
1
1
2
i
−
1
⋅
2
(
h
−
i
)
≤
4
n
\sum_{i=h-1}^{1} 2^{i-1} \cdot 2(h-i) \leq 4 n
∑i=h−112i−1⋅2(h−i)≤4n
調整“堆頂”要做n-1 次“篩選”,每次“篩選”都要將根結點下移到合適的位置,比較2(h-1)次。n 個關鍵字的完全二叉樹的深度為
⌊
log
2
n
⌋
+
1
\lfloor \log_2n\rfloor+1
⌊log2n⌋+1,則重建堆時關鍵字總的比較次數不超過:
2
(
log
2
(
n
−
1
)
⌋
+
⌊
log
2
(
n
−
2
)
⌋
+
…
+
log
2
2
)
<
2
n
(
log
2
n
⌋
)
\left.\left.2\left(\log _{2}( n -1)\right\rfloor+\left\lfloor\log _{2}( n -2)\right\rfloor+\ldots+\log _{2} 2\right)<2 n \left(\log _{2} n \right\rfloor\right)
2(log2(n−1)⌋+⌊log2(n−2)⌋+…+log22)<2n(log2n⌋)
因此,堆排序的時間複雜度為
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n)。
空間複雜度為
O
(
1
)
O(1)
O(1)。
穩定性:不穩定。
歸併排序
2-路歸併排序
- 初始序列看成n個有序子序列,每個子序列長度為1,兩兩合併,得到 ⌊ n / 2 ⌋ \lfloor n/2\rfloor ⌊n/2⌋ 個長度為2或1的有序子序列;
- 再兩兩合併,重複直至得到一個長度為n的有序序列為止。
void Merge( SqList &L,int low,int mid,int high)
{
SqList L1; L1.length = high-low+1;
int i=low, j=mid+1, k=0;//k是L1.R的下標,i、j分別為第1、2路的下標
while ( i<=mid && j<=high )
if (L.R[i].key<=L.R[j].key) //將關鍵字值小的記錄放入L1中
{ L1.R[k]=L.R[i]; i++;k++; }
else { L1.R[k]=L.R[j]; j++;k++; }
while (i<=mid) //如果第1路還有剩餘記錄,將其餘下部分複製到L1
{ L1.R[k]=L.R[i]; i++;k++; }
while (j<=high) //如果第2路還有剩餘記錄,將其餘下部分複製到L1
{ L1.R[k]=L.R[j]; j++;k++; }
for (k=0,i=low;i<=high; k++,i++)
L.R[i]=L1.R[k];//將歸併後的記錄複製回L中
}
void MergePass(SqList &L,int m)
{ for (int i=0;i+2*m<L.length;i=i+2*m) //歸併長為m的兩相鄰子表
Merge(L,i,i+m-1,i+2*m-1);
if (i+m-1<L.length) //還剩下兩個子表,第1段長度為m,第2段長度小於m
Merge(L,i,i+m-1,L.length-1); //歸併剩餘的這兩個子表
}
時空複雜度和分析
最好情況時間複雜度為
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n)。
最壞情況時間複雜度為
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n)。
空間複雜度為
O
(
n
)
O(n)
O(n)。
穩定性:穩定。
排序小結
為避免順序儲存時大量移動記錄的時間開銷,可考慮用連結串列作為儲存結構:直接插入排序、歸併排序、基數排序
不宜採用連結串列作為儲存結構:折半插入排序、希爾排序、快速排序、堆排序
排序演算法選擇規則
- n較大時
(1)分佈隨機,穩定性不做要求,則採用快速排序
(2)記憶體允許,要求排序穩定時,則採用歸併排序
(3)可能會出現正序或逆序,穩定性不做要求,則採用堆排序或歸併排序 - n較小時
(1)基本有序,則採用直接插入排序
(2)分佈隨機,則採用簡單選擇排序,若排序碼不接近逆序,也可以採用直接插入排序
相關文章
- 資料結構與排序資料結構排序
- Python資料結構與演算法_第6節_排序 & 搜尋Python資料結構演算法排序
- 資料結構32:選擇排序資料結構排序
- 資料結構之計數排序資料結構排序
- 資料結構(python) —— 【18排序: 桶排序】資料結構Python排序
- 資料結構--排序--插入排序--python語言描述資料結構排序Python
- 資料結構與演算法——排序資料結構演算法排序
- 資料結構第九節(排序(上))資料結構排序
- 資料結構基礎第3講資料結構
- 資料結構基礎第4講資料結構
- 資料結構與演算法(八):排序資料結構演算法排序
- 資料結構與演算法之排序資料結構演算法排序
- 資料結構與演算法:堆排序資料結構演算法排序
- 資料結構:歸併排序(非遞迴)資料結構排序遞迴
- 資料結構學習筆記-堆排序資料結構筆記排序
- 【資料結構與演算法】堆排序資料結構演算法排序
- 資料結構與演算法——堆排序資料結構演算法排序
- 資料結構與演算法----# 一、排序資料結構演算法排序
- (戀上資料結構筆記):計數排序、基數排序 、桶排序資料結構筆記排序
- 【PHP資料結構】插入類排序:簡單插入、希爾排序PHP資料結構排序
- [資料結構拾遺]字串排序演算法總結資料結構字串排序演算法
- 資料結構與演算法整理總結---排序 2資料結構演算法排序
- 【資料結構與演算法】非比較排序(計數排序、桶排序、基數排序)資料結構演算法排序
- 資料結構與演算法 基礎排序資料結構演算法排序
- 資料結構與演算法 進階排序資料結構演算法排序
- 資料結構:快速排序程式碼(已優化)資料結構排序優化
- Java資料結構與排序演算法 (三)Java資料結構排序演算法
- Java資料結構與排序演算法 (一)Java資料結構排序演算法
- Java資料結構與排序演算法 (二)Java資料結構排序演算法
- 高階資料結構---堆樹和堆排序資料結構排序
- 資料結構與演算法-反轉排序資料結構演算法排序
- 【資料結構與演算法】歸併排序資料結構演算法排序
- 資料結構初階--堆排序+TOPK問題資料結構排序TopK
- 資料結構與演算法之快速排序資料結構演算法排序
- 資料結構與演算法——排序演算法-歸併排序資料結構演算法排序
- 資料結構與演算法——排序演算法-基數排序資料結構演算法排序
- 資料結構與演算法——排序演算法-氣泡排序資料結構演算法排序
- 資料結構與演算法——排序演算法-選擇排序資料結構演算法排序