【紫書】演算法競賽之排序演算法筆記
阿新 • • 發佈:2020-05-05
一:氣泡排序(O(n^2))
每組每個位置的數與它後面的數比較,前面數大於後面的數就交換數值,交換n-1組。
每次每組交換的時候,都會把最大的排到後面去,就類似與在水底泡泡慢慢的向上浮出。
特性:
穩定。
動圖演示:
詳細解析程式碼:
#include<stdio.h> int main() { int a[] = { 3,44,38,5,47,15,36,26,27,2,46,4,19,50,48 }; //為了方便大家理解,我將數值設為與動圖一樣的數值。 int n = 15; //氣泡排序 for(int i=0;i<n-1;i++)//代表的是要做多少組比對,為什麼是n-1呢? //因為計數從0開始而且第一個數和自身沒有比對的必要性。 for(int j=0;j<n-1-i;j++) //代表組裡面的序號,因為要與後面數做比較,而最後一個數沒有與之比較的(可能會越界)所以n-1 //為什麼還要-i呢? //因為每排過一遍,這一組最大值就排到後面去了,也就沒必要再去比較了。 if (a[j] > a[j + 1]) { int temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; } for (int i = 0;i < n;i++) printf("%d ", a[i]); return 0; }
二、選擇排序(O(n^2))
設定兩重迴圈,第一重迴圈做為比較的基準,第二重迴圈找它後面比它小的數,然後與之交換。
就是選擇一個它後面的所有比他小的數中的最小的數進行交換,氣泡排序是固定後面的數,而選擇排序是固定前面的數。
特性:
移動資料的次數已知(n-1 次)。
動圖演示:
詳細解析程式碼:
#include<stdio.h> int main() { int a[] = { 3,44,38,5,47,15,36,26,27,2,46,4,19,50,48 }; //為了方便大家理解,我將數值設為與動圖一樣的數值。 int n = 15; //選擇排序 for (int i = 0;i < n - 1;i++) // 代表的是要做基數數值的下標,為什麼是n - 1呢? //因為計數從0開始而且最後一個數沒得數和它進行比較了。 { int k = i; for (int j = i + 1;j < n;j++)//找到比基數小的數中的最小數的下標 if (a[k] > a[j]) k = j; if (k != i) { int temp = a[k]; a[k] = a[i]; a[i] = temp; } } for (int i = 0;i < n;i++) printf("%d ", a[i]); return 0; }
三、插入排序(O(n^2))
從第二個數開始,將數字抽出來與前面的數依次比較,大於它的統統向後移動一格。
也就是將抽出來的數放到它該在的位置。
特性:
在大多數元素已經有序的情況下,插入排序的工作量較小。
動圖演示:
詳細解析程式碼:
#include<stdio.h> int main() { int a[] = { 3,44,38,5,47,15,36,26,27,2,46,4,19,50,48 }; //為了方便大家理解,我將數值設為與動圖一樣的數值。 int n = 15; //選擇排序 for (int i = 1;i < n ;i++) // 代表的是要放回原位數數值的下標 { int front = i- 1;//初始化前一個數的下標 int sum = a[i];//放回原位數數值 while (front >= 0 && a[front] > sum) //防止陣列越界並且前面數比後面的數大才能繼續 a[front + 1] = a[front--]; //把前面數向後移動,前一個數的下標-1,,想象動圖中空格的變化 //上個迴圈結束了,說明空格不能再變化了,將其賦值回去就好了 a[++front] = sum; } for (int i = 0;i < n;i++) printf("%d ", a[i]); return 0; }
四、歸併排序 O(nlog(2)n)
特性:
可順便處理逆序對的問題。
動圖演示:
這個演算法用動圖充分體現了它的思想,放在下面的是輔助陣列,將每部分以兩段兩段的分開進行排序,分開的排好了,再進行總的排序。
詳細解析程式碼
#include<stdio.h> int a[] = { 3,44,38,5,47,15,36,26,27,2,46,4,19,50,48 }; //為了方便大家理解,我將數值設為與動圖一樣的數值。 int t[20];//輔助陣列,中轉站的意思。 void merge_sort(int l, int r) { //如果兩個陣列下標相鄰,就不必要再進行二分了,再分就只有一個了,無法比較了。 if (r - l > 1) { int m = l + (r - l >> 1), i = l, j = m, k = l; //m為分開的的這一段的中間下標,i為左邊起始點,j為右邊起始點,k為輔助陣列起始點。 //將總的一段二分。 merge_sort(l, m); merge_sort(m, r); //然後再將這兩段歸併到一起進行排序。 while (i < m || j < r)//防止下標越界 //先放到輔助陣列中的肯定是小的,所以我們只需要將兩邊較小的放到輔助陣列中就好了。 if (j >= r || (i < m && a[i] <= a[j])) t[k++] = a[i++]; //注意下標越界問題。 else t[k++] = a[j++];//這裡處理逆序對問題。 for (i = l;i < r;i++) a[i] = t[i]; } } int main() { int n = 15; merge_sort(0, n); for (int i = 0;i < n;i++) printf("%d ", a[i]); return 0; }
逆序對問題 程式碼:
#include<stdio.h> int a[] = { 3,44,38,5,47,15,36,26,27,2,46,4,19,50,48 }; //為了方便大家理解,我將數值設為與動圖一樣的數值。 int t[20];//輔助陣列,中轉站的意思。 int ans;//逆序對數量 void merge_sort(int l, int r) { //如果兩個陣列下標相鄰,就不必要再進行二分了,再分就只有一個了,無法比較了。 if (r - l > 1) { int m = l + (r - l >> 1), i = l, j = m, k = l; //m為分開的的這一段的中間下標,i為左邊起始點,j為右邊起始點,k為輔助陣列起始點。 //將總的一段二分。 merge_sort(l, m); merge_sort(m, r); //然後再將這兩段歸併到一起進行排序。 while (i < m || j < r)//防止下標越界 //先放到輔助陣列中的肯定是小的,所以我們只需要將兩邊較小的放到輔助陣列中就好了。 if (j >= r || (i < m && a[i] <= a[j])) t[k++] = a[i++]; //注意下標越界問題。 else t[k++] = a[j++],ans+=m-i;//這裡處理逆序對問題。 for (i = l;i < r;i++) a[i] = t[i]; } } int main() { int n = 15; merge_sort(0, n); printf("%d\n", ans); return 0; }
只要你把
else t[k++] = a[j++];成else t[k++]=a[j++],ans+=m-i;就好了,記得宣告ans變數。
逆序對的定義如下:對於數列的第 i 個和第 j 個元素,如果滿足 i < j 且 a[i] > a[j],則其為一個逆序對;否則不是。
而else恰好符合這個條件只要有一個是從右邊的數加進來的,就說明當前(左邊)下標的 i到m-1都是大於這個數的。
五、快速排序 O(nlog(2)n)
以中間數作為基數,同時從左右邊開始比較,因為基數是在中間,所以左邊的數要比基數小,右邊的數要比基數大,否則就交換數值,分支遞迴反覆便得出正確的順序。
特性:
可以處理求第幾大的問題;極快,資料移動少;
缺點:
不穩定。
詳細解析程式碼:
#include<stdio.h> int a[] = { 3,44,38,5,47,15,36,26,27,2,46,4,19,50,48 }; //為了方便大家理解,我將數值設為與之前一樣的數值。 void quick_sort(int l, int r) { if (l < r) {//防止越界 int i = l - 1, j = r + 1, x = a[l + r >> 1]; //-1、+1的原因是因為下面我們要用++i、--j。 //為什麼不用i++、j++呢?因為我們要比較數值,要讓i、j與我們比較的數值下標同步,提前+1或者-1會導致後面的交換髮生錯誤。 //前者是先使用i+1再將i=i+1,後者是先使用i再將i=i+1。 while (i < j) { //雙指標移動不符合就暫停 while (a[++i] < x); while (a[--j] > x); //兩邊都不符合了,交換數值。 if (i < j) {//學了c++後可直接用swap函式。 int t = a[i]; a[i] = a[j]; a[j] = t; } } //再分治,左右兩邊遞迴排序。 quick_sort(l, j);quick_sort(j + 1, r); } } int main() { int n=15; quick_sort(0, n - 1); for (int i = 0;i < n;i++) printf("%d ", a[i]); return 0; }
尋找第k小的數 程式碼:
#include<stdio.h> int a[] = { 3,44,38,5,47,15,36,26,27,2,46,4,19,50,48 }; //為了方便大家理解,我將數值設為與之前一樣的數值。 int quick_sort(int l, int r, int k) { if (l == r) return a[l]; int i = l - 1, j = r + 1, x = a[l + r >> 1]; //-1、+1的原因是因為下面我們要用++i、--j。 //為什麼不用i++、j++呢?因為我們要比較數值,要讓i、j與我們比較的數值下標同步,提前+1或者-1會導致後面的交換髮生錯誤。 //前者是先使用i+1再將i=i+1,後者是先使用i再將i=i+1。 while (i < j) { //雙指標移動不符合就暫停 while (a[++i] < x); while (a[--j] > x); //兩邊都不符合了,交換數值。 if (i < j) {//學了c++後可直接用swap函式。 int t = a[i]; a[i] = a[j]; a[j] = t; } } int s = j - l + 1;//判斷左邊有多少數,是否包含第k個數 //分治,不斷縮小範圍,直到l==r,輸出答案。 if (k <= s) return quick_sort(l, j, k); //k>s的話說明第K個值在右邊的第k-s 個數中,那麼在右邊的k=k-s; return quick_sort(j + 1, r, k - s); } int main() { int n = 15, k = 5; int ans = quick_sort(0, n - 1, k); printf("%d \n", ans); return 0; }
&n