最近对几个常见排序算法进行复习和总结,发现认真思考一下然后代码练习一下,这些排序基本上不会有太大难度。做任何事都是一样,难在你把它想得太难了,逼下自己,动脑思考一下,事情就会变得很简单。
下面是我个人对常见几个排序算法的理解和代码实现,如有什么错误还请指出。
1. 最简单的排序 -- 冒泡排序
原理就是:从第一个数开始,和后面的数两两进行比较;若前面的数比后面的数大,则将其后移。一轮下来,最后一个数一定是最大的。所以现在可以抛弃最后一个数,从第一个数开始,到倒数第二个数,重复以上步骤。依此类推,最后只剩下两个数比较交换完后,排序结束。
图1.冒泡排序(动态图来自维基百科,下同)
前面几个排序算法很容易理解和实现,故不详述。代码实现:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 void BubbleSort(int *a, int len) 5 { 6 int i, j, temp; 7 for (i = len - 1; i >= 0; i--) 8 { 9 for (j = 0; j < i; j++) 10 { 11 if (a[j] > a[j + 1]) 12 { 13 temp = a[j]; 14 a[j] = a[j + 1]; 15 a[j + 1] = temp; 16 } 17 } 18 } 19 } 20 21 int main(int argc, char *argv[]) 22 { 23 int a[10] = {12, 3, 16, 58, 18, 24, 6, 5, 64, 1}; 24 BubbleSort(a, 10); 25 for (int i = 0; i < 10; i++) 26 { 27 printf("%d ", a[i]); 28 } 29 30 return 0; 31 }
算法时间复杂度:O(n²)。在排序算法中属最慢,不过好在算法容易理解、代码容易实现。
2. 从剩下的当中选最大的 -- 选择排序
原理:遍历一遍数组找到最小的数放在首位,再遍历数组的剩余部分,找到最小的数放在第二位,依此类推。执行到最后一个数完成数组的排序。
图2. 选择排序动态图
代码实现:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 void SelectionSort(int *a, int len) 5 { 6 int i, j, min, temp; 7 for (i = 0; i < len; i++) 8 { 9 min = i; 10 for (j = i + 1; j < len; j++) 11 { 12 if (a[j] < a[min]) 13 { 14 min = j; 15 } 16 } 17 temp = a[i]; 18 a[i] = a[min]; 19 a[min] = temp; 20 } 21 } 22 23 int main(int argc, char *argv[]) 24 { 25 int a[10] = {12, 3, 16, 58, 18, 24, 6, 5, 64, 1}; 26 SelectionSort(a, 10); 27 for (int i = 0; i < 10; i++) 28 { 29 printf("%d ", a[i]); 30 } 31 return 0; 32 }
时间复杂度同样为O(n²),不过速度比冒泡快。拿100000个倒序的数在我的电脑试了下,冒泡用了21秒左右,选择用了12秒左右。
3. 在输入的时候排序 -- 插入排序
原理:从第二个数开始,逐个和前面的数进行比较,直到碰到比自己小的数或数组头部,则插入到其后面。再从下一个数开始,依此类推,直至最后一个数。这是对已有数组排序的情况,而在用户输入数组的情况下,可在输入过程中拿刚输入的数与已输入的数,如碰到比自己小的在插入到其后。
图3. 插入排序动态图
如:[6 5 1 4],5比6小,6后移,5插到6前面,得5614;1比5小,5和6后移,1插到5前面,得[1 5 6 4];4比5小,5和6后移,4插到1后面,[1 4 5 6]排序完成。
代码实现:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 void InsertionSort(int *a, int len) 5 { 6 int i, j, temp; 7 for (int i = 1; i < len; i++) 8 { 9 temp = a[i]; 10 for (j = i - 1; j >= 0 && temp < a[j]; j--) 11 { 12 a[j + 1] = a[j]; 13 } 14 a[j + 1] = temp; 15 } 16 } 17 18 int main(int argc, char *argv[]) 19 { 20 int a[10] = {12, 3, 16, 58, 18, 24, 6, 5, 64, 1}; 21 InsertionSort(a, 10); 22 for (int i = 0; i < 10; i++) 23 { 24 printf("%d ", a[i]); 25 } 26 27 return 0; 28 }
时间复杂度同样为O(n²)。同样拿100000个倒序的数在我的电脑试了下,插入排序用了15秒左右,似乎最坏情况插入排序要比选择排序慢。没用使用大量数据测试,所以不知道平均情况下上面三种排序谁快。
4. 改良插入排序 -- 希尔排序
原理:对插入排序的改良算法,通过设定步长(一般为n/2),将数组分成若干列,对每一列进行排序。再缩小步长,重复以上步骤,直至步长为1排序完成。
图4. 希尔排序动态图
我们假设有数组[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],现在以步长5排序,先将它们分列:
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后各自排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
再变换步长3,完成排序:
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后变换步长1,执行一次插入排序即完成排序。
代码实现:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 void ShellSort(int *a, int len) 5 { 6 int i, j, temp, step = len / 2; 7 while (step >= 1) 8 { 9 step /= 2; 10 for (i = 1; i < len; i++) 11 { 12 temp = a[i]; 13 j = i - step; 14 while (j >= 0 && temp < a[j]) 15 { 16 a[j + step] = a[j]; 17 j -= step; 18 } 19 a[j + step] = temp; 20 } 21 } 22 } 23 24 int main(int argc, char *argv[]) 25 { 26 int a[10] = {12, 3, 16, 58, 18, 24, 6, 5, 64, 1}; 27 ShellSort(a, 10); 28 for (int i = 0; i < 10; i++) 29 { 30 printf("%d ", a[i]); 31 } 32 33 return 0; 34 }
时间复杂度为O(nlog²n),相比上面的算法速度有显著提升。
5. 分成两半来排序 -- 归并排序
原理: 使用二分法将数组分成若干子数组,再在合并的过程中进行排序。因为每次递归都要创建新数组,所以内存空间占用大。
图5. 归并排序动态图
说白了就是将数组对半分,排序完后合并起来。例如数组[13 14 94 33 82 25 59 94]可分为[13 14 94 33]和[82 25 59 94],前者又可分为[13 14]和[94 33],排完序后变成[13 14]和[33 94],按顺序合并成[13 14 33 94],其它部分执行相同步骤。
代码实现:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 void MergeSort(int *a, int len) 5 { 6 int i, j, k; 7 int len1 = len / 2; 8 int len2 = len - len1; 9 int *a1 = (int *)calloc(sizeof(int), len1); 10 int *a2 = (int *)calloc(sizeof(int), len2); 11 if (len <= 1) 12 { 13 return; 14 } 15 for (i = 0; i < len1; i++) 16 { 17 a1[i] = a[i]; 18 } 19 for (j = 0; j < len2; j++) 20 { 21 a2[j] = a[i++]; 22 } 23 MergeSort(a1, len1); 24 MergeSort(a2, len2); 25 i = 0; j = 0; 26 //合并两个数组 27 for (k = 0; i < len1 && j < len2; k++) 28 { 29 if (a1[i] < a2[j]) 30 { 31 a[k] = a1[i++]; 32 } 33 else 34 { 35 a[k] = a2[j++]; 36 } 37 } 38 //将剩余的数拷回原数组 39 while (i < len1) 40 { 41 a[k++] = a1[i++]; 42 } 43 while (j < len2) 44 { 45 a[k++] = a2[j++]; 46 } 47 free(a1); 48 free(a2); 49 } 50 51 int main(int argc, char *argv[]) 52 { 53 int a[10] = {12, 3, 16, 58, 18, 24, 6, 5, 64, 1}; 54 MergeSort(a, 10); 55 for (int i = 0; i < 10; i++) 56 { 57 printf("%d ", a[i]); 58 } 59 60 return 0; 61 }
时间复杂度为O(nlogn),比希尔排序快。
6. 用二叉树来排序 -- 堆排序
原理:使用近似完全二叉树的堆结构(不同于内存的“堆”),利用最大堆/最小堆原理,使用一个保持最大堆/最小堆状态的维护函数来排序。
图6. 堆排序动态图
首先要理解堆结构,最大堆是一颗根结点比其它子结点都大的二叉树,一般用数组保存:
图7. 堆结构示意图
结构的实现并不难,难点在于堆的维护,一个保持最大堆的维护函数。首先先写一个最大堆的实现,保证父结点比任何子结点都大,a[0]存放堆大小:
void MaxHeapify(int *a, int i) { int largest = i; int l = LEFT(i); int r = RIGHT(i); if (l <= a[0] && a[l] >= a[i]) { largest = l; } if (r <= a[0] && a[r] >= a[largest]) { largest = r; } if (largest != i) { int temp = a[i]; a[i] = a[largest]; a[largest] = temp; MaxHeapify(a, largest); } }
然后实现将普通数组转换为最大堆:
void BuildHeapSort(int *a) { int i; for (i = a[0] / 2; i > 0; i--) { MaxHeapify(a, i); } }
再然后则是递归实现最大堆的根结点放至数组最后,最后一个元素脱离最大堆,然后重新维护最大堆,继续以上步骤直至最大堆只剩下一个结点:
void HeapSort(int *a) { int i; BuildHeapSort(a); for (i = a[0]; i > 1; i--) { int temp = a[i]; a[i] = a[1]; a[1] = temp; a[0]--; MaxHeapify(a, 1); } }
完整代码实现:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define LEFT(x) (x * 2) 5 #define RIGHT(x) (x * 2 + 1) 6 7 void MaxHeapify(int *a, int i) 8 { 9 int largest = i; 10 int l = LEFT(i); 11 int r = RIGHT(i); 12 if (l <= a[0] && a[l] >= a[i]) 13 { 14 largest = l; 15 } 16 if (r <= a[0] && a[r] >= a[largest]) 17 { 18 largest = r; 19 } 20 if (largest != i) 21 { 22 int temp = a[i]; 23 a[i] = a[largest]; 24 a[largest] = temp; 25 MaxHeapify(a, largest); 26 } 27 } 28 29 void BuildHeapSort(int *a) 30 { 31 int i; 32 for (i = a[0] / 2; i > 0; i--) 33 { 34 MaxHeapify(a, i); 35 } 36 } 37 38 void HeapSort(int *a) 39 { 40 int i; 41 BuildHeapSort(a); 42 for (i = a[0]; i > 1; i--) 43 { 44 int temp = a[i]; 45 a[i] = a[1]; 46 a[1] = temp; 47 a[0]--; 48 MaxHeapify(a, 1); 49 } 50 } 51 52 int main(int argc, char *argv[]) 53 { 54 int a[11] = {10, 12, 3, 16, 58, 18, 24, 6, 5, 64, 1}; 55 HeapSort(a); 56 for (int i = 1; i < 11; i++) 57 { 58 printf("%d ", a[i]); 59 } 60 61 return 0; 62 }
时间复杂度为O(nlogn),相对于归并排序稳定且快,不过代码实现麻烦。
7. 要的就是快 -- 快速排序
原理:选择一个元素作为基准,比它大的元素放在其后面,比它小的元素放在其前面。由于不断改变基准元素的位置需要耗费大量时间,所以代码实现需要用到两个变量,一个表示基准元素应该所在的下标i,一个表示下一个元素的下标j。碰到比基准元素小的元素则i增加1并交换i下标元素和j下标元素,然后j加1;碰到比基准变量大的元素则只有j加一。比较晚所有元素后,交换i+1下标元素和基准元素的位置并开始新一轮递归。
图8. 快速排序示意图
图9. 快速排序动态图
想法比较难理解,不过代码实现很简单。代码实现:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 void swap(int *x, int *y) 5 { 6 int temp = *x; 7 *x = *y; 8 *y = temp; 9 } 10 11 void QuickSort(int *a, int start, int end) 12 { 13 int i = start - 1, j = start; 14 if (start >= end) 15 { 16 return; 17 } 18 while (j < end && i < end) 19 { 20 if (a[j] <= a[end]) 21 { 22 i++; 23 swap(&a[i], &a[j]); 24 } 25 j++; 26 } 27 swap(&a[i + 1], &a[end]); 28 QuickSort(a, start, i); 29 QuickSort(a, i + 1, end); 30 } 31 32 int main(int argc, char *argv[]) 33 { 34 int a[10] = {12, 3, 16, 58, 18, 24, 6, 5, 64, 1}; 35 QuickSort(a, 0, 9); 36 for (int i = 0; i < 10; i++) 37 { 38 printf("%d ", a[i]); 39 } 40 41 return 0; 42 }
快速排序的时间复杂度为:O(nlogn),在大量数据的情况下是最快的排序算法,但及其不稳定,不适合小数据排序。对于快速排序来说,需要排序的元素越乱,速度越快。通常需要排序的元素并不会太乱,这时可为其增加一个随机基准元素,从而提高算法速度和稳定性。代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <time.h> 4 5 void swap(int *x, int *y) 6 { 7 int temp = *x; 8 *x = *y; 9 *y = temp; 10 } 11 12 void QuickSort(int *a, int start, int end) 13 { 14 int r, i = start - 1, j = start; 15 time_t t; 16 if (start >= end) 17 { 18 return; 19 } 20 srand((unsigned) time(&t)); 21 r = rand() % (end - start) + start; 22 swap(&a[r + 1], &a[end]); 23 while (j < end && i < end) 24 { 25 if (a[j] <= a[end]) 26 { 27 i++; 28 swap(&a[i], &a[j]); 29 } 30 j++; 31 } 32 swap(&a[i + 1], &a[end]); 33 QuickSort(a, start, i); 34 QuickSort(a, i + 1, end); 35 } 36 37 int main(int argc, char *argv[]) 38 { 39 int a[10] = {12, 3, 16, 58, 18, 24, 6, 5, 64, 1}; 40 QuickSort(a, 0, 9); 41 for (int i = 0; i < 10; i++) 42 { 43 printf("%d ", a[i]); 44 } 45 46 return 0; 47 }