当前位置:   article > 正文

快速排序的原理及其多种方法的实现和优化

快速排序的原理及其多种方法的实现和优化

✨✨✨学习的道路很枯燥,希望我们能并肩走下来!


前言

本篇详细介绍了快速排序,让使用者对快速排序有进一步认识,而不是仅仅停留在表面,更好的模拟,为了更好的使用. 文章可能出现错误,如有请在评论区指正,让我们一起交流,共同进步!


一、快速排序介绍

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

  1. // 假设按照升序对array数组中[left, right)区间中的元素进行排序
  2. void QuickSort(int array[], int left, int right)
  3. {
  4. if(right - left <= 1)
  5. return;
  6. // 按照基准值对array数组的 [left, right)区间中的元素进行划分
  7. int div = partion(array, left, right);
  8. // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
  9. // 递归排[left, div)
  10. QuickSort(array, left, div);
  11. // 递归排[div+1, right)
  12. QuickSort(array, div+1, right);
  13. }

 上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉 树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

二、快速排序实现的方法(升序)

1.hoare版本:

 思路图如下:

 我们先对单趟升序排序进行分析:

1.从右边先走(后面会解释),右找比key小,找到停止,否则继续向左前进

2.右边停止后,左边开始找比key大的,找到停止,否则继续向右前进

3.两边都停止时,且未相遇,则交换

4.相遇时,则将下标为key与下表为left的值进行交换,此时key到达正确位置

  1. void PartSort1(int* a, int left, int right)
  2. {
  3. int keyi = left;
  4. int begin = left;
  5. int end = right;
  6. while (left < right)
  7. {
  8. if (left < right && a[right] >= a[keyi]) //避免数组内找不到导致left或right无法停止
  9. {
  10. right--;
  11. }
  12. if (left < right && a[left] <= a[keyi])
  13. {
  14. left++;
  15. }
  16. Swap(&a[left], &a[right]);
  17. }
  18. Swap(&a[left], &a[keyi]);
  19. keyi = left;
  20. }

 现在我们对已经找到key的正确位置了

此时数组内可分为

对begin到keyi-1的数据我们也可以看作一个完整的数组,这样将大问题分成小问题解决,我们就利用递归来解决这道问题。

  1. void Swap(int* px, int* py)
  2. {
  3. int tmp = *px;
  4. *px = *py;
  5. *py = tmp;
  6. }
  7. void PartSort1(int* a, int left, int right) //升序左找大右找小;
  8. {
  9. if (left >= right) // 区间只有一个值或者不存在就是最小子问题
  10. {
  11. return;
  12. }
  13. int keyi = left;
  14. int begin = left;
  15. int end = right;
  16. while (left < right)
  17. {
  18. if (left < right && a[right] >= a[keyi])
  19. {
  20. right--;
  21. }
  22. if (left < right && a[left] <= a[keyi])
  23. {
  24. left++;
  25. }
  26. Swap(&a[left], &a[right]);
  27. }
  28. Swap(&a[left], &a[keyi]);
  29. keyi = left;
  30. PartSort1(a, begin, keyi - 1); //递归左
  31. PartSort1(a, keyi + 1, end); //递归右
  32. }

 到这里我们代码基本完成

很多人想问,为什么一定要从右边开始找呢?

 

2.挖坑法

思路图如下:

 这个思路核心仍然是hoare的方法,但不需要区分左走还是右走,更便于教学理解

代码如下:

  1. int PartSort2(int* a, int begin, int end)
  2. {
  3. //begin是坑
  4. int key = a[begin];
  5. while (begin < end)
  6. {
  7. while (begin < end && a[end] >= key)
  8. --end;
  9. // end给begin这个坑,end就变成了新的坑。
  10. a[begin] = a[end];
  11. while (begin < end && a[begin] <= key)
  12. ++begin;
  13. // end给begin这个坑,begin就变成了新的坑。
  14. a[end] = a[begin];
  15. }
  16. a[begin] = key;
  17. return begin;
  18. }

3.前后指针法 

核心

1.cur>=key,cur++

2.cur<key,++prev,交换prev和cur的值,cur++

 prev和cur的关系:

1. prev紧跟cur一前一后

2.prev和cur之间是比key大的值

根据这些以及递归思想,我们可以写出如下代码 

  1. void PartSort3(int* a, int left, int right)
  2. {
  3. if (left >= right) // 区间只有一个值或者不存在就是最小子问题
  4. return;
  5. int keyi = left;
  6. int cur = left + 1;
  7. int prev = left;
  8. while (cur <= right)
  9. {
  10. if (a[cur] < a[keyi] && ++prev != cur)
  11. Swap(&a[cur], &a[prev]);
  12. cur++;
  13. }
  14. Swap(&a[keyi], &a[prev]);
  15. keyi = prev;
  16. PartSort3(a, left, keyi - 1);
  17. PartSort3(a, keyi + 1, right);
  18. }

 三、快速排序的优化

1. 关于所排序的数据有序或接近有序的问题

假设我们得到一个有序或者接近有序的数据,利用我们之前写的快速排序,我们会发现快速排序却不如时间复杂度高的排序方法,这是为什么呢?

