当前位置:   article > 正文

数据结构初阶(c语言)-排序算法

数据结构初阶(c语言)-排序算法

        数据结构初阶我们需要了解掌握的几种排序算法(除了直接选择排序,这个原因我们后面介绍的时候会解释)如下:

        其中的堆排序与冒泡排序我们在之前的文章中已经详细介绍过并对堆排序进行了一定的复杂度分析,所以这里我们不再过多介绍。

一,插入排序 

1.1直接插入排序 

1.1.1概念及实现代码 

        直接插入排序是⼀种简单的插⼊排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到⼀个新的有序序列 。

       就像我们玩扑克牌一样,当我们将发给我们的牌拿到我们自己手中进行排序一样,当插⼊第 i(i>=1) 个元素时,前⾯的 array[0],array[1],…,array[i-1] 已经排好序,此时用array[i] 的排序码与 array[i-1],array[i-2],… 的排序码顺序进行比较,找到插入位置即将 array[i] 插入,原来位置上的元素顺序后移。实现代码如下:

  1. void InsertSort(int* arr, int n)
  2. {
  3. for (int i = 0; i < n - 1; i++)
  4. {
  5. int end = i;
  6. int tmp = arr[end + 1];
  7. while (end >= 0)
  8. {
  9. if (arr[end] > tmp)
  10. {
  11. arr[end + 1] = arr[end];
  12. end--;
  13. }
  14. else
  15. {
  16. break;
  17. }
  18. }
  19. arr[end + 1] = tmp;
  20. }
  21. }

       从实现代码中我们不难看出, 直接插入的思想为先将前两个元素排好,再一个一个的往后进行,如果后面数据比我们上一次排好的数据的末尾数据大时,让末尾数据后移,再与末尾数据的前一个数据进行比较,直到找到小于的数据为止。插入的方法思想可以类比下我们顺序表中的头插法。

1.1.2复杂度的计算 

        空间复杂度由于未创建辅助空间,所以为O(1)。时间复杂度从代码中我们可以得知,最好情况下为O(N)(数组本身即为顺序),最坏的情况下(即数组逆序的情况下)为O(N^2)。所以直接插入的时间复杂度为O(N^2)。

1.2希尔排序

 1.1.1概念及实现代码 

       希尔排序是对直接插入排序的改良,再原有的排序方式上加入预排序,以达到减小时间复杂度的目的,实现代码如下:

  1. void ShellSort(int* arr, int n)
  2. {
  3. int gap = n;
  4. while (gap > 1)
  5. {
  6. gap = gap / 3 + 1;
  7. for (int i = 0; i < n - gap; i++)
  8. {
  9. int end = i;
  10. int tmp = arr[end + gap];
  11. while (end >= 0)
  12. {
  13. if (arr[end] > tmp)
  14. {
  15. arr[end + gap] = arr[end];
  16. end -= gap;
  17. }
  18. else
  19. {
  20. break;
  21. }
  22. }
  23. arr[end + gap] = tmp;
  24. }
  25. }
  26. }

        希尔排序一般先将原有数组分为三份,不断的三分对每份里面的数据进行排序,到了最后gap为1时数组已经基本有序,但我们不能直接设gap=n/3,比如我们的数组有效元素个数为9个,分三次之后为0,但我们需要让它的最后一次为一,所以我们的gap每次循环都设置为除三加以,从而确保最后进入循环的gap值为一。

1.1.2复杂度的简要计算

        空间复杂度与直接插入一致为O(1)。而时间复杂度对于外层(预排序),我们可以联想树,一直三分其实时间复杂度就为树的深度h=logn。所以外层的时间复杂度为logn。对于内层的时间复杂度,博主智商有限,难以推出,因为它的gap能够取得的值太多了,从而导致内层的复杂度难以计算,所以我们直接给出希尔排序的粗略时间复杂度的估算:O(N^1.3)。需要详细推理过程的读者可以自行上网搜寻。

二,选择排序

        这一部分我们不细讲,堆排序上篇文章已经详细介绍过,直接选择排序给出代码我们即可理解其思想:

  1. void SelectSort(int* arr, int n)
  2. {
  3. int begin = 0;
  4. int end = n - 1;
  5. while (begin < end)
  6. {
  7. int max = begin;
  8. int min = begin;
  9. for(int i = begin;i <= end;i++)
  10. {
  11. if (arr[max] < arr[i])
  12. {
  13. max = i;
  14. }
  15. if (arr[min] > arr[i])
  16. {
  17. min = i;
  18. }
  19. }
  20. if (max == begin)
  21. {
  22. max = min;
  23. }
  24. Swap(&arr[min], &arr[begin]);
  25. Swap(&arr[max], &arr[end]);
  26. begin++;
  27. end--;
  28. }
  29. }

        它的思想是逐步的将数据向中间集拢,第一遍找数组中的最大(小)值,并将最大(小)值分别放到头尾,接着让头++尾--。再次重复第一次的操作,最后直到没有数据为止。 

        可见直接选择排序,无论是最好的情况顺序还是最坏的情况逆序,他的时间复杂度均为O(N^2)。在实际学习与生产过程中都不可能使用,这也是我们简略介绍的原因。

