• 排序算法详解


    一、冒泡排序

    冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。

    步骤:

    1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
    2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
    3. 针对所有的元素重复以上的步骤,除了最后一个。
    4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

    C代码实现

    1. #include <stdio.h>
    2. void bubble_sort(int arr[], int len) {
    3. int i, j, temp;
    4. for (i = 0; i < len - 1; i++)
    5. for (j = 0; j < len - 1 - i; j++)
    6. if (arr[j] > arr[j + 1]) {
    7. temp = arr[j];
    8. arr[j] = arr[j + 1];
    9. arr[j + 1] = temp;
    10. }
    11. }
    12. int main() {
    13. int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
    14. int len = (int) sizeof(arr) / sizeof(*arr);
    15. bubble_sort(arr, len);
    16. int i;
    17. for (i = 0; i < len; i++)
    18. printf("%d ", arr[i]);
    19. return 0;
    20. }

    二、选择排序

    选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。

    步骤:

    1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
    2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
    3. 重复第二步,直到所有元素均排序完毕。
    1. void swap(int *a,int *b) //交換兩個變數
    2. {
    3. int temp = *a;
    4. *a = *b;
    5. *b = temp;
    6. }
    7. void selection_sort(int arr[], int len)
    8. {
    9. int i,j;
    10. for (i = 0 ; i < len - 1 ; i++)
    11. {
    12. int min = i;
    13. for (j = i + 1; j < len; j++) //走訪未排序的元素
    14. if (arr[j] < arr[min]) //找到目前最小值
    15. min = j; //紀錄最小值
    16. swap(&arr[min], &arr[i]); //做交換
    17. }
    18. }

    三、插入排序

    插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

    插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

    将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

    步骤:

    从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

    1. void insertion_sort(int arr[], int len){
    2. int i,j,key;
    3. for (i=1;i<len;i++){
    4. key = arr[i];
    5. j=i-1;
    6. while((j>=0) && (arr[j]>key)) {
    7. arr[j+1] = arr[j];
    8. j--;
    9. }
    10. arr[j+1] = key;
    11. }
    12. }

    四、希尔排序

    希尔排序是非稳定排序算法。 该方法因 D.L.Shell 于 1959 年提出而得名。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的 关键词 越来越多,当增量减至 1 时,整个文件恰被分成一组, 算法 便终止。

    例:增量序列为1,3,5

    需排序的数字为:15,5,2,7,12,6,1,4,3,9,8,10

    希尔排序先从增量较高的地方开始

    先用5排序

    15,5,2,7,12,6,1,4,3,9,8,10

    15,6,8分别比较大小得到

    6,5,2,7,12,8,1,4,3,9,15,10

    然后看第二个

    6,5,2,7,12,8,1,4,3,9,15,10

    5,1,10比较大小得到

    6,1,2,7,12,8,5,4,3,9,15,10

    .......

    5排完后得到

    6,1,2,3,9,8,5,4,7,12,15,10

    3和1同理

    最后得到

    1,2,3,4,5,6,7,8,9,10,12,15

    代码实现:

    1. void shell_sort(int arr[], int n) {
    2. int i,j,inc,key //inc为增量
    3. //初始增量:n/2,每一趟之后除以2
    4. for(inc = n/2;inc > 0;inc /= 2){
    5. //每一趟采用插入排序
    6. for(i = inc;i < n;i++){
    7. key = arr[i]; //记录要插入的元素
    8. for(j = i;j >= inc && key < arr[j-inc];j -= inc){ //当前的Key要比下一个元素小才插入
    9. arr[j] = arr[j=inc];
    10. }
    11. arr[j] = key;
    12. }
    13. }
    14. }


    五、归并排序

    归并排序算法是在分治算法基础上设计出来的一种排序算法,它可以对指定序列完成升序(由小到大)或降序(由大到小)排序,对应的时间复杂度为O(nlogn)

    归并排序算法实现排序的思路是:

    1. 将整个待排序序列划分成多个不可再分的子序列,每个子序列中仅有 1 个元素;
    2. 所有的子序列进行两两合并,合并过程中完成排序操作,最终合并得到的新序列就是有序序列。

    使用归并排序算法对 {7, 5, 2, 4, 1, 6, 3, 0} 实现升序排序的过程是:
    1) 将 {7, 5, 2, 4, 1, 6, 3, 0} 分割成多个子序列,每个子序列中仅包含 1 个元素,分割过程如下所示:

     整个序列不断地被一分为二,最终被分割成 {7}、{5}、{2}、{4}、{1}、{6}、{3}、{0} 这几个序列。
    2) 将 {7}、{5}、{2}、{4}、{1}、{6}、{3}、{0} 以“两两合并”的方式重新整合为一个有序序列,合并的过程如下图所示:

    1. #include <stdio.h>
    2. //实现分割操作的函数
    3. void merge_sort(int* arr, int p, int q);
    4. //实现归并操作的函数
    5. void merge(int* arr, int p, int mid, int q);
    6. int main() {
    7. int i = 0;
    8. int arr[8] = { 7,5,2,4,1,6,3,0 };
    9. //对 arr 数组中第 18 个元素进行归并排序
    10. merge_sort(arr, 1, 8);
    11. while (i < 8)
    12. {
    13. printf("%d ", arr[i]);
    14. i++;
    15. }
    16. return 0;
    17. }
    18. //实现分割操作的函数,[p,q] 用于指定归并排序的区域范围,
    19. void merge_sort(int* arr, int p, int q) {
    20. int mid;
    21. if (arr == NULL || p > q || p == q) {
    22. return ;
    23. }
    24. mid = (p + q) / 2;
    25. //将 [p,q] 分为[p,mid] 和 [mid+1,q] 区域
    26. merge_sort(arr, p, mid);
    27. merge_sort(arr, mid + 1, q);
    28. //对分好的 [p,mid] 和 [mid,q] 进行归并操作
    29. merge(arr, p, mid, q);
    30. }
    31. //实现归并操作的函数,归并的 2 个区域分别为 [p,mid] 和 [mid+1,q]
    32. void merge(int* arr, int p, int mid, int q) {
    33. int i,j,k;
    34. int leftarr[100], rightarr[100];
    35. int numL = mid - p + 1;
    36. int numR = q - mid;
    37. //将 arr 数组中 [p,mid] 区域内的元素逐一拷贝到 leftarr 数组中
    38. for (i = 0; i < numL; i++) {
    39. leftarr[i] = arr[p - 1 + i];
    40. }
    41. //将 arr 数组中 [mid+1,q] 区域内的元素逐一拷贝到 rightarr 数组中
    42. leftarr[i] = 2147483647;
    43. for (i = 0; i < numR; i++) {
    44. rightarr[i] = arr[mid + i];
    45. }
    46. rightarr[i] = 2147483647;
    47. i = 0;
    48. j = 0;
    49. //逐一比较 leftarr 和 rightarr 中的元素,每次将较小的元素拷贝到 arr 数组中的 [p,q] 区域内
    50. for (k = p; k <= q; k++) {
    51. if (leftarr[i] <= rightarr[j]) {
    52. arr[k - 1] = leftarr[i];
    53. i++;
    54. }
    55. else {
    56. arr[k - 1] = rightarr[j];
    57. j++;
    58. }
    59. }
    60. }

    六、快速排序

    快速排序算法的实现思路是:

    • 从待排序序列中任选一个元素(假设为 pivot)作为中间元素,将所有比 pivot 小的元素移动到它的左边,所有比 pivot 大的元素移动到它的右边;
    • pivot 左右两边的子序列看作是两个待排序序列,各自重复执行第一步。直至所有的子序列都不可再分(仅包含 1 个元素或者不包含任何元素),整个序列就变成了一个有序序列。
    1. #include <stdio.h>
    2. // arr 为待排序数组,[p,q] 用于指定排序区域
    3. int partition(int* arr, int p, int q) {
    4. int temp = 0;
    5. // lo、hi分别表示指向首个元素和倒数第 2 个元素的指针
    6. int lo = p;
    7. int hi = q - 1;
    8. //pivot 表示选中的中间值
    9. int pivot = arr[q];
    10. while (1)
    11. {
    12. //lo从左往右遍历,直至找到一个不小于 pivot 的元素
    13. while (arr[lo] < pivot) {
    14. lo++;
    15. };
    16. //hi从右往左遍历,直至找到一个不大于 pivot 的元素
    17. while (hi > 0 && arr[hi] > pivot) {
    18. hi--;
    19. }
    20. //如果 lo≥hi,退出循环
    21. if (lo >= hi)
    22. {
    23. break;
    24. }
    25. else {
    26. //交换 arr[lo] 和 arr[hi] 的值
    27. temp = arr[lo];
    28. arr[lo] = arr[hi];
    29. arr[hi] = temp;
    30. // lo 和 hi 都向前移动一个位置,准备继续遍历
    31. lo++;
    32. hi--;
    33. }
    34. }
    35. //交换 arr[lo] 和 arr[q] 的值
    36. temp = arr[lo];
    37. arr[lo] = pivot;
    38. arr[q] = temp;
    39. //返回中间值所在序列中的位置
    40. return lo;
    41. }
    42. void quick_sort(int* arr, int p, int q) {
    43. int par;
    44. //如果待排序序列不存在,或者仅包含 1 个元素,则不再进行分割
    45. if (q - p <= 0) {
    46. return;
    47. }
    48. else {
    49. //调用 partition() 函数,分割 [p,q] 区域
    50. par = partition(arr, p, q);
    51. //以 [p,par-1]作为新的待排序序列,继续分割
    52. quick_sort(arr, p, par - 1);
    53. //以[par+1,q]作为新的待排序序列,继续分割
    54. quick_sort(arr, par + 1, q);
    55. }
    56. }
    57. int main()
    58. {
    59. int i = 0;
    60. int arr[10] = { 35,33,42,10,14,19,27,44,26,31 };
    61. //对于 arr 数组中所有元素进行快速排序
    62. quick_sort(arr, 0, 9);
    63. for (; i < 10; i++) {
    64. printf("%d ", arr[i]);
    65. }
    66. return 0;
    67. }

    七、计数排序

    计数排序不是将数进行比较,而是将数字在整个待排序的数组出现的位置用一个新的计数数组表示出来,计数数组的下标就代表了这个数,他出现的次数就是值,比如1在待排序数组中出现了4次,那么计数数组arr[1] = 4,由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),同时还需要一个累计数组,最后在输出最终结果时,需要累计数组来确定每个元素的值。

     算法的步骤如下:

    1. 找出待排序的数组中最大和最小的元素
    2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
    3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
    4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <time.h>
    4. void print_arr(int *arr, int n) {
    5. int i;
    6. printf("%d", arr[0]);
    7. for (i = 1; i < n; i++)
    8. printf(" %d", arr[i]);
    9. printf("\n");
    10. }
    11. void counting_sort(int *ini_arr, int *sorted_arr, int n) {
    12. int *count_arr = (int *) malloc(sizeof(int) * 100);
    13. int i, j, k;
    14. for (k = 0; k < 100; k++)
    15. count_arr[k] = 0;
    16. for (i = 0; i < n; i++)
    17. count_arr[ini_arr[i]]++;
    18. for (k = 1; k < 100; k++)
    19. count_arr[k] += count_arr[k - 1];
    20. for (j = n; j > 0; j--)
    21. sorted_arr[--count_arr[ini_arr[j - 1]]] = ini_arr[j - 1];
    22. free(count_arr);
    23. }
    24. int main(int argc, char **argv) {
    25. int n = 10;
    26. int i;
    27. int *arr = (int *) malloc(sizeof(int) * n);
    28. int *sorted_arr = (int *) malloc(sizeof(int) * n);
    29. srand(time(0));
    30. for (i = 0; i < n; i++)
    31. arr[i] = rand() % 100;
    32. printf("ini_array: ");
    33. print_arr(arr, n);
    34. counting_sort(arr, sorted_arr, n);
    35. printf("sorted_array: ");
    36. print_arr(sorted_arr, n);
    37. free(arr);
    38. free(sorted_arr);
    39. return 0;
    40. }

    八、基数排序

    基数排序也叫作桶子排序,基数排序不是讲各个数值直接进行比较,而是先比较各个数值的个位,将他有序的放入一个子桶中,在比较十位,然后放入子桶中,如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。最后将排好序的合并到原数组,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数

    1. #include<math.h>
    2. testBS()
    3. {
    4. inta[] = {2, 343, 342, 1, 123, 43, 4343, 433, 687, 654, 3};
    5. int *a_p = a;
    6. //计算数组长度
    7. intsize = sizeof(a) / sizeof(int);
    8. //基数排序
    9. bucketSort3(a_p, size);
    10. //打印排序后结果
    11. inti;
    12. for(i = 0; i < size; i++)
    13. {
    14. printf("%d\n", a[i]);
    15. }
    16. intt;
    17. scanf("%d", t);
    18. }
    19. //基数排序
    20. voidbucketSort3(int *p, intn)
    21. {
    22. //获取数组中的最大数
    23. intmaxNum = findMaxNum(p, n);
    24. //获取最大数的位数,次数也是再分配的次数。
    25. intloopTimes = getLoopTimes(maxNum);
    26. inti;
    27. //对每一位进行桶分配
    28. for(i = 1; i <= loopTimes; i++)
    29. {
    30. sort2(p, n, i);
    31. }
    32. }
    33. //获取数字的位数
    34. intgetLoopTimes(intnum)
    35. {
    36. intcount = 1;
    37. inttemp = num / 10;
    38. while(temp != 0)
    39. {
    40. count++;
    41. temp = temp / 10;
    42. }
    43. returncount;
    44. }
    45. //查询数组中的最大数
    46. intfindMaxNum(int *p, intn)
    47. {
    48. inti;
    49. intmax = 0;
    50. for(i = 0; i < n; i++)
    51. {
    52. if(*(p + i) > max)
    53. {
    54. max = *(p + i);
    55. }
    56. }
    57. returnmax;
    58. }
    59. //将数字分配到各自的桶中,然后按照桶的顺序输出排序结果
    60. voidsort2(int *p, intn, intloop)
    61. {
    62. //建立一组桶此处的20是预设的根据实际数情况修改
    63. intbuckets[10][20] = {};
    64. //求桶的index的除数
    65. //798个位桶index=(798/1)%10=8
    66. //十位桶index=(798/10)%10=9
    67. //百位桶index=(798/100)%10=7
    68. //tempNum为上式中的110100
    69. inttempNum = (int)pow(10, loop - 1);
    70. inti, j;
    71. for(i = 0; i < n; i++)
    72. {
    73. introw_index = (*(p + i) / tempNum) % 10;
    74. for(j = 0; j < 20; j++)
    75. {
    76. if(buckets[row_index][j] == NULL)
    77. {
    78. buckets[row_index][j] = *(p + i);
    79. break;
    80. }
    81. }
    82. }
    83. //将桶中的数,倒回到原有数组中
    84. intk = 0;
    85. for(i = 0; i < 10; i++)
    86. {
    87. for(j = 0; j < 20; j++)
    88. {
    89. if(buckets[i][j] != NULL)
    90. {
    91. *(p + k) = buckets[i][j];
    92. buckets[i][j] = NULL;
    93. k++;
    94. }
    95. }
    96. }
    97. }

    九、桶排序

    桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

    1. 在额外空间充足的情况下,尽量增大桶的数量
    2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

    同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #define N 7 // 待排序序列中的元素个数
    4. #define NBUCKET 6 // 桶的数量
    5. #define INTERVAL 10 // 每个桶能存放的元素个数
    6. //建立桶
    7. struct Node {
    8. float data;
    9. struct Node *next;
    10. };
    11. void BucketSort(float arr[]);
    12. struct Node *InsertionSort(struct Node *list);
    13. void print(float arr[]);
    14. int main() {
    15. float array[N] = { 0.42, 0.32, 0.23, 0.52, 0.25, 0.47, 0.51 };
    16. BucketSort(array);
    17. print(array);
    18. return 0;
    19. }
    20. // 桶排序,arr 为待排序序列
    21. void BucketSort(float arr[]) {
    22. int i, j;
    23. struct Node **buckets;
    24. // 创建所有桶
    25. buckets = (struct Node **)malloc(sizeof(struct Node *) * NBUCKET);
    26. // 设置每个桶为空桶
    27. for (i = 0; i < NBUCKET; ++i) {
    28. buckets[i] = NULL;
    29. }
    30. // 根据规定,将 arr 中的每个元素分散存储到各个桶中
    31. for (i = 0; i < N; ++i) {
    32. struct Node *current;
    33. int pos = arr[i] * 10; //根据规则,确定元素所在的桶
    34. //创建存储该元素的存储块,并连接到指定的桶中
    35. current = (struct Node *)malloc(sizeof(struct Node));
    36. current->data = arr[i];
    37. current->next = buckets[pos];
    38. buckets[pos] = current;
    39. }
    40. // 调用自定义的排序算法,对各个桶进行排序
    41. for (i = 0; i < NBUCKET; ++i) {
    42. buckets[i] = InsertionSort(buckets[i]);
    43. }
    44. // 合并所有桶内的元素
    45. for (j = 0, i = 0; i < NBUCKET; ++i) {
    46. struct Node *node;
    47. node = buckets[i];
    48. while (node) {
    49. arr[j++] = node->data;
    50. node = node->next;
    51. }
    52. }
    53. }
    54. // 自定义的排序算法,用于对各个桶内元素进行排序
    55. struct Node *InsertionSort(struct Node *list) {
    56. struct Node *k, *nodeList;
    57. if (list == NULL || list->next == NULL) {
    58. return list;
    59. }
    60. nodeList = list;
    61. k = list->next;
    62. nodeList->next = NULL;
    63. while (k != NULL) {
    64. struct Node *ptr;
    65. if (nodeList->data > k->data) {
    66. struct Node *tmp;
    67. tmp = k;
    68. k = k->next;
    69. tmp->next = nodeList;
    70. nodeList = tmp;
    71. continue;
    72. }
    73. for (ptr = nodeList; ptr->next != 0; ptr = ptr->next) {
    74. if (ptr->next->data > k->data)
    75. break;
    76. }
    77. if (ptr->next != 0) {
    78. struct Node *tmp;
    79. tmp = k;
    80. k = k->next;
    81. tmp->next = ptr->next;
    82. ptr->next = tmp;
    83. continue;
    84. }
    85. else {
    86. ptr->next = k;
    87. k = k->next;
    88. ptr->next->next = 0;
    89. continue;
    90. }
    91. }
    92. return nodeList;
    93. }
    94. void print(float ar[]) {
    95. int i;
    96. for (i = 0; i < N; ++i) {
    97. printf("%.2f ", ar[i]);
    98. }
    99. }

    十、堆排序

    堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

    分为两种方法:

    1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
    2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

    堆排序的平均时间复杂度为 Ο(nlogn)。

    1. 算法步骤

    1. 创建一个堆 H[0……n-1];

    2. 把堆首(最大值)和堆尾互换;

    3. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;

    4. 重复步骤 2,直到堆的尺寸为 1。

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. void swap(int *a, int *b) {
    4. int temp = *b;
    5. *b = *a;
    6. *a = temp;
    7. }
    8. void max_heapify(int arr[], int start, int end) {
    9. // 建立父節點指標和子節點指標
    10. int dad = start;
    11. int son = dad * 2 + 1;
    12. while (son <= end) { // 若子節點指標在範圍內才做比較
    13. if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比較兩個子節點大小,選擇最大的
    14. son++;
    15. if (arr[dad] > arr[son]) //如果父節點大於子節點代表調整完畢,直接跳出函數
    16. return;
    17. else { // 否則交換父子內容再繼續子節點和孫節點比較
    18. swap(&arr[dad], &arr[son]);
    19. dad = son;
    20. son = dad * 2 + 1;
    21. }
    22. }
    23. }
    24. void heap_sort(int arr[], int len) {
    25. int i;
    26. // 初始化,i從最後一個父節點開始調整
    27. for (i = len / 2 - 1; i >= 0; i--)
    28. max_heapify(arr, i, len - 1);
    29. // 先將第一個元素和已排好元素前一位做交換,再重新調整,直到排序完畢
    30. for (i = len - 1; i > 0; i--) {
    31. swap(&arr[0], &arr[i]);
    32. max_heapify(arr, 0, i - 1);
    33. }
    34. }
    35. int main() {
    36. int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
    37. int len = (int) sizeof(arr) / sizeof(*arr);
    38. heap_sort(arr, len);
    39. int i;
    40. for (i = 0; i < len; i++)
    41. printf("%d ", arr[i]);
    42. printf("\n");
    43. return 0;
    44. }

    最后附上各种排序的时间复杂度以及空间复杂度表以供参考

  • 相关阅读:
    Anaconda安装+Tensorflow2.0安装配置+Pycharm安装+GCN调试(Window 10)
    Django(3)模型
    iview项目中,radio选中值回显问题
    Centos 里面为什么有的磁盘命名/dev/vda 有的是/dev/sda ?
    Typescript 函数类型详解
    java毕业设计参考文献Springboot+mysql+freemark校园竞赛报名管理平台[包运行成功]
    vmware文件比实际的大缩减办法
    AWS--多个VPC上封禁一个 域名,禁止所有主机访问这个域名
    uni-app 打包H5外部浏览器唤起微信支付
    java基础面试题(一)
  • 原文地址:https://blog.csdn.net/m0_72647108/article/details/126796567