如何使用C语言实现快速排序

其他教程   发布日期:2024年11月01日   浏览次数:230

本篇内容主要讲解“ 如何使用C语言实现快速排序”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“ 如何使用C语言实现快速排序”吧!

快速排序的基本思想是:任取待排序数列中的一个数作为 key 值,通过某种方法使得 key 的左边所有的数都比它小,右边的数都比它大;以 key 为中心,将 key 左边的数列与右边的数列取出,做同样的操作(取 key 值,分割左右区间),直至所有的数都到了正确的位置。

上述所提到的某种方法可以有很多种,例如:hoare法、挖坑法、前后指针法。它们虽然做法不相同,但做的都是同一件事——分割出 key 的左右区间(左边的数比 key 小,右边的数比 key 大)。

1. hoare法

方法与步骤

以数列 6,1,2,7,9,3,4,5,8,10 为例:

1.取最左边为 key ,分别有 left 和 right 指向数列的最左端与最右端;

2. right 先走,找到比 key 小的数就停下来;

3. left 开始走,找到比 key 大的数就停下来;

4. 交换 left 与 right 所在位置的数;

5.重复上述操作,right 找小,left 找大,进行交换;

6. right 继续找小;

7. left 继续找大,若与 right 就停下来;

8.交换二者相遇位置与 key 处的值;

此时一趟排序就完成了,此时的数列有两个特点:

1. key 所指向的值(6)已经到了正确的位置;

2. key 左边的数字都比 key 要小,右边的都比 key 要大;

接下来就是递归的过程了,分别对左右区间进行同样的操作:

代码实现

知道了详解步骤,用代码来实现并不困难,但是有很多很多的细节需要注意。(这里的代码未经优化,当前的代码有几种极端的情况不能适应)

  1. void Swap(int* p, int* q)
  2. {
  3. int tmp = *p;
  4. *p = *q;
  5. *q = tmp;
  6. }
  7. void QuickSort(int* a, int begin, int end)
  8. {
  9. //数列只有一个数,或无数列则返回
  10. if (begin >= end)
  11. {
  12. return;
  13. }
  14. int left = begin;
  15. int right = end;
  16. int keyi = left;
  17. while (left < right)
  18. {
  19. //右边先走
  20. while (left < right && a[right] >= a[keyi])
  21. {
  22. right--;
  23. }
  24. while (left < right && a[left] <= a[keyi])
  25. {
  26. left++;
  27. }
  28. Swap(&a[left], &a[right]);
  29. }
  30. Swap(&a[keyi], &a[left]);
  31. QuickSort(a, begin, left - 1);
  32. QuickSort(a, left + 1, end);
  33. }

2. 挖坑法

挖坑法相比于hoare法,思路上更为简单易懂。

方法与步骤

还是以同样的数列 6,1,2,7,9,3,4,5,8,10 为例:

1. 先将第一个数存放到 key 中,形成一个坑位:分别有 left 和 right 指向数列的最左端与最右端;

2. right 先走,找到比 key 小的数,将该数丢到坑里;同时又形成了一个新的坑;

3. left 开始走,找到比 key 大的数,将该数丢到坑里;同时形成一个新的坑;

4. right继续找小,进行重复的操作;

5. left 找大;

6. right 找小;

7. left 找大;

8.若二者相遇就停下来;将 key 值放入坑;

至此,一趟排序已经完成,我们发现此时的数列与hoare具有相同的特点:

1. key 所指向的值(6)已经到了正确的位置;

2. key 左边的数字都比 key 要小,右边的都比 key 要大;

挖坑法、hoare、前后指针法完成一趟排序后都具有相同的特点,所以不同版本的快速排序不一样的只有单趟排序的实现,总体思路都是相同的。

代码实现

  1. void QuickSort(int* a, int begin, int end)
  2. {
  3. if (begin >= end)
  4. {
  5. return;
  6. }
  7. int left = begin;
  8. int right = end;
  9. int key = a[left];
  10. int hole = left;//坑位
  11. while (left < right)
  12. {
  13. while (left < right && a[right] >= key)
  14. {
  15. right--;
  16. }
  17. a[hole] = a[right];
  18. hole = right;
  19. while (left < right && a[left] <= key)
  20. {
  21. left++;
  22. }
  23. a[hole] = a[left];
  24. hole = left;
  25. }
  26. a[hole] = key;
  27. QuickSort(a, begin, hole - 1);
  28. QuickSort(a, hole + 1, end);
  29. }

