排序算法的概念
1. 算法相關名詞
穩定:如果a原本在b前面,而a = b,排序之後a仍然在b的前面。
不穩定:如果a原本在b的前面,而a = b,排序之後 a 可能會出現在 b 的後面。
時間複雜度:排序時數據總的操作次數所用的時間規模。
空間複雜度:排序時在計算機內執行所需的臨時存儲空間。
2. 排序算法分類
比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此也稱爲非線性時間比較類排序。
非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱爲線性時間非比較類排序。
3. 排序算法複雜度
插入排序(Insertion Sort)
1. 算法描述
(1). 從第2個元素開始,依次取出下一元素Key;
(2). 從已排序的元素中從後往前掃描,如果掃描到的元素大於取出的元素Key,將該元素移動下一位置;
(3). 如果掃描已排序的元素中,某個元素小於或等於元素Key,則將Key插入該元素之後;
2. 動畫演示
黃色表示已排序部分,藍色表示未排序部分,紅色表示當前正在處理的key
3. 算法實現
void InsertSort(int arr[],int len){
int i,j;
// 從第2個元素開始
for(i=2;i<len;i++){
// 取出要參與比較的元素
arr[0] = arr[i];
// 從取出元素的前一個元素開始掃描
j = i--;
while(arr[0]<arr[j]){
// 元素後移
arr[j+1] = arr[j];
// 繼續往前掃描
j--;
};
// 插入取出比較的元素
arr[j+1] = arr[0];
}
}
注意上面的arr[0],算是一個小技巧,可以使循環的時間減少一半。
冒泡排序(Bubble Sort)
1. 算法描述
(1). 比較相鄰的元素,如果第一個比第二個大,就交換它們兩個;
(2). 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對;
(3). 每一趟下來,都會將一個當前比較大數按順序排到後面應有的位置,排完所有的趟數後,排序完成。
2. 動畫演示
黃色表示已排序部分,藍色表示未排序部分。
3. 算法實現
void BubbleSort(int arr[], int len){
int i, j,temp;
for(i=1;i<n-1;i++){
// 設立崗哨,判斷是否還需要排序
int exchange = 0;
for(j=1;j<n-i-1;j++){
if(arr[j]>arr[j+1]){
// 交換位置
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
// 交換過了
exchange = 1;
}
};
// 結束排序
if(exchange == 0){
break;
}
}
}
快速排序(Quick Sort)
1. 算法描述
(1). 從數列中挑出一個元素,稱爲"基準"(pivot);
(2). 重新排序數列,把所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在操作退出之後,該基準就處於數列的中間位置,這個操作稱爲分區(partition);
(3). 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列按前兩步進行排序;
2. 動畫演示
3. 算法實現
int QucikSort(int arr[], int low, int high){
if(low ==high){
return;
};
// 左指針
int i = low;
// 右指針
int j = high;
// 選擇第一個元素作爲基準
int privot = arr[low];
// 以privot爲基準,按左小右大依次進行交換
while(i<j){
// 如果右邊的值大於或等於基準,且左、右指針未相遇,則右指針左移
while(arr[j] >=privot && i<j){
--j;
};
// 右邊找到比基準小的元素,則將其送到左邊
if(i<j){
arr[i] = arr[j];
++i;
};
// 如果左邊的值小於或等於基準,且左、右指針未相遇,則左指針右移
while((arr[i]<=privot) && i<j){
++i;
};
// 左邊找到比基準大的元素,則將其送到右邊
if(i<j){
arr[j] = arr[i];
--j;
};
};
// 將基準放到中間
arr[i] = privot;
if(low<i-1){
QucikSort(arr,low,i-1);
};
if(j+1<high){
QucikSort(arr, j+1, high);
}
}
選擇排序(Quick Sort)
1. 算法描述
(1). 在未排序序列中找到最小元素,存放到排序序列的起始位置;
(2). 在剩餘未排序元素中繼續尋找最小元素,放到已排序序列的末尾;
(3). 重複步驟2,直到所有元素排序完畢;
2. 動畫演示
黃色表示已排序部分,藍色表示未排序部分,紅色表示從未排序中選擇的最小值。
3. 算法實現
void SelectSort(int arr[], int len){
// 外層循環按制輪數
for (int i = 0; i < len; i++){
// 內層循環找出最小值進行交換
int tmp;
for (int j = i; j < len; j++){
if (arr[j] < arr[i]){
tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
}
}
}
堆排序(Heap Sort)
1. 算法描述
(1). 將一組數據構建成一個堆;
(2). 調整這個堆,使之成爲最大堆,將根結點上最大的數與倒數第一個數進行交換;
(3). 重新調整交換過的堆,將根結點上最大的數與倒數第二個數進行交換;
(4). 重複執行建堆操作,依次與倒數沒有交換過的數進行交換。
2. 動畫演示
紅色代表交換過的,綠色代表調整好了的,藍色代表正在調整的。
3. 算法實現
// 交換
void swap(int arr[], int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 建堆
void buildHeap(int tree[], int n){
int lastNode = n - 1;
// 計算父節點的位置
int parent = (lastNode-1) / 2;
for (int i = parent; i >= 0; i--){
heapify(tree, n, i);
};
}
// 堆化(數組、節點個數、當前節點下標)
void heapify(int tree[], int n, int p){
// 到達最後一個葉子節點
if (p >= n){
return;
};
// 左孩子索引
int c1 = 2 * p + 1;
// 右孩子索引
int c2 = 2 * p + 2;
// 最大數的下標
int max =-1;
// 如果左子節點的下標未越界,並且左子節點的值大於父節點的值
if (c1 < n && tree[c1] > tree[p]){
max = c1;
};
// 如果右子節點的下標未越界,並且右子節點的值大於父節點的值
if (c2<n && tree[c2] > tree[p]) {}
// 如果左子節點大於父節點,並且右子節點又大於左子節點
if(max !=-1 && tree[c2]>tree[c1]){
// 將子結點中最大的那個索引給max
max = c2;
}
};
// 如果存在子大於父
if (max != -1) {
// 交換
swap(tree, max, p);
// 重建堆
heapify(tree, n, max);
}
}
// 堆排序
void heapSort(int tree[], int n){
// 建立堆
buildHeap(tree, n);
// 從最後一個元素開始交換
for (int i = n - 1; i >= 0; i--){
// 將堆頂存放最大值與最後一個元素做交換
swap(tree, i, 0);
// 做交換以後,前面的n-1個數的堆的性質被破壞,重建推
heapify(tree, i, 0);
}
}
歸併排序(Merge Sort)
1. 算法描述
(1). 把長度爲n的序列看成n個子元素;
(2). 從頭到尾依次對兩個元素進行歸併排序;
(3). 將歸併排序後的看成一個整體元素,從頭到尾再進行歸併排序,直到所有的元素都成爲一個歸併排序整體。
2. 動畫演示
3. 算法實現
// 合併(排序的數組、低位下標、中位下標、高位下標、臨時存儲數組)
void merge(int arr[], int low, int mid, int high, int temp[]){
// 左子數組開始位置
int i = low;
// 右子數組開始位置
int j = mid + 1;
// 臨時下標
int t = 0;
// 將數組分爲左、右子數組進行循環
while (i <= mid && j <= high){
// 如果左子數組裏的某一個值小於右子數組裏的某一個值
if (arr[i] < arr[j]){
// 將下標爲i的數組存到臨時數組裏
temp[t++] = arr[i++];
// 否則左子數組裏的某一個值大於或等於右子數組裏的某一個值
}else{
// 將下標爲j的數組存到臨時數組裏
temp[t++] = arr[j++];
}
};
// 將左邊剩餘元素填充進存到臨時數組
while (i <= mid){
temp[t++] = arr[i++];
};
// 將右邊剩餘元素填充進存到臨時數組
while (j <= high){
temp[t++] = arr[j++];
};
// 臨時下標歸零
t = 0;
// 將處理後的數據賦值到原數組中
while (low <= high){
arr[low++] = temp[t++];
}
}
// 合併排序(排序的數組、低位下標、高位下標、臨時存儲數組)
void mergeSort(int arr[], int low, int high, int temp[]){
// 當動態的低位下標小於動態的高位下標時
if (low < high){
int mid = (low + high) / 2;
// 左子數組融合排序
mergeSort(arr, low, mid, temp);
// 右子數組融合排序
mergeSort(arr, mid + 1, high, temp);
// 已經排序好的子數組有序融合
merge(arr, low, mid, high, temp);
}
}
希爾排序(Shell Sort)
1. 算法描述
(1). 將待排記錄序列以變量X爲間隔劃分爲若干子序列,對子序列分別進行插入排序;
(2). 將變量X按一定的規則減少,再將待排記錄序列以變量X爲間隔劃分成爲若干子序列,對子序列分別進行插入排序;
(3). 直到變量X減少爲1時,對待排記錄序列整體進行一次插入排序。
2. 動畫演示
3. 算法實現
// 希爾排序(排序數組、數組長度)
void ShellSort(int arr[], int len){
int i, j, k, tmp;
int gap = len;
do{
// 間隔的選擇可以有多種方案,如二分之一
// 這裏使用的是業界統一實驗平均情況最好的,收斂爲1
gap = gap / 3 + 1;
// 按間隔劃分爲多個組
for (i = gap; i < len; i += gap){
// 每組使用插入排序
k = i;
tmp = arr[k];
for (j = i - gap; (j >= 0) && (arr[j] > tmp); j -= gap){
arr[j + gap] = arr[j];
k = j;
};
arr[k] = tmp;
}
} while (gap > 1);
}
計數排序(Counting Sort)
1. 算法描述
(1). 找出待排序列中最大值 max 和最小值 min,算出序列的數據範圍 r = max - min + 1,申請輔助空間 C[r];
(2). 遍歷待排序列,統計序列中當前值 x 出現的次數,記錄於輔助空間C[x - min] ;
(3). 對輔助空間 C[r] 內的統計數字進行計算,每一個統計數字等於與前一個統計數字的和,以確定值爲 x 在數組中的位置;
(4). 反向遍歷原始數組序列每一個數,設當前數減最小數的值爲y,C[y]的值減1爲這個數在有序序列中的位置,同一個數每重複出現一次,將對應的C[y]位置減1,遍歷完成後所有數即爲有序序列。
2. 動畫演示
3. 算法實現
void countingSort(int arr[], int n) {
if (arr == NULL){
return;
};
// 定義輔助空間並初始化
int max = arr[0], min = arr[0];
int i;
for (i = 1; i < n; i++) {
if (max < arr[i]) {
max = arr[i];
};
if (min > arr[i]){
min = arr[i];
};
};
int r = max - min + 1;
int C[r];
memset(C, 0, sizeof(C));
// 定義目標數組
int R[n];
// 統計每個元素出現的次數
for (i = 0; i < n; i++){
C[arr[i] - min]++;
};
// 對輔助空間內數據進行計算
for (i = 1; i < r; i++) {
C[i] += C[i - 1];
};
// 反向填充目標數組
for (i = n - 1; i >= 0; i--) {
// 設當前數減最小數的值爲y,C[y]的值減1爲這個數在有序序列中的位置
// 當前數每重複出現一次,將對應的C[y]位置減1向前推一次
int y = arr[i] - min;
R[--C[y]] = arr[i];
};
// 目標數組裏的結果重新賦值
for (i = 0; i < n; i++){
arr[i] = R[i];
}
}
桶排序(Bucket Sort)
1. 算法描述
(1). 設置固定數量的空桶;
(2). 把數據放在對應的桶內,分別對每個非空桶內數據進行排序;
(3). 拼接非空的桶內數據,得到最終的結果。
2. 動畫演示
3. 算法實現
void bucketSort(int arr[], int n, int r) {
if (arr == NULL || r < 1) {
return;
};
// 根據最大/最小元素和桶數量,計算出每個桶對應的元素範圍
int max = arr[0], min = arr[0];
int i, j;
for (i = 1; i < n; i++) {
if (max < arr[i]) max = arr[i];
if (min > arr[i]) min = arr[i];
}
int range = (max - min + 1) / r + 1;
// 建立桶對應的二維數組,一個桶裏最多可能出現 n 個元素
int buckets[r][n];
memset(buckets, 0, sizeof(buckets));
int counts[r];
memset(counts, 0, sizeof(counts));
for (i = 0; i < n; i++) {
int k = (arr[i] - min) / range;
buckets[k][counts[k]++] = arr[i];
};
int index = 0;
// 分別對每個非空桶內數據進行排序,比如計數排序
for (i = 0; i < r; i++) {
if (counts[i] == 0) {
continue}
};
countingSort(buckets[i], counts[i]);
// 拼接非空的桶內數據,得到最終的結果
for (j = 0; j < counts[i]; j++) {
arr[index++] = buckets[i][j];
}
}
}
基數排序(Radix Sort)
1. 算法描述
(1). 將所有待比較數值(非負整數)統一爲同樣的數位長度,數位不足的數值前面補零;
(2). 比較時分最高位優先法(MSD法)和最低位優先法(LSD法),此處以LSD爲例,從最低位(個位)開始,依次進行一次排序;
(3). 從最低位排序一直到最高位排序完成以後, 數列就變成一個有序序列。
2. 動畫演示
3. 算法實現
// 基數,範圍0~9
const RADIX 10
void radixSort(int arr[], int n) {
// 獲取最大值和最小值
int max = arr[0], min = arr[0];
int i, j, l;
for (i = 1; i < n; i++) {
if (max < arr[i]) max = arr[i];
if (min > arr[i]) min = arr[i];
};
// 假如序列中有負數,所有數加上一個常數,使序列中所有值變成正數
if (min < 0) {
for (i = 0; i < n; i++){
arr[i] -= min;
};
max -= min;
};
// 獲取最大值位數
int d = 0;
while (max > 0) {
max /= RADIX;
d ++;
};
int queue[RADIX][n];
memset(queue, 0, sizeof(queue));
int count[RADIX] = {0};
for (i = 0; i < d; i++) {
// 分配數據
for (j = 0; j < n; j++) {
int key = arr[j] % (int)pow(RADIX, i + 1) / (int)pow(RADIX, i);
queue[key][count[key]++] = arr[j];
};
// 收集數據
int c = 0;
for (j = 0; j < RADIX; j++) {
for (l = 0; l < count[j]; l++) {
arr[c++] = queue[j][l];
queue[j][l] = 0;
}
count[j] = 0;
};
};
// 假如序列中有負數,收集排序結果時再減去前面加上的常數
if (min < 0) {
for (i = 0; i < n; i++) {
arr[i] += min;
}
}
}