三,快速排序 

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

         其实快速排序最主要的思想即为基准值,无限的二分去寻找基准值,与我们前面学习的二叉树非常类似。所以它的递归版本基础框架为(key为我们每次寻找到的基准值):
 

  1. void QuickSort(int* arr, int left, int right)
  2. {
  3. if (left >= right)
  4. {
  5. return;
  6. }
  7. int key = _QuickSort3(arr, left, right);
  8. QuickSort(arr, left, key - 1);
  9. QuickSort(arr, key + 1, right);
  10. }

3.1hoare版本快排

          hoare版本的思想为,开始设首数据为基准值,同时设左右指针,左边先找比基准值大的数据,找到停下,右边找比基准值小的数据,停下后于左指针的数据交换,循环下去,直到左指针的对应数组下标小于右指针截止。实现代码如下:

  1. int _QuickSort1(int* arr, int left, int right)
  2. {
  3. int key = left;
  4. ++left;
  5. while (left <= right)
  6. {
  7. while (left <= right && arr[left] < arr[key])
  8. {
  9. left++;
  10. }
  11. while (left <= right && arr[right] > arr[key])
  12. {
  13. right--;
  14. }
  15. if (left <= right)
  16. {
  17. Swap(&arr[left++], &arr[right--]);
  18. }
  19. }
  20. Swap(&arr[right], &arr[key]);
  21. return right;
  22. }

          为什么left=right时还要进入循环,是因为每次我们交换完后,right--,left++。万一此时它们正好重合,此处数据若小于或大于基准值都会导致最终基准值的落脚点错误。

3.2挖坑版本快排

          创建左右指针。首先从右向左找出比基准小的数据,找到后立即放入左边坑中,当前位置变为新的"坑",然后从左向右找出比基准⼤的数据,找到后立即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)。实现代码如下:

  1. int _QuickSort2(int* arr, int left, int right)
  2. {
  3. int key = arr[left];
  4. int hole = left;
  5. while (left < right)
  6. {
  7. while (left < right && arr[right] >= key)
  8. {
  9. --right;
  10. }
  11. Swap(&arr[hole], &arr[right]);
  12. hole = right;
  13. while (left < right && arr[left] <= key)
  14. {
  15. left++;
  16. }
  17. Swap(&arr[hole], &arr[left]);
  18. hole = left;
  19. }
  20. arr[hole] = key;
  21. return hole;
  22. }

          这里我们不需要去考虑left与right的相等的情况,因为我们最终基准值的位置与hole相关,所以不会影响我们基准值的落脚点正确。

3.3lomuto前后指针版本

           创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。实现代码如下:

  1. int _QuickSort3(int* arr, int left, int right)
  2. {
  3. int key = left;
  4. int slow = left;
  5. int fast = left + 1;
  6. while (fast <= right)
  7. {
  8. if (arr[fast] < arr[key] && ++slow != fast)
  9. {
  10. Swap(&arr[fast], &arr[slow]);
  11. }
  12. fast++;
  13. }
  14. Swap(&arr[slow], &arr[key]);
  15. return slow;
  16. }

3.4非递归版本的快排框架 

           由于每次我们的快排对基准值的寻找都需要上一次的基准值给出首尾位置,这里我们就可以借助我们之前学习到的堆的知识来进行首尾位置的记录:

  1. void Swap(int* x, int* y)
  2. {
  3. int tmp = *x;
  4. *x = *y;
  5. *y = tmp;
  6. }
  7. void STInit(Stack* ps)
  8. {
  9. assert(ps);
  10. ps->top = ps->capacity = 0;
  11. ps->arr = NULL;
  12. }
  13. void DestoryST(Stack* ps)
  14. {
  15. assert(ps);
  16. assert(ps->arr);
  17. free(ps->arr);
  18. ps->capacity = ps->top = 0;
  19. }
  20. void STpush(Stack* ps, STDataType x)
  21. {
  22. assert(ps);
  23. if (ps->top == ps->capacity)
  24. {
  25. int exchange = ps->capacity == 0 ? 4 : ps->capacity * 2;
  26. STDataType* new = (STDataType*)realloc(ps->arr, sizeof(STDataType) * exchange);
  27. if (new == NULL)
  28. {
  29. perror("new:");
  30. exit(1);
  31. }
  32. ps->arr = new;
  33. ps->capacity = exchange;
  34. }
  35. ps->arr[ps->top++] = x;
  36. }
  37. void STDelt(Stack* ps)
  38. {
  39. assert(ps);
  40. assert(!EmptyST(ps));
  41. --ps->top;
  42. }
  43. STDataType EleSTop(Stack* ps)
  44. {
  45. assert(ps && !EmptyST(ps));
  46. return ps->arr[ps->top - 1];
  47. }
  48. int EmptyST(Stack* ps)
  49. {
  50. assert(ps);
  51. return (ps->top == 0);
  52. }
  53. void QuickSortNone(int* arr, int left, int right)
  54. {
  55. Stack st;
  56. STInit(&st);
  57. STpush(&st, right);
  58. STpush(&st, left);
  59. while (!EmptyST(&st))
  60. {
  61. int begin = EleSTop(&st);
  62. STDelt(&st);
  63. int end = EleSTop(&st);
  64. STDelt(&st);
  65. int meet = _QuickSort3(arr, begin, end);
  66. if (begin < meet - 1)
  67. {
  68. STpush(&st, meet-1);
  69. STpush(&st, begin);
  70. }
  71. if (end > meet + 1)
  72. {
  73. STpush(&st, end);
  74. STpush(&st, meet+1);
  75. }
  76. }
  77. DestoryST(&st);
  78. }

        这里我们使用之前文章所给出的堆的实现方法来实现我们的非递归版本的快排的基本架构,这样及做到了减少时间复杂度,又减少了空间复杂度。

