資料結構(7)—— 排序
寫在前面
雖然這是課本以及教輔書的最後一章,但我相信資料結構起碼半年內是不會離開我了。迴歸正題,這是排序演算法。排序演算法歷來也是考試中的重點與難點,不僅要求實現過程,也會要求程式碼。因此這裡對常見的內部排序演算法進行實現,外部排序盡做了了解,並沒有敲程式碼。
按照慣例,上一節地址:資料結構(6)—— 查詢
插入排序
這部分主要包括直接插入排序、折半插入排序與希爾排序,其中希爾排序的時間複雜度是最優的,且三種演算法的空間複雜度均是O(1),即常數個空間。
注意點
這裡直接把這倆非常簡單的演算法拿來直接說了。折半插入就是在尋找被插入位置的時候使用了折半查詢,實現的難度並不大。需要注意的是在我們之前實現的折半查詢中的判斷條件和這裡不一樣的。這裡為了確保穩定性,當相等的時候會查詢右字表,這樣就可以保證演算法的穩定性。
關於希爾排序,這裡實現的是非常簡單的方法,以長度的一半為初始的增量,然後不斷進行縮小。
程式碼
/* * @Description: 插入排序演算法 空間複雜度均為O(1) * @version: 1.0 * @Author: Liuge * @Date: 2021-07-31 21:01:25 */ #include<bits/stdc++.h> // 直接插入排序(帶哨兵) 預設排序成升序 時間複雜度:O(n2) 穩定排序演算法 void insertSort(int A[],int n){ int i; int j; // 比較次數 int count; for(i = 2;i <= n;i++){ count++; if(A[i] < A[i-1]){ // 把0這個位置空出來當哨兵 A[0] = A[i]; // 查詢插入位置,然後整體後移 for(j = i - 1;A[0] < A[j];--j){ count++; A[j+1] = A[j]; } // 插入 A[j+1] = A[0]; } } printf("直接插入排序比較次數為:%d\n",count); } // 折半插入排序(帶哨兵) 升序 時間複雜度:O(n2) 穩定排序演算法 void insertSort2(int A[],int n){ int i,j,low,high,mid; int count; for(i = 2;i<=n;i++){ A[0] = A[i]; // 設定折半查詢的範圍 low=1; high = i - 1; // 當low > high時停止 // 與折半查詢不同的是,為了保證演算法的穩定性,這裡在相等時不停止,而是繼續查詢右半部分 while(low <= high){ mid = (low + high) / 2; count++; if(A[mid] > A[0]){ high = mid - 1; }else{ low = mid + 1; } } // 後移元素 for(j = i-1;j >= high + 1;--j){ A[j+1] = A[j]; } // 插入 A[high+1] = A[0]; } printf("折半插入排序比較次數為:%d\n",count); } // 希爾排序 升序 時間複雜度:當n在某個範圍時為O(n1.3) 不穩定排序演算法 void shellSort(int A[],int n){ // 這裡的A[0]只是暫存單元,並不是哨兵,當j<=0時,插入位置已到 int dk,i,j; int count; // 步長變化 for(dk = n / 2;dk >= 1;dk = dk / 2){ // 對每個子表進行直接插入排序 for(i = dk + 1;i <= n;i++){ count++; if(A[i] < A[i-dk]){ A[0] = A[i]; for(j = i - dk;j > 0 && A[0] < A[j];j -= dk){ count++; A[j + dk] = A[j]; } A[j + dk] = A[0]; } } } printf("希爾排序比較次數:%d\n",count); } // 生成隨機數的函式 int getRand() { // 設定隨機數範圍1-100 return rand() % 100 + 1; } // 生成隨機數的陣列 void getRandArray(int A[]){ // 隨機生成十個隨機數 for(int i = 1;i <= 10;i++){ A[i] = getRand(); } } // 陣列輸出函式 void printArray(int A[]){ for(int i = 1;i <= 10;i++){ printf("%d ",A[i]); } printf("\n"); } // 主函式測試 int main(){ // 以系統時間為種子 srand((unsigned)time(NULL)); int A1[11],A2[11],A3[11]; getRandArray(A1); getRandArray(A2); getRandArray(A3); // 打印出這十個數 printf("原始排序序列為:\n"); printArray(A1); // 進行直接插入排序 insertSort(A1,10); printf("直接插入排序後:\n"); printArray(A1); printf("原始排序序列為:\n"); printArray(A2); insertSort2(A2,10); printf("折半插入排序後:\n"); printArray(A2); printf("原始排序序列為:\n"); printArray(A3); shellSort(A3,10); printf("希爾排序後:\n"); printArray(A3); return 0; }
交換排序
這部分主要包括氣泡排序和快速排序。其中氣泡排序的時間複雜度會達到O(n2),但跟初始序列是有關的,而快排的平均時間複雜度能到O(nlog2n),還是十分快的。
注意點
氣泡排序十分簡單,這裡就不再多說。只需要兩兩對比,不斷把最大或最小的元素“冒”到最終位置即可。
快速排序是一種基於分治法的演算法,實際上快排也是我們要學習的各種演算法中最快的一個了。快排將待排序的序列分成兩塊,確定一個樞軸,通過跟這個樞軸進行對比,來不斷交替搜尋和交換,最後達到有序。
程式碼
/* * @Description: 交換排序(氣泡排序和快速排序) * @version: 1.0 * @Author: Liuge * @Date: 2021-08-02 20:12:44 */ #include<bits/stdc++.h> // 交換順序 void swap(int &m,int &n){ int temp = m; m = n; n = temp; } // 氣泡排序 升序 時間複雜度:O(n2) 穩定 void bubbleSort(int A[],int n){ int flag; int count; for(int i = 0;i< n - 1;i++){ flag = false; // 一趟冒泡過程 for(int j = n - 1;j > i;j--){ // 逆序則交換 if(A[j-1] > A[j]){ swap(A[j-1],A[j]); count++; flag = true; } } // 沒有進行交換,說明全都有序了,直接跳出 if(flag == false){ printf("氣泡排序交換了 %d 次\n",count); return; } } printf("氣泡排序交換了%d次\n",count); } // 快速排序的分割槽函式 int partition(int A[], int low,int high){ // 將表中第一個元素作為樞軸,對錶進行劃分 int pivot = A[low]; while (low < high){ while(low < high && A[high] >= pivot){ high--; } A[low] = A[high]; while(low < high && A[low] <= pivot){ low++; } A[high] = A[low]; } A[low] = pivot; return low; } // 快速排序 升序 時間複雜度(nlog2n) 不穩定 遞迴 空間複雜度O(log2n) void quickSort(int A[],int low,int high){ if(low < high){ int pivotPos = partition(A,low,high); // 遞迴排序左子表 quickSort(A,low,pivotPos-1); // 遞迴排序右子表 quickSort(A,pivotPos+1,high); } } // 生成隨機數的函式 int getRand() { // 設定隨機數範圍1-100 return rand() % 100 + 1; } // 生成隨機數的陣列 void getRandArray(int A[]){ // 隨機生成十個隨機數 for(int i = 0;i < 10;i++){ A[i] = getRand(); } } // 陣列輸出函式 void printArray(int A[]){ for(int i = 0;i < 10;i++){ printf("%d ",A[i]); } printf("\n"); } // 主函式測試 int main(){ // 以系統時間為種子 srand((unsigned)time(NULL)); int A1[10],A2[10]; getRandArray(A1); getRandArray(A2); printf("原始關鍵字序列為:\n"); printArray(A1); bubbleSort(A1,10); printf("氣泡排序後的序列為:\n"); printArray(A1); printf("原始關鍵字序列為:\n"); printArray(A2); quickSort(A2,0,9); printf("快速排序後的序列為:\n"); printArray(A2); return 0; }
選擇排序
選擇排序主要包括簡單選擇排序和堆排序,其中堆排序的時間複雜度不管在何種情況都能達到O(nlog2n)的數量級,也是十分優秀的演算法了。
注意點
簡單選擇排序,正如他的名字,簡單。沒什麼好說的。選出最小的位置,然後把小的元素放到最終位置。
堆排序,可以看成是一顆完全二叉樹,分為大根堆和小根堆,每次輸出一個元素後就要再次調整,成為堆。具體的實現看程式碼吧。
程式碼
/*
* @Description: 選擇排序(簡單選擇排序,堆排序)
* @version: 1.0
* @Author: Liuge
* @Date: 2021-08-03 19:58:46
*/
#include<bits/stdc++.h>
// 交換順序
void swap(int &m,int &n){
int temp = m;
m = n;
n = temp;
}
// 簡單選擇排序 升序 O(n2) 不穩定
void selectSort(int A[],int n){
int min;
for(int i = 0;i < n - 1;i++){
// 記錄最小元素下標
min = i;
for(int j = i + 1;j < n;j++){
// 更新最小元素位置
if(A[j] < A[min]){
min = j;
}
}
// 將最小元素和當前元素交換位置
if(min != i){
swap(A[i],A[min]);
}
}
}
// 調整堆 O(n)
void headAdjust(int A[],int k,int len){
// 用A[0]暫存根結點
A[0] = A[k];
// 進行調整
for(int i = 2 * k;i <= len;i *= 2){
// 篩選出較大的子結點
if(i < len && A[i] < A[i+1]){
i++;
}
if(A[0] >= A[i]){
// 篩選完畢,結束
break;
}else{
// 沒有篩選完,將孩子放到雙親結點上
A[k] = A[i];
// 修改k值,以便接著進行篩選
k = i;
}
}
// 把被篩選結點的值放入最終位置
A[k] = A[0];
}
// 建立大根堆
void buildMaxHeap(int A[],int len){
// 從非葉子結點開始挨個調整
for(int i = len / 2;i > 0;i--){
headAdjust(A,i,len);
}
}
// 堆排序 升序 O(nlog2n) 不穩定
void heapSort(int A[],int len){
buildMaxHeap(A,len);
for(int i = len;i > 1;i--){
// 和堆底元素進行交換,相當於輸出
swap(A[i],A[1]);
// 對剩下的i-1個元素進行調整
headAdjust(A,1,i-1);
}
}
// 生成隨機數的函式
int getRand() {
// 設定隨機數範圍1-100
return rand() % 100 + 1;
}
// 生成隨機數的陣列
void getRandArray(int A[],int len,bool isZeroBlank){
// 隨機生成len個隨機數
if(isZeroBlank){
for(int i = 1;i <= len;i++){
A[i] = getRand();
}
}else{
for(int i = 0;i < len;i++){
A[i] = getRand();
}
}
}
// 陣列輸出函式
void printArray(int A[],int len,bool isZeroBlank){
if(isZeroBlank){
for(int i = 1;i <= len;i++){
printf("%d ",A[i]);
}
}else{
for(int i = 0;i < len;i++){
printf("%d ",A[i]);
}
}
printf("\n");
}
// 主函式測試
int main(){
int A1[10],A2[11];
getRandArray(A1,10,false);
getRandArray(A2,10,true);
printf("原始關鍵字序列為:\n");
printArray(A1,10,false);
selectSort(A1,10);
printf("簡單選擇排序後序列為:\n");
printArray(A1,10,false);
printf("原始關鍵字序列為:\n");
printArray(A2,10,true);
heapSort(A2,10);
printf("堆排序後序列為:\n");
printArray(A2,10,true);
return 0;
}
2路歸併排序
注意點
2路歸併排序是歸併排序的一種,歸併排序包含著一種遞迴思想,將每個子表進行兩兩歸併,通過若干趟歸併後就可以獲得一個有序的序列。由於使用了遞迴,所以實現還是非常簡單的。2路歸併排序的時間複雜度也能到O(nlog2n)。
程式碼
/*
* @Description: 二路歸併排序
* @version: 1.0
* @Author: Liuge
* @Date: 2021-08-03 21:19:53
*/
#include<bits/stdc++.h>
// 合併函式
void merge(int A[],int low,int mid,int high){
// 定義輔助陣列B,長度和A陣列相同
int *B = (int *) malloc((high-low + 1) * sizeof(int));
// 把A的所有元素複製到B中
for(int k = low;k <= high;k++){
B[k] = A[k];
}
// 開始合併
int i,j,k;
for(i = low,j = mid + 1,k = i;i <= mid && j <= high;k++){
// 比較B兩個子表,把小的複製到A裡
if(B[i] <= B[j]){
A[k] = B[i++];
}else{
A[k] = B[j++];
}
}
// 把表中剩餘部分直接複製過去
while(i <= mid){
A[k++] = B[i++];
}
while(j <= high){
A[k++] = B[j++];
}
}
// 二路歸併排序 時間複雜度(Olog2n),空間複雜度O(n) 升序
void mergeSort(int A[],int low,int high){
// 遞迴排序
if(low < high){
int mid = (low + high) / 2;
mergeSort(A,low,mid);
mergeSort(A,mid+1,high);
merge(A,low,mid,high);
}
}
// 生成隨機數的函式
int getRand() {
// 設定隨機數範圍1-100
return rand() % 100 + 1;
}
// 生成隨機數的陣列
void getRandArray(int A[]){
// 隨機生成十個隨機數
for(int i = 0;i < 10;i++){
A[i] = getRand();
}
}
// 陣列輸出函式
void printArray(int A[]){
for(int i = 0;i < 10;i++){
printf("%d ",A[i]);
}
printf("\n");
}
// 主函式測試
int main(){
int A[10];
getRandArray(A);
printf("原始關鍵字序列為:\n");
printArray(A);
mergeSort(A,0,9);
printf("二路歸併排序後的序列為:\n");
printArray(A);
return 0;
}
總結
長達一個月左右的資料結構學習就差不多該結束了。磨磨蹭蹭的一個多月,基本上把每章的重點演算法都基本實現了一遍,雖然實現過程並不是完全脫稿,只是簡單的照著書上的思路學習了一下。之後會複習C++,然後嘗試用C++來自己手動敲一些常見的演算法,因此這個系列估計會更新到考研的最後幾天吧。加油奧利給!