当前位置:   article > 正文

数据结构初阶之排序(下)

数据结构初阶之排序(下)

前言

上一期内容中我们了解了基本排序中的插入与选择排序,今天我将为大家带来剩下的几种排序算法

快速排序

快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列在相应位置上为⽌。

快速排序有着许多种实现方式,今天我们就hoare、挖坑法以及lomuto前后指针法来对其进行递归实现,同时还将运用数据结构中的栈来实现快速排序的非递归实现方法。

快速排序实现的主框架

  1. //快速排序
  2. void QuickSort(int* a, int left, int right)
  3. {
  4. if (left >= right) {
  5. return;
  6. }
  7. //_QuickSort⽤于按照基准值将区间[left,right)中的元素进⾏划分
  8. int meet = _QuickSort(a, left, right);
  9. QuickSort(a, left, meet - 1);
  10. QuickSort(a, meet + 1, right);
  11. }

将区间中的元素进⾏划分的 _QuickSort ⽅法主要有以下⼏种实现⽅式:

我们可以将这些方法看做一个找基准值的过程:

hoare版本

思路:

1)创建左右指针,确定基准值

2)从右向左找出⽐基准值⼩的数据,从左向右找出⽐基准值⼤的数据,左右指针数据交换,进⼊下次循环

问题1:为什么跳出循环后right位置的值⼀定不⼤于key?

        当 left > right 时,即right⾛到left的左侧,⽽left扫描过的数据均不⼤于key,因此right此时指向的数据⼀定不⼤于key

如图:

问题2:为什么left和right指定的数据和key值相等时也要交换?

        相等的值参与交换确实有⼀些额外消耗。实际还有各种复杂的场景,假设数组中的数据⼤量重复时,⽆法进⾏有效的分割排序。

如图:

代码的实现
  1. int _QuickSort(int* a, int left, int right)
  2. {
  3. int begin = left;
  4. int end = right;
  5. int keyi = left;
  6. ++left;
  7. while (left <= right)
  8. {
  9. // 右边找⼩
  10. while (left <= right && a[right] > a[keyi])
  11. {
  12. --right;
  13. }
  14. // 右边找⼩
  15. while (left <= right && a[left] < a[keyi])
  16. {
  17. ++left;
  18. }
  19. if (left <= right)
  20. {
  21. swap(&a[left++], &a[right--]);
  22. }
  23. }
  24. swap(&a[keyi], &a[right]);
  25. return right;
  26. }

挖坑法

思路:

创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)

我们通过一张图来了解:

代码的实现
  1. int _QuickSort(int* a, int left, int right)
  2. {
  3. int mid = a[left];
  4. int hole = left;
  5. int key = a[hole];
  6. while (left < right)
  7. {
  8. while (left < right && a[right] >= key)
  9. {
  10. --right;
  11. }
  12. a[hole] = a[right];
  13. hole = right;
  14. while (left < right && a[left] <= key)
  15. {
  16. ++left;
  17. }
  18. a[hole] = a[left];
  19. hole = left;
  20. }
  21. a[hole] = key;
  22. return hole;
  23. }

lomuto前后指针

思路:

创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。

如图:

代码的实现
  1. int _QuickSort(int* a, int left, int right)
  2. {
  3. int prev = left, cur = left + 1;
  4. int key = left;
  5. while (cur <= right)
  6. {
  7. if (a[cur] < a[key] && ++prev != cur)
  8. {
  9. swap(&a[cur], &a[prev]);
  10. }
  11. ++cur;
  12. }
  13. swap(&a[key], &a[prev]);
  14. return prev;
  15. }

快速排序的特征

1. 时间复杂度:O(nlogn)

2. 空间复杂度:O(logn)


快速排序的非递归版本

⾮递归版本的快速排序需要借助数据结构:

根据栈结构先进后出的原则,我们先将待排序数组的末尾元素先入栈,头元素后入栈。

随后分别取栈顶与栈底元素,利用循环来实现递归操作。

代码的实现

  1. void QuickSortNonR(int* a, int left, int right)
  2. {
  3. ST st;
  4. STInit(&st);
  5. STPush(&st, right);
  6. STPush(&st, left);
  7. while (!STEmpty(&st))
  8. {
  9. int begin = STTop(&st);
  10. STPop(&st);
  11. int end = STTop(&st);
  12. STPop(&st);
  13. // 单趟
  14. int keyi = begin;
  15. int prev = begin;
  16. int cur = begin + 1;
  17. while (cur <= end)
  18. {
  19. if (a[cur] < a[keyi] && ++prev != cur)
  20. Swap(&a[prev], &a[cur]);
  21. ++cur;
  22. }
  23. Swap(&a[keyi], &a[prev]);
  24. keyi = prev;
  25. // [begin, keyi-1] keyi [keyi+1, end]
  26. if (keyi + 1 < end)
  27. {
  28. STPush(&st, end);
  29. STPush(&st, keyi + 1);
  30. }
  31. if (begin < keyi-1)
  32. {
  33. STPush(&st, keyi-1);
  34. STPush(&st, begin);
  35. }
  36. }
  37. STDestroy(&st);
  38. }

归并排序

归并排序的思想:

归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide  and Conquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。归并排序核⼼步骤(如下图):

利用递归分别将待排序的数组二等分割成n份,每次取一半,随后就分割后的两两数组进行合并操作(先比大小后放置)