3. 前后指针法

方法与步骤

以同样的数列为例:

1. 取第一个值为 key ;有 prev 和 cur 分别指向数列开头和 prev 的下一个数;

2. cur 先走,找到比 key 小的数就停下来;

3. ++prev ,交换 prev 与 cur 位置的数;(前两次无需交换,因为自己与自己换没有意义)

4. 重复此步骤;

5. 直到 cur 走完整个数列,交换 prev 与 key 处的值;

至此,第一趟排序就结束了,又是与前两种方法相同的结果;

代码实现

  1. void QuickSort(int* a, int begin, int end)
  2. {
  3. if (begin >= end)
  4. {
  5. return;
  6. }
  7. int prev = begin;
  8. int cur = prev + 1;
  9. int keyi = begin;
  10. while (cur <= end)
  11. {
  12. if (a[cur] < a[keyi] && ++prev != cur)
  13. {
  14. Swap(&a[prev], &a[cur]);
  15. }
  16. cur++;
  17. }
  18. Swap(&a[keyi], &a[prev]);
  19. keyi = prev;
  20. QuickSort(a, begin, keyi - 1);
  21. QuickSort(a, keyi + 1, end);
  22. }

4. 快速排序的缺点与优化

1.快速排序的缺点

我们用三种方式实现了快速排序,其实这三种方式并无明显的优劣之分。但是我们前面设计的快速排序其实是有两个缺点的:

1.在最坏情况下它的的效率极慢;

2.在数据量太大时会造成栈溢出。

那么什么情况是最坏情况呢?答案是,当数据本身就是有序的时候(无论是逆序还是顺序)。在最坏情况下,每次我们的 key 值都是最大或者最小,这样就会使 key 与数列的每个数都比较一次,它的时间复杂度为 O(n^2);

为什么会发生栈溢出呢?因为我们的快速排序是利用递归实现的,有递归调用,就要建立函数栈帧,并且随着递归的深度越深所要建立的函数栈帧的消耗就越大 。如这幅图所示:

2.快速排序的优化

① 三数取中法选 key

为了应对最坏情况会出现时间复杂度为 O(N^2) 的情况,有人提出了三数取中的方法。

旧方法中,我们每次选 key 都是数列的第一个元素。三数取中的做法是,分别取数列的第一个元素、最后一个元素和最中间的元素,选出三个数中不是最大也不是最小的那个数当作 key 值。

有了三数取中,之前的最坏情况立马变成了最好情况。

代码实现

由于hoare法、挖坑法、前后指针法最终的效果都相同且效率差异很小,所以就任意选取一个为例,其余两者都类似。

  1. //三数取中的函数
  2. int GetMidIndex(int* a, int begin, int end)
  3. {
  4. int mid = (begin + end) / 2;
  5. if (a[begin] < a[mid])
  6. {
  7. if (a[mid] < a[end])
  8. {
  9. return mid;
  10. }
  11. else if (a[begin] > a[end])
  12. {
  13. return begin;
  14. }
  15. else
  16. {
  17. return end;
  18. }
  19. }
  20. else // a[begin] > a[mid]
  21. {
  22. if (a[mid] > a[end])
  23. {
  24. return mid;
  25. }
  26. else if (a[begin] < a[end])
  27. {
  28. return begin;
  29. }
  30. else
  31. {
  32. return end;
  33. }
  34. }
  35. }
  36. //hoare法
  37. void QuickSort(int* a, int begin, int end)
  38. {
  39. if (begin >= end)
  40. {
  41. return;
  42. }
  43. int mid = GetMidIndex(a, begin, end);
  44. Swap(&a[mid], &a[begin]);
  45. int left = begin;
  46. int right = end;
  47. int keyi = left;
  48. while (left < right)
  49. {
  50. while (left < right && a[right] >= a[keyi])
  51. {
  52. right--;
  53. }
  54. while (left < right && a[left] <= a[keyi])
  55. {
  56. left++;
  57. }
  58. Swap(&a[left], &a[right]);
  59. }
  60. Swap(&a[keyi], &a[left]);
  61. QuickSort(a, begin, left - 1);
  62. QuickSort(a, left + 1, end);
  63. }

