一、基本概念
1.1增排序和減排序
按關鍵字從大到小或從小到大劃分。
1.2內部排序和外部排序
數據元素均在內存中即內部排序,否則則包含外部排序。
1.3穩定排序和不穩定排序
關鍵字相同的兩個元素,排序後相對位置發生變化即不穩定,否則即穩定。
1.4排序算法的評價指標
時間複雜度 和 空間複雜度。
二、插入排序
2.1基本思想
將待排序表看作左右兩個部分,左邊爲有序區,右邊爲無序區,整個排序過程就是將右邊無序區的元素逐個插入到有序區中,以構成有序區。主要介紹 直接插入排序和希爾排序。
2.2直接插入排序
現直接給出代碼和註釋:
void insertSort(elementType A[n+1]) {
for(int i = 2; i <= n; i++){ //I表示待插入元素的下標
A[0] = A[I]; //設置監視哨保存待插入元素,以騰出A[i]的空間
j = I - 1; //j表示當前空位置的前一個
while(A[j].key > A[0].key){ //搜索插入位置並騰出空位
A[j+1] = A[j];
j = j - 1;
}
A[j+1] = A[0]; //插入元素
}
}
算法分析:
1.穩定性:該算法爲穩定算法。
2.空間性能:該算法僅需要一個記錄的監視哨輔助空間。
3.時間性能:整個算法循環n-1次,每次循環中的基本操作爲比較和移動元素,一般情況下爲O(N^2).
2.3希爾排序(Shell Sort)
基本思想:將待排序的序列劃分爲若干組別,在每組內進行直接選擇插入排序,以使得整個序列基本有序,然後再對整個序列進行直接插入排序。
這種排序的關鍵在於選組。而我們所決定的選擇是將整個序列的長度的1/2在初始選擇爲步長。後面依次遞減1/2。
僞代碼:
void ShellSort(elementType A[n+1], int dh) { //dh means the ORIGINAL FOOTSTEP
while(dh>=1) {
for(I = dh + 1; I <= n; I++){
temp = A[I];
j = I;
while(j > d && temp.key<A[j-dh].key){
A[j] = A[j-dh];
j = j - dh;
}
A[j] = temp;
}
dh = dh/2;
}
}
算法分析:
希爾排序是分組插入排序,先按照規定將元素分組,同一組內採用直接插入排序。
對比希爾排序和直接插入排序,希爾排序除了分組循環外,其餘同插入排序幾乎完全一致,只是步長從1變爲了dh
1.該算法爲不穩定算法。
2.空間複雜度爲O(1)
3.時間複雜度爲O(nlog2N)
與分區方法有很大關係
性能優於直接插入排序,時間複雜度介於O(n)和O(n^2)之間,大致爲O(1.3)或O(1.5)
三、交換排序
兩兩比較待排序元素,發現倒序則交換。
3.1冒泡排序
逐個比較兩相鄰元素,發現倒序則交換。
典型做法是從後往前(從下往上)逐個比較相鄰2個元素,發現倒序則交換。
每次掃描一定能將當前最小/大的元素交換到最終位置,如同水泡冒出水面
僞代碼:
void bubbleSort(int A[]){
for(int I = 1; I < n; I++){
for(int j = n; j >= I + 1; j--){
if(A[j].key<A[j-1].key){
swap(A[j],A[j-1]; //SWAP in IOSTREAM
}
}
}
}
改進的冒泡排序
接下來考慮一種極端情況:序列本身就是有序。
此種情況下,依然將進行O(n^2)級別的掃描。
很明顯不夠划算。
因此,我們可以設置一種含有標誌是否已經交換完成的標誌。這樣作爲每次冒泡排序完成後是否還需要繼續的標誌。
因此得到的改進算法如下:
void bubbleSort(int A[n+1]) {
I = 1;
do{
exchanged = FALSE; // As a sign of Exchanged or not
for(j = n; j >= I + 1; j--) {
if(A[j].key < A[j-1].key){
swap(A[j],A[j-1]);
exchanged = TRUE;
}
}
I++;
}while(I<=n-1&&exchanged == TRUE);
}
算法分析:
穩定性:穩定排序
空間複雜度:O(1)的輔助空間
時間複雜度:
受到數據表初始狀態影響大。
最好情況:正序 比較n-1次,交換 0 次, 時間複雜度O(n)
最壞情況:全部逆序 比較與交換 均爲 n*(n-1)/2;
一般:O(n^2)
3.2快速排序
3.2.1基本思想:分治法。
選定一個元素作爲中間元素,然後將表中所有元素與之比較:
比其小的放在表的前面;
比其大的放在表的後面;
該元素放在兩部分中間做劃分,這就是其最終位置。
這樣就可以得到一個劃分(二分)
然後對左右子表再分別進行劃分。
快速排序通過一趟排序將排序序列分成左右兩部分,使得左邊任意元素均不大於/小於右邊任意元素,並將中間元素放到最終位置。
3.2.2操作方法
選擇第一個元素作爲中間元素
1.先保存該元素到其他位置,騰出該位置。
2.從後往前掃描一個比中間數小的元素,並將其放置到(1)中的空位置上,此時後面空出一個位置。
3.從前往後掃描一個比中間數大的元素,並將其放置到(2)中的空位置上,此時前面空出一個位置。
重複2、3直到兩邊掃描到的空位重合,此時將中間元素放在空位中。
3.3.3算法設計
分區算法
1.保存中間元素的值到臨時變量x以騰出空間,並且用low指向該元素,即x = A[low];
2.從後往前搜索比這個數字小的元素,並將其放在空位上,從而在後面騰出一個位置(high指向)
3.從前往後掃描到比這個數字大的元素,將其放置在(2)中的high上,從而使得前面空出一個位置(low指向)
重複2、3直到兩邊掃描的位置重合(low==high,即在該空位前沒有更大的元素,此後沒有更小的元素)因而可以將中間元素放在此位置,該元素歸位。
void Partition(int A[], int low,int high, int &mid) {
//low 分區的第一個元素下標,high 作爲最後一個元素下標
//mid爲中間元素
A[0] = A[low];
while(low < high) {
//A[high] >= mid元素則不交換,high左移
while(low < high && A[high].key >= A[0].key) high--;
//右區間遇到第一個小於mid的元素,移動到 A[low]
//此時A[low]的元素已經取到A[0]
//同時A[high]已經移動,其爲空位置,可以存放其他數據
A[low] = A[high];
//A[low]<= mid 元素,則不交換,low右邊移動
while(low < high && A[low].key <= A[0].key) low++;
//左區間遇到第一個大於此中間元素的值,移動到 A[high]
//此時A[high]空
A[high] = A[low];
}
//此時low == high 爲目標的空位置
A[low] = A[0];//將中間元素移動到目標位置
mid = low; //返回本次中間值的最終位置
}
快速排序即用到上述的分區算法
void QuickSort(int A[n], int low, int high){
int mid; // mid 由Partition函數給出
if(low <high){
Partition(A,low,high,mid);
QuickSort(A,low,mid-1);
QuickSort(A,mid+1,high);
}
}
算法分析:
1.穩定性:不穩定排序
2.空間複雜度:需要一個輔助空間
3.時間複雜度:
理想情況:每次選擇元素正好兩等份子表。整個算法複雜度爲O(nlog2N)
最壞情況:每次選擇的元素恰爲最大/最小。即需要(n-1)次劃分,掃描(n-i+1)次。整個複雜度爲O(n^2)
一般情況:O(K*nlog2^N)
分析可得:劃分中中間元素的選擇非常重要,因此改進選擇爲:比較子表第一個、最後一個、中間元素。選取中值作爲樞紐元素。
而快排目前也被認爲是內部排序最優解之一。
四、選擇排序
基本思想:在每次排序中選出關鍵字最小/最大的元素放在最終位置。
4.1簡單(直接)選擇排序
通過在待排序子表中完整的比較一遍以確定最值元素,並將該元素放在子表的最前/後面。
void SelectSort(int A[],int n) {
//1~n
for(int i = 1; i < n; i++) {
int min = i;
for(int j = i + 1; j < n; j++) {
if(A[j] < A[min])
min = j;
if(min!=i) {
swap(A[min],A[i]);
}
}
}
}
算法分析:
穩定性:不穩定排序。
空間複雜度:需要一個額外空間。O(1)
時間複雜度:
共比較n*(n-1)/2次
最多交換n-1次,一趟最多交換1次
O(n^2)
4.2堆排序
4.2.1堆及其基本概念
堆實際上是一棵完全二叉樹
·若其每個結點均不大於其左右孩子的值,稱爲小根堆(根結點的值最小)
·若其每個結點均不小於其左右孩子的值,稱爲大根堆(根結點的值最大)
可見,若某序列爲堆,其堆頂必爲序列中的最大值或最小值。
堆排序的基本思想:
假設要求遞增排序且已有一個大根堆
1.輸出根
2.用二叉樹的最後一個結點替代根,重新調整堆(待排序元素-1)
3.重複上述直到輸出全部結點。
可見,要解決兩個問題:
一是如何建立初始堆、二是輸出根後如何調整堆。
4.2.2堆的篩選(調整)
1.輸出根,用二叉樹最後一個結點代替新的根。
2.調整堆,此時,除了跟結點和其左右孩子違反條件外,其餘左右子樹仍然滿足條件。即整個序列不是堆,但其左右子樹仍然是堆。
如何調整:
1.由於其左右子樹是堆,此時左右孩子結點的值分別是兩個子樹中的最大值。因此,新的堆頂只可能從當前根點、其左右孩子中產生,故可以比較這三者得到。
2.如果當前根結點已經是最大值,即已經是堆,則無需調整;否則將左右孩子中的最大值與根對換。
但是調整之後可能違反子樹中堆的大小,因此需要在執行調換的子樹中繼續進行。
算法設計:
1.保存臨時根的值到一個變量(設爲x)用i標記該結點。
2.比較i結點的左右孩子和x的最大值:
2.1 i結點沒有左右孩子,即已經到達葉子結點。將x填到i結點中。
2.2 i結點的左右孩子的值小於x的值,表示搜索到了填充位置,將x填入i結點中。
2.3 否則將左右孩子中的最大填充在i結點中,從而出現新的空位,因此,同樣用i指示,並且轉2.2繼續執行。
整理可得 所需參數:
調整中,堆頂的下標不一定爲1,因此需要將堆頂的下標作爲參數---K,輸出根之後,參與運算的元素個數減一,因此,需要將當前序列的元素個數作爲參數---M,加上數組參數A[].
void sift(int A[], int k, int m) {
//調整以K爲根的子樹序列爲堆
//其中K爲子樹根,M爲最大元素編號
//假設以2K和2K+1爲根的左右子樹均爲堆
int x = A[k]; //臨時保存當前根值,空出位置
bool finished = false;//設置未結束標誌
int i = k; //i指示空位,子樹根
int j = 2*i; //j指向k的左孩子結點
while(j<=m && !finished) {
//確定i結點不是葉子且未搜索結束
if(j < m && A[j] < A[j + 1])
j = j +1;//找出i左右孩子中的最大者,用j指向
if(x>=A[j])
finished = true;
//根值最大,無需再調整,結束標誌置真
else {
A[i] = A[j]; //最大值A[j]上升爲樹根
i = j; //跟新子樹根i爲j繼續調整j以下的子樹爲堆
j = 2 * j; //繼續下篩,i仍爲子樹樹根,j指向其左孩子結點
}
}
A[i] = x; //循環結束i即爲x的最終位置,使得K爲根的子樹爲大根堆
}
從N/2開始從右往左、自下而上逐棵子樹調整。
建立初堆:
for(int I = n/2; I>=1;i--){
sift(A,i,n);
}
堆排序:
void HeapSort(int A[],int n){
int i;
//初建堆--由初始序列產生堆(此處爲大根堆)
//從第n/2結點開始往上篩,
//直到1號結點(根、堆頂)
for(i = n/2; i>=1;i--) {
sift(A,i,n);
//每次調用此函數,
//都將以i爲根結點的子樹調整爲堆。
}//由堆序列產生排序序列,
//此時整棵樹(完全二叉樹)爲堆(此處爲大根堆)
for(i=n;i>=2;i--)
{
A[0]=A[i]; //完全二叉樹最後一個結點保存到A[0],
//空出位置i輸出根A[1],即當前子樹的根(堆頂)
A[i]=A[1]; //輸出根,即A[1]保存到排序後的最終位置i
A[1]=A[0]; //原第i元素暫作爲“根”。
//又A[1]=A[0]後可能破壞了當前樹的堆屬性,
//需要從根結點1開始重新調整爲堆
//因爲輸出根,此時樹的結點數爲i-1。
sift(A,1,i-1);
}
}
算法分析:
穩定性:不穩定。
空間複雜度:需要一個輔助空間,O(1)
時間複雜度:
主要花費在建立初堆和調整堆上。
高度爲h的堆,篩選算法中所進行的關鍵比較次數最多爲2(h-1)次。
h=floor(log2n)+1;
即最多爲log2N次
堆排序共調用篩選n-1次;建立初堆共調用篩選n/2次。
總複雜度爲O(nlog2n)
五、歸併排序
歸併排序先設法將原序列劃分爲只含有1個元素的子表(視爲有序)
然後反覆選擇兩個有序子表進行合併直到合併後的序列長度爲n
歸併算法基於兩個基本操作:劃分和合並
劃分操作將1個未排序序列劃分成2個更短的子序列。
歸併操作將2個或者多個有序子序列合併成1個更長的有序序列。
歸併排序可以分爲:
·自頂向下的
·自底向上的
歸併排序同快速排序一樣,都是分治法的典型應用。
5.1歸併
(同線性表一樣的三情況分情況討論)
void merge(int A[],int B[],int C[],int la, int lb, int lc) {
//非降序數組A,B前la,lb個元素合併到C 並且保持其次序
int ia = 1, ib = 1, ic = 1;
while(ia <= la && ib <= lb)
if(A[ia]<=B[ib])
C[ic++] = A[ia++];
else
C[ic++] = B[ib++];
while(ia <= la)
C[ic++] = A[ia++];
while(ib<=lb)
C[ic++] = B[ib++];
}
算法分析:
對於A和B均是一遍掃描,整個時間複雜度爲O(|A| + |B|)
而歸併排序中歸併的兩個字序列要放在同一個表A中,因此要通過元素下標參數對兩個字表進行定界。
通過三個參數low、mid、high來確定2個有序子序列。
第一個子序列放在A[low~mid]
第二個子序列放在A[mid+1~high]
此外,歸併中要把歸併後的元素放在一個臨時表T中,T的大小與A相同歸併完成後,再將T中的元素複製到A中。
Merge函數需要4個參數:
A[]存放元素序列 low序列第一個元素下標 high序列最後一個元素下標 mid劃分點下標
改造後的歸併序列:
void Merge(int A[], int low, int mid, int high) {
int T[10005];
int i, j, k;
//i作爲low~mid的下標 j作爲mid+1~high的下標 k作爲T的下標
i = low;
k = low;
j = mid + 1;
while(i<=mid && j<=high) {
//A兩個子表都有元素
if(A[i] <= A[j]) {//A[i]較小
T[k] = A[i];
i++;
}else {
T[k] = A[j];
j++;
}
k++;
}
//處理一個表結束,另一個尚未結束的場景
while(i<=mid) {
T[k] = A[i];
i++;
k++;
}
while(j<=high) {
T[k] = A[j];
j++;
k++;
}
//複製回原表
memcpy(A,T, sizeof(T));
}
自底向上
·將原序列視爲(劃分爲)n個有序子序列,子序列長度爲1,每個子表只有一個元素;
·當子序列長度小於N的時候,循環選擇2個相鄰有序子序列,歸併