代码的实现

  1. void _MergeSort(int* a, int left, int right, int* tmp)
  2. {
  3. if (left >= right)
  4. {
  5. return;
  6. }
  7. int mid = (right + left) / 2;
  8. //[left,mid] [mid+1,right]
  9. _MergeSort(a, left, mid, tmp);
  10. _MergeSort(a, mid + 1, right, tmp);
  11. int begin1 = left, end1 = mid;
  12. int begin2 = mid + 1, end2 = right;
  13. int index = begin1;
  14. //合并两个有序数组为⼀个数组
  15. while (begin1 <= end1 && begin2 <= end2)
  16. {
  17. if (a[begin1] < a[begin2])
  18. {
  19. tmp[index++] = a[begin1++];
  20. }
  21. else
  22. {
  23. tmp[index++] = a[begin2++];
  24. }
  25. }
  26. while (begin1 <= end1)
  27. {
  28. tmp[index++] = a[begin1++];
  29. }
  30. while (begin2 <= end2)
  31. {
  32. tmp[index++] = a[begin2++];
  33. }
  34. for (int i = left; i <= right; i++)
  35. {
  36. a[i] = tmp[i];
  37. }
  38. }
  39. void MergeSort(int* a, int n)
  40. {
  41. int* tmp = new int[n];
  42. _MergeSort(a, 0, n - 1, tmp);
  43. delete[] tmp;
  44. }

归并排序的特征

1. 时间复杂度:O(nlogn)

2. 空间复杂度:O(n)


测试代码

通过以下代码,我们可以得出各种排序算法的时间效率。

  1. // 测试排序的性能对⽐
  2. void TestOP()
  3. {
  4. srand(time(0));
  5. const int N = 100000;
  6. int* a1 = (int*)malloc(sizeof(int)*N);
  7. int* a2 = (int*)malloc(sizeof(int)*N);
  8. int* a3 = (int*)malloc(sizeof(int)*N);
  9. int* a4 = (int*)malloc(sizeof(int)*N);
  10. int* a5 = (int*)malloc(sizeof(int)*N);
  11. int* a6 = (int*)malloc(sizeof(int)*N);
  12. int* a7 = (int*)malloc(sizeof(int)*N);
  13. for (int i = 0; i < N; ++i)
  14. {
  15. a1[i] = rand();
  16. a2[i] = a1[i];
  17. a3[i] = a1[i];
  18. a4[i] = a1[i];
  19. a5[i] = a1[i];
  20. a6[i] = a1[i];
  21. a7[i] = a1[i];
  22. }
  23. int begin1 = clock();
  24. InsertSort(a1, N);
  25. int end1 = clock();
  26. int begin2 = clock();
  27. ShellSort(a2, N);
  28. int end2 = clock();
  29. int begin3 = clock();
  30. SelectSort(a3, N);
  31. int end3 = clock();
  32. int begin4 = clock();
  33. HeapSort(a4, N);
  34. int end4 = clock();
  35. int begin5 = clock();
  36. QuickSort(a5, 0, N-1);
  37. int end5 = clock();
  38. int begin6 = clock();
  39. MergeSort(a6, N);
  40. int end6 = clock();
  41. int begin7 = clock();
  42. BubbleSort(a7, N);
  43. int end7 = clock();
  44. printf("InsertSort:%d\n", end1 - begin1);
  45. printf("ShellSort:%d\n", end2 - begin2);
  46. printf("SelectSort:%d\n", end3 - begin3);
  47. printf("HeapSort:%d\n", end4 - begin4);
  48. printf("QuickSort:%d\n", end5 - begin5);
  49. printf("MergeSort:%d\n", end6 - begin6);
  50. printf("BubbleSort:%d\n", end7 - begin7);
  51. free(a1);
  52. free(a2);
  53. free(a3);
  54. free(a4);
  55. free(a5);
  56. free(a6);
  57. free(a7);
  58. }

非比较排序

学习了以上的代码,那么有没有不通过比较大小就能实现的排序算法呢?有!

计数排序

计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。

操作步骤:

1)统计相同元素出现次数

2)根据统计的结果将序列回收到原来的序列中

通过两张图片来了解一下:

为了避免空间的无端浪费,我们将遍历待排数组中的最大最小值,后得出其间的差值,用其差值来申请空间大小以达到排序的效果。

代码的实现

  1. void CountSort(int* a, int n)
  2. {
  3. int min = a[0], max = a[0];
  4. for (int i = 1; i < n; i++)
  5. {
  6. if (a[i] > max)
  7. max = a[i];
  8. if (a[i] < min)
  9. min = a[i];
  10. }
  11. int range = max - min + 1;
  12. int* count = (int*)malloc(sizeof(int) * range);
  13. if (count == NULL)
  14. {
  15. perror("malloc fail");
  16. return;
  17. }
  18. memset(count, 0, sizeof(int) * range);
  19. // 统计次数
  20. for (int i = 0; i < n; i++)
  21. {
  22. count[a[i] - min]++;`
  23. }
  24. // 排序
  25. int j = 0;
  26. for (int i = 0; i < range; i++)
  27. {
  28. while (count[i]--)
  29. {
  30. a[j++] = i + min;
  31. }
  32. }
  33. }

计数排序的特征

计数排序在数据范围集中时,效率很⾼,但是适⽤范围及场景有限。

时间复杂度:O(N + range)

空间复杂度:O(range)

稳定性:稳定

*计数排序仅适用于整数的排序


排序算法的稳定性及其复杂度综合分析

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的 相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,⽽在排序后的序列中,r[i]仍在r[j]之 前,则称这种排序算法是稳定的;否则称为不稳定的。


以上便是数据结构初阶的全部内容,感谢各位的支持!后面我将为大家带来C++有关的知识分享,敬请期待!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/951304?site
推荐阅读
相关标签
  

闽ICP备14008679号