数据结构与算法系列之一:八大排序之快速排序

喜欢ヅ旅行 2022-06-02 08:57 267阅读 0赞

  • 转载请注明作者和出处:http://blog.csdn.net/u011475210
  • 个人博客:https://wordzzzz.github.io/ && https://wordzzzz.gitee.io/
  • 代码地址:https://github.com/WordZzzz/Note/tree/master/DS-A
  • 博客作者:WordZzzz,一只热爱技术的文艺青年

    • 快速排序

      • 前言
      • 简介
      • 步骤
      • 代码

        • 公用接口
        • 递归法
        • 迭代法
      • 算法复杂度
      • 分析

        • 局限性
        • 优化

快速排序

前言

  建议先看排序综述,传送门:数据结构与算法系列之一:八大排序综述。

简介

  快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序 n 个项目要 O(nlogn) (大O符号)次比较。在最坏状况下则需要 O(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 O(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

步骤

  快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

  步骤为:

  • 从数列中挑出一个元素,称为”基准”(pivot).
  • 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
  • 递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

  wikipedia的大数据规模演示:

quicksort from wikipedia

  wordzzzz的小数据规模演示:

quicksort from wordzzzz

代码

  先给出公用接口,之后的三个递归实现和一个迭代实现在代码中都有详细的说明,我就不再在此赘述。

公用接口

  1. /*
  2. * 快速排序主体函数(递归)
  3. */
  4. template <typename T>
  5. void QuickSort(T *array, const int length) {
  6. if (array == NULL)
  7. throw invalid_argument("Array must not be empty");
  8. if (length <= 0)
  9. return;
  10. Partion1(array, 0, length - 1);
  11. // Partion2(array, 0, length - 1);
  12. // Partion3(array, 0, length - 1);
  13. // PartionInsert(array, 0, length - 1);
  14. // PartionSecond(array, 0, length - 1);
  15. // PartionThird(array, 0, length - 1);
  16. }

递归法

  1. /*
  2. * 快速排序1:
  3. * 将第一个元素array[left]提出来作pivot,i=left(该索引之前的数比pivot小,初始值为left),
  4. * j从left+1开始遍历数组,找到一个比pivot小的数,i+1,如果i和j序列号不等就交换(小值到前)。
  5. * j到最右端之后,for循环结束,再把pivot与i所指数据做交换,当前pivot就到达了它的最终位置。
  6. */
  7. template <typename T>
  8. void Partion1(T *array, int left, int right){
  9. if (left >= right)
  10. return;
  11. int i = left;
  12. int j = left + 1;
  13. T pivot = array[left]; // 取第一个数为基准
  14. for (; j <= right; ++j){ // 循环直至 j 扫描至 right
  15. if (array[j] < pivot){ // 如果遇到比基准小的数,i右移一位
  16. i++;
  17. if (j != i){ // 如果i与j不重合,则交换他们指向的值
  18. swap(array[j],array[i]);
  19. }
  20. }
  21. }
  22. swap(array[left], array[i]); // 基准值的位置确定
  23. Partion1(array, left, i - 1);
  24. Partion1(array, i + 1, right);
  25. }
  26. /*
  27. * 快速排序2:
  28. * 将第一个元素array[left]提出来作pivot,i = left+1从左向右遍历找到一个比pivot大的数停止,
  29. * 然后等待j从右往左遍历找到一个pivot小的数,两者交换,然后继续寻找直到i=j,for循环结束。
  30. * 之后我们需要做判断,如果pivot比i所指数据大就交换两者,否则i回退一步(因为开始忽略了首元素)。
  31. */
  32. template <typename T>
  33. void Partion2(T *array, int left, int right) {
  34. if (left >= right)
  35. return;
  36. int i = left + 1;
  37. int j = right;
  38. T pivot = array[left]; // 取第一个数为基准
  39. while (i < j){ // 循环直至 i,j 相遇
  40. while (i < j && array[j] >= pivot) // j向左遍历,直到找到比pivot小的值
  41. --j;
  42. while (i < j && array[i] < pivot) // i向右遍历,直到找到比pivot大的值
  43. ++i;
  44. if (i < j) // 如果i < j,就交换刚才找到的那两个值
  45. swap(array[j], array[i]);
  46. }
  47. if (array[i] <= array[left]) // 这里一定要做判断再决定是否交换
  48. swap(array[i], array[left]);
  49. else // 如果不交换,说明left是最小,但i是不是第二小不确定,所以需要下次判断
  50. --i;
  51. Partion2(array, left, i - 1);
  52. Partion2(array, i + 1, right);
  53. }
  54. /*
  55. * 快速排序3:
  56. * 将第一个元素array[left]提出来作pivot,然后从j = right向前搜索第一个比pivot小的元素假设为array[k],
  57. * 该元素放在array[left]的位置。因为array[left]已经保存pivot覆盖也没关系,于是array[k]又可以被覆盖了,
  58. * 从前往后搜索比pivot大的元素放到array[k]。一直进行下去直到i=j。
  59. */
  60. template <typename T>
  61. void Partion3(T *array, int left, int right) {
  62. if (left >= right)
  63. return;
  64. int i = left;
  65. int j = right;
  66. T pivot = array[left]; // 取第一个数为基准
  67. while (i < j){ // 循环直至 i,j 相遇
  68. while (i < j && array[j] >= pivot)
  69. --j;
  70. if (i < j)
  71. array[i++] = array[j]; // 从右向左扫描,将比基准小的数填到左边
  72. while (i < j && array[i] < pivot)
  73. ++i;
  74. if (i < j)
  75. array[j--] = array[i]; // 从左向右扫描,将比基准大的数填到右边
  76. }
  77. array[i] = pivot; // 将基准数填回
  78. Partion3(array, left, i - 1);
  79. Partion3(array, i + 1, right);
  80. }

迭代法

  1. /*
  2. * 快速排序迭代实现
  3. */
  4. template<typename T>
  5. void QuickSortIteration(T *array, const int length) {
  6. stack<pair<int, int>> trace;
  7. trace.push(make_pair(0, length - 1)); // 将数组首尾压栈
  8. while (!trace.empty()) {
  9. auto top = trace.top(); // 将栈顶元素保存下来
  10. trace.pop(); // 弹出栈顶
  11. int i = top.first; // 取出首尾地址
  12. int j = top.second;
  13. T pivot = array[i]; // 取第一个数为基准
  14. while (i < j) { // 循环直至 i,j 相遇
  15. while (i < j && array[j] >= pivot)
  16. --j;
  17. if (i < j)
  18. array[i++] = array[j]; // 从右向左扫描,将比基准小的数填到左边
  19. while (i < j && array[i] < pivot)
  20. ++i;
  21. if (i < j)
  22. array[j--] = array[i]; // 从左向右扫描,将比基准大的数填到右边
  23. }
  24. array[i] = pivot; // 将基准数填回
  25. if (i > top.first) trace.push(make_pair(top.first, i - 1));
  26. if (j < top.second) trace.push(make_pair(j + 1, top.second));
  27. }
  28. }

算法复杂度

  • 数据结构 不定
  • 最坏时间复杂度 O(n2)
  • 最优时间复杂度 O(nlogn)
  • 平均时间复杂度 O(nlogn)
  • 空间复杂度 根据实现的方式不同而不同

分析

  快速排序是二叉查找树(二叉查找树)的一个空间最优化版本。不是循序地把数据项插入到一个明确的树中,而是由快速排序组织这些数据项到一个由递归调用所隐含的树中。这两个算法完全地产生相同的比较次数,但是顺序不同。对于排序算法的稳定性指标,原地分区版本的快速排序算法是不稳定的。其他变种是可以通过牺牲性能和空间来维护稳定性的。

  快速排序的最直接竞争者是堆排序(Heapsort)。堆排序通常比快速排序稍微慢,但是最坏情况的运行时间总是 O(nlogn) 。快速排序是经常比较快,除了introsort变化版本外,仍然有最坏情况性能的机会。如果事先知道堆排序将会是需要使用的,那么直接地使用堆排序比等待introsort再切换到它还要快。堆排序也拥有重要的特点,仅使用固定额外的空间(堆排序是原地排序),而即使是最佳的快速排序变化版本也需要 O(logn) 的空间。然而,堆排序需要有效率的随机存取才能变成可行。

  快速排序也与归并排序(Mergesort)竞争,这是另外一种递归排序算法,但有坏情况 O(nlogn) 运行时间的优势。不像快速排序或堆排序,归并排序是一个稳定排序,且可以轻易地被采用在链表(linked list)和存储在慢速访问媒体上像是磁盘存储或网络连接存储的非常巨大数列。尽管快速排序可以被重新改写使用在链串列上,但是它通常会因为无法随机存取而导致差的基准选择。归并排序的主要缺点,是在最佳情况下需要 Ω(n) 额外的空间。

局限性

  快排的优化、归并排序的优化一向是面试的考察重点,至于算法的优化,重点还是要知道现有算法的不足之处。

  • 当序列长度很小时,快排效率低,研究表明长度在5~25的数组,快排表现不如插入排序。
  • 当pivot选择不当是,会导致树的不平衡,这样导致快排的时间复杂度为O(n2)。
  • 当数组中有大量重复的元素,快排效率将非常之低。

优化

  针对上面提出的快排的局限性,我们依次做出优化策略:

  • 当当前序列长度小于特定值时,直接采用插入排序,或者不做处理,等到快排都执行完毕后(大致有序)在执行一次插入排序。
  • 针对pivot的选择,不再选取固定值,而是采用其他选取策略,如随机、三值取中等。
  • 如果数组中重复元素多,就采用三路划分算法:以某个数为基准将一个数组分成三部分:第一部分表示小于该pivot,第二部分等于pivot,第三部分大于pivot,要得到三部分得区间范围。

  下面的代码是对上述改进算法的实现:

  1. /*
  2. * 快速排序3优化1:
  3. * 当排序的子序列小于预定的值M时,采用插入排序
  4. */
  5. template <typename T>
  6. void PartionInsert(T *array, int left, int right) {
  7. if (left >= right)
  8. return;
  9. if (right - left <= M)
  10. InsertSort(array, left, right);
  11. else{
  12. int i = left;
  13. int j = right;
  14. T pivot = array[left]; // 取第一个数为基准
  15. while (i < j){ // 循环直至 i,j 相遇
  16. while (i < j && array[j] >= pivot)
  17. --j;
  18. if (i < j)
  19. array[i++] = array[j]; // 从右向左扫描,将比基准小的数填到左边
  20. while (i < j && array[i] < pivot)
  21. ++i;
  22. if (i < j)
  23. array[j--] = array[i]; // 从左向右扫描,将比基准大的数填到右边
  24. }
  25. array[i] = pivot; // 将基准数填回
  26. PartionInsert(array, left, i - 1);
  27. PartionInsert(array, i + 1, right);
  28. }
  29. }
  30. //产生随机数
  31. template <typename T>
  32. void Random(T *array, int left, int right)
  33. {
  34. int size = right - left + 1;
  35. int i = left + rand() % size;
  36. swap(array[i], array[left]);
  37. }
  38. //取中位数移至left
  39. template <typename T>
  40. void Median(T *array, int left, int right)
  41. {
  42. int mid = left + ((right - left )>> 1);
  43. int minIndex = right;
  44. if (array[minIndex] > array[mid])
  45. minIndex = mid;
  46. if (array[minIndex] > array[left])
  47. minIndex = left;
  48. if (minIndex != right) //三个判断,把最小值移到最右侧
  49. swap(array[minIndex], array[right]);
  50. if (array[mid] < array[left]) //那么剩下的两个数,最小的那个就是中位数了
  51. swap(array[left], array[mid]);
  52. }
  53. /*
  54. * 快速排序3优化2:
  55. * 取随机数或者三值取中作为基准值
  56. */
  57. template <typename T>
  58. void PartionSecond(T *array, int left, int right) {
  59. if (left >= right)
  60. return;
  61. // Random(array, left, right); // 优化2-1:取随机数至最左端(基准值)
  62. Median(array, left, right); // 优化2-2:取中位数至最左端(基准值)
  63. int i = left;
  64. int j = right;
  65. T pivot = array[left]; // 取第一个数为基准
  66. while (i < j){ // 循环直至 i,j 相遇
  67. while (i < j && array[j] >= pivot)
  68. --j;
  69. if (i < j)
  70. array[i++] = array[j]; // 从右向左扫描,将比基准小的数填到左边
  71. while (i < j && array[i] < pivot)
  72. ++i;
  73. if (i < j)
  74. array[j--] = array[i]; // 从左向右扫描,将比基准大的数填到右边
  75. }
  76. array[i] = pivot; // 将基准数填回
  77. PartionSecond(array, left, i - 1);
  78. PartionSecond(array, i + 1, right);
  79. }
  80. /*
  81. * 快速排序3优化3:
  82. * 重复数据比较多的话,可以分为小于等于大于三段
  83. */
  84. template <typename T>
  85. void PartionThird(T *array, int left, int right) {
  86. if (left >= right)
  87. return;
  88. int less = left;
  89. int greater = right;
  90. int it = left;
  91. T pivot = array[left]; // 取第一个数为基准
  92. while (it <= greater){ // 循环直至it和greater相遇
  93. if (array[it] == pivot) // 如果等于pivot,it右移
  94. ++it;
  95. else if (array[it] < pivot){ // 如果小于pivot,扔左边,it和less右移
  96. swap(array[less], array[it]);
  97. ++it;
  98. ++less;
  99. }
  100. else{ // 如果大于pivot,扔右边,greater左移
  101. swap(array[greater], array[it]);
  102. --greater;
  103. }
  104. }
  105. PartionThird(array, left, less - 1);
  106. PartionThird(array, greater + 1, right);
  107. }

系列教程持续发布中,欢迎订阅、关注、收藏、评论、点赞哦~~( ̄▽ ̄~)~

完的汪(∪。∪)。。。zzz

发表评论

表情:
评论列表 (有 0 条评论,267人围观)

还没有评论,来说两句吧...

相关阅读