如图所示,在数据有序的情况下,我们每一次都 要遍历整个数组来确定key的位置,时间复杂度达到了O(N^2),这显然不能符合他的“快速”

因此我们要对此进行优化

1.1 随机取key方法

在有序问题中,我们发现取key基本上是最小或最大的数值,导致left基本不懂,right几乎每次都要到数组靠前的位置,因此,我们如果取的key为随机呢。

  1. void QuickSort1(int* a, int left, int right)
  2. {
  3. // 区间只有一个值或者不存在就是最小子问题
  4. if (left >= right)
  5. return;
  6. int begin = left, end = right;
  7. //选[left, right]区间中的随机数做key
  8. int randi = rand() % (right - left + 1);
  9. randi += left;
  10. Swap(&a[left], &a[randi]);
  11. int keyi = left;
  12. while (left < right)
  13. {
  14. // right先走,找小
  15. while (left < right && a[right] >= a[keyi])
  16. {
  17. --right;
  18. }
  19. // left再走,找大
  20. while (left < right && a[left] <= a[keyi])
  21. {
  22. ++left;
  23. }
  24. Swap(&a[left], &a[right]);
  25. }
  26. Swap(&a[left], &a[keyi]);
  27. keyi = left;
  28. // [begin, keyi-1]keyi[keyi+1, end]
  29. QuickSort1(a, begin, keyi - 1);
  30. QuickSort1(a, keyi + 1, end);
  31. }
  32. }

1.2 三数取中法

顾名思义,就是取key为[left,right]区间内的中间值

取中代码如下:

  1. int GetMidi(int* a, int left, int right)
  2. {
  3. int mid = (left + right) / 2;
  4. if (a[left] < a[mid])
  5. {
  6. if (a[mid] < a[right])
  7. {
  8. return mid;
  9. }
  10. else if (a[left] > a[right])
  11. {
  12. return left;
  13. }
  14. else
  15. {
  16. return right;
  17. }
  18. }
  19. else // a[left] > a[mid]
  20. {
  21. if (a[mid] > a[right])
  22. {
  23. return mid;
  24. }
  25. else if (a[left] < a[right])
  26. {
  27. return left;
  28. }
  29. else
  30. {
  31. return right;
  32. }
  33. }
  34. }

整体代码如下:

  1. void QuickSort1(int* a, int left, int right)
  2. {
  3. // 区间只有一个值或者不存在就是最小子问题
  4. if (left >= right)
  5. return;
  6. int begin = left, end = right;
  7. // 三数取中
  8. int midi = GetMidi(a, left, right);
  9. Swap(&a[left], &a[midi]);
  10. int keyi = left;
  11. while (left < right)
  12. {
  13. // right先走,找小
  14. while (left < right && a[right] >= a[keyi])
  15. {
  16. --right;
  17. }
  18. // left再走,找大
  19. while (left < right && a[left] <= a[keyi])
  20. {
  21. ++left;
  22. }
  23. Swap(&a[left], &a[right]);
  24. }
  25. Swap(&a[left], &a[keyi]);
  26. keyi = left;
  27. // [begin, keyi-1]keyi[keyi+1, end]
  28. QuickSort1(a, begin, keyi - 1);
  29. QuickSort1(a, keyi + 1, end);
  30. }
  31. }

 

2.关于递归深度过深导致栈溢出问题 

2.1 利用栈实现非递归结构

  1. #include"Stack.h"
  2. void QuickSortNonR(int* a, int left, int right)
  3. {
  4. Stack st;
  5. StackInit(&st);
  6. StackPush(&st, right); //将区间压入
  7. StackPush(&st, left);
  8. while (!StackEmpty(&st))
  9. {
  10. int begin = StackTop(&st);
  11. StackPop(&st);
  12. int end = StackTop(&st);
  13. StackPop(&st);
  14. //单趟
  15. int keyi = begin;
  16. int cur = begin + 1;
  17. int prev = begin;
  18. while (cur <= end)
  19. {
  20. if (a[cur] < a[keyi] && ++prev != cur)
  21. Swap(&a[cur], &a[prev]);
  22. cur++;
  23. }
  24. Swap(&a[keyi], &a[prev]);
  25. keyi = prev;
  26. // [begin, keyi-1] keyi [keyi+1, end]
  27. if (keyi + 1 < end)
  28. {
  29. StackPush(&st, end);
  30. StackPush(&st, keyi+1);
  31. }
  32. if (begin < keyi - 1)
  33. {
  34. StackPush(&st, keyi-1);
  35. StackPush(&st, begin);
  36. }
  37. }
  38. StackDestroy(&st);
  39. }

2.2 小区间优化

快速排序在大量数据的处理上有着显著的优势,但在小区间内的排序速度有时却不如插入排序等时间复杂度高的排序.

快排递归到小区间时,我们可以采用插入排序进行优化,减少递归次数,提速和减少空间利用。

代码如下

  1. if (right - left + 1 < 10)
  2. {
  3. InsertSort(a + left, right - left + 1);
  4. }

总结

✨✨✨各位读友,本篇分享到内容是否更好的让你理解了数据在内存中的存储,如果对你有帮助给个

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