3.5复杂度的计算

        快速排序由于是以二叉树的方式进行栈的创建与销毁,所以其空间复杂度为logn,时间复杂度则与我们之前的向下建堆的时间复杂度一致为O(NlogN)。

四,归并排序

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

初始数组:

初始未排序数组是 [10, 6, 7, 1, 3, 9, 4, 2]。

  1. 分解阶段(自顶向下):

    • 数组被递归地分解成更小的子数组,直到每个子数组包含一个元素。
    • 第一次分解:[10, 6, 7, 1] 和 [3, 9, 4, 2]。
    • 第二次分解:
      • [10, 6, 7, 1] 分解为 [10, 6] 和 [7, 1]。
      • [3, 9, 4, 2] 分解为 [3, 9] 和 [4, 2]。
    • 第三次分解:
      • [10, 6] 分解为 [10] 和 [6]。
      • [7, 1] 分解为 [7] 和 [1]。
      • [3, 9] 分解为 [3] 和 [9]。
      • [4, 2] 分解为 [4] 和 [2]。
  2. 合并阶段(自底向上):

    • 分解到最小子数组后,开始两两合并,并在合并过程中进行排序。
    • 第一次合并:
      • [10] 和 [6] 合并为 [6, 10]。
      • [7] 和 [1] 合并为 [1, 7]。
      • [3] 和 [9] 合并为 [3, 9]。
      • [4] 和 [2] 合并为 [2, 4]。
    • 第二次合并:
      • [6, 10] 和 [1, 7] 合并为 [1, 6, 7, 10]。
      • [3, 9] 和 [2, 4] 合并为 [2, 3, 4, 9]。
    • 第三次合并:
      • [1, 6, 7, 10] 和 [2, 3, 4, 9] 合并为 [1, 2, 3, 4, 6, 7, 9, 10]

图示如下:

 

实现归并排序的代码如下:

  1. void _MergeSort(int* arr, int left, int right, int* tmp)
  2. {
  3. if (left >= right)
  4. {
  5. return;
  6. }
  7. int mid = (left + right) / 2;
  8. _MergeSort(arr, left, mid, tmp);
  9. _MergeSort(arr, mid + 1, right, tmp);
  10. int begin1 = left, end1 = mid;
  11. int begin2 = mid + 1, end2 = right;
  12. int index = begin1;
  13. while (begin1 <= end1 && begin2 <= end2)
  14. {
  15. if (arr[begin1] < arr[begin2])
  16. {
  17. tmp[index++] = arr[begin1++];
  18. }
  19. else {
  20. tmp[index++] = arr[begin2++];
  21. }
  22. }
  23. while (begin1 <= end1)
  24. {
  25. tmp[index++] = arr[begin1++];
  26. }
  27. while (begin2 <= end2)
  28. {
  29. tmp[index++] = arr[begin2++];
  30. }
  31. for (int i = left; i <= right; i++)
  32. {
  33. arr[i] = tmp[i];
  34. }
  35. }
  36. void MergeSort(int* arr, int n)
  37. {
  38. int* tmp = (int*)malloc(sizeof(int) * n);
  39. _MergeSort(arr, 0, n - 1, tmp);
  40. free(tmp);
  41. }

五,各类排序算法的时间及空间复杂度的比较

5.1对于稳定性的解释 

         稳定性的概念在排序算法中指的是如果两个元素在原始数组中有相同的值,那么在排序完成后,它们的相对顺序是否保持不变。

5.1.1稳定排序算法

一个稳定的排序算法会保持相同值的元素在原数组中的相对顺序。例如,考虑以下数组:

[5a, 3, 4, 5b, 2]

在这里,5a5b 是两个相同值的元素,但它们是不同的个体。

如果使用稳定的排序算法(例如冒泡排序或插入排序),那么排序后的数组可能是:

[2, 3, 4, 5a, 5b]

注意,5a 仍然在 5b 之前,这保持了它们在原始数组中的相对顺序。

5.1.2不稳定排序算法

一个不稳定的排序算法则不保证相同值的元素在排序后的相对顺序。例如,考虑同样的数组:

[5a, 3, 4, 5b, 2]

如果使用不稳定的排序算法(例如选择排序或快速排序),那么排序后的数组可能是: 

[2, 3, 4, 5b, 5a]

在这里,5a5b 的相对顺序被改变了。 

 

 

 

 

 


 

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

闽ICP备14008679号