② 小区间优化

随着递归的调用越深入,此时有个很大的缺点就是函数栈帧的消耗很大。但是同时又有一个好处,就是越往下,数列就越接近有序,且此时每个小区间的数据个数特别少。

那么有什么办法可以取其长处避其短处呢?不知道你是否还记得插入排序的特点&mdash;&mdash;数据越接近有序,效率就越高。并且,在数据量极少的情况下,时间复杂度为 O(N^2) 的插入排序与时间复杂度为 O(N*log N) 的快速排序基本没有什么区别。所以,我们干脆就在排序数据量少的数列时,采用插入排序代替。

代码实现

  1. //三数取中的函数
  2. int GetMidIndex(int* a, int begin, int end)
  3. {
  4. int mid = (begin + end) / 2;
  5. if (a[begin] < a[mid])
  6. {
  7. if (a[mid] < a[end])
  8. {
  9. return mid;
  10. }
  11. else if (a[begin] > a[end])
  12. {
  13. return begin;
  14. }
  15. else
  16. {
  17. return end;
  18. }
  19. }
  20. else // a[begin] > a[mid]
  21. {
  22. if (a[mid] > a[end])
  23. {
  24. return mid;
  25. }
  26. else if (a[begin] < a[end])
  27. {
  28. return begin;
  29. }
  30. else
  31. {
  32. return end;
  33. }
  34. }
  35. }
  36. //插入排序
  37. void InsertSort(int* a, int n)
  38. {
  39. for (int i = 0; i < n - 1; i++)
  40. {
  41. int end = i;
  42. int tmp = a[end + 1];
  43. while (end >= 0)
  44. {
  45. if (a[end] > tmp) //大于tmp,往后挪一个
  46. {
  47. a[end + 1] = a[end];
  48. end--;
  49. }
  50. else
  51. {
  52. break;
  53. }
  54. }
  55. a[end + 1] = tmp; //把tmp插入空隙
  56. }
  57. }
  58. //hoare法
  59. void QuickSort(int* a, int begin, int end)
  60. {
  61. if (begin >= end)
  62. {
  63. return;
  64. }
  65. if ((end - begin + 1) < 15)
  66. {
  67. // 小区间用直接插入替代,减少递归调用次数
  68. InsertSort(a+begin, end - begin + 1);
  69. }
  70. else
  71. {
  72. int mid = GetMidIndex(a, begin, end);
  73. Swap(&a[mid], &a[begin]);
  74. int left = begin;
  75. int right = end;
  76. int keyi = left;
  77. while (left < right)
  78. {
  79. while (left < right && a[right] >= a[keyi])
  80. {
  81. right--;
  82. }
  83. while (left < right && a[left] <= a[keyi])
  84. {
  85. left++;
  86. }
  87. Swap(&a[left], &a[right]);
  88. }
  89. Swap(&a[keyi], &a[left]);
  90. QuickSort(a, begin, left - 1);
  91. QuickSort(a, left + 1, end);
  92. }
  93. }

两外两种方法的代码实现已打包完成,可在文末直接取用。

5. 快速排序的非递归实现

快速排序的非递归思路与递归相差无几,唯一不同的是,非递归用栈或队列模拟函数递归建立栈帧的过程。

  1. void QuickSortNonR(int* a, int begin, int end)
  2. {
  3. Stack st;
  4. StackInit(&st);
  5. StackPush(&st, begin);
  6. StackPush(&st, end);
  7. while (!StackEmpty(&st))
  8. {
  9. int right = StackTop(&st);
  10. StackPop(&st);
  11. int left = StackTop(&st);
  12. StackPop(&st);
  13. int keyi = PartSort1(a, left, right);//三种方法任选其一
  14. //int keyi = PartSort2(a, left, right);
  15. //int keyi = PartSort3(a, left, right);
  16. if (keyi + 1 < right)
  17. {
  18. StackPush(&st, keyi + 1);
  19. StackPush(&st, right);
  20. }
  21. if (left < keyi - 1)
  22. {
  23. StackPush(&st, left);
  24. StackPush(&st, keyi - 1);
  25. }
  26. }
  27. StackDestroy(&st);
  28. }

附录

以上就是如何使用C语言实现快速排序的详细内容,更多关于如何使用C语言实现快速排序的资料请关注九品源码其它相关文章!