写在前边

这篇文章呢,我们接着聊一下排序算法,我们之前已经谈到了简单插入排序 和ta的优化版希尔排序,这节我们要接触一个更“高级”的算法了--快速排序。

在做洛谷的时候,遇到了一道卡优化的题,如果没有去对快排进行优化的话,会有几个点是TLE的,后边我们可以围绕这道题来做各种优化,先来认识一下快速排序。

思路

假如我们的计算机每秒钟可以运行 10 亿次,那么对 1 亿个数进行排序,排序只需要 0.1 秒,而冒泡排序则需要 1 千万秒,达到 115 天之久,是不是很吓人?那有没有既不浪费空间又可以快一点的排序算法呢?那就是“快速排序”!

假设我们现在对“** 6 1 2 7 9 3 4 5 10 8”这 10 个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,这就是一个用来参照的数,待会儿你就知道它用来做啥了)。为了方便,就让第一个数 6 作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在 6 的右边,比基准数小的数放在 6 的左边**,类似下面这种排列。

3 1 2 5 4 6 9 7 10 8

在初始状态下,数字 6 在序列的第 1 位。我们的目标是将 6 挪到序列中间的某个位置,假设这个位置是 k。现在就需要寻找这个 k,并且以第 k 位为分界点,左边的数都小于等于 6,右边的数都大于等于 6。

方法其实很简单:分别从初始序列“ 6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于 6 的数,再从左往右找一个大于 6 的数,然后交换它们。这里可以用两个变量 i 和 j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵 i”和“哨兵 j”。刚开始的时候让哨兵 i 指向序列的最左边(即 i=1),指向数字 6。让哨兵 j 指向序列的最右边(即 j=10),指向数字 8。





图源: 《啊哈算法》

完整流程图

如果mid取最左端的话,当本身有序时,会沦为冒泡排序,变成n平方

(例题)洛谷1177快速排序

https://www.luogu.com.cn/problem/P1177

错误版本

无优化,有序会TLE

  1. #include <iostream>
  2. #include<cstdio>
  3. int a[1000001];//定义全局变量,这两个变量需要在子函数中使用
  4. void swap(int* a, int* b) {
  5. int temp;
  6. temp = *a;
  7. *a = *b;
  8. *b = temp;
  9. }
  10. //递归方法
  11. void quicksort(int left, int right) {
  12. //举例子可得出若左边大于等于右边就返回?
  13. //大于的情况是:index刚好==right,然后还要再去(index+1,right)
  14. if (left >= right) {
  15. return;
  16. }
  17. //默认基准index是最左边那个,且i从左边开始,j从右边开始
  18. int i = left, j = right, index = left;
  19. //重构方法
  20. //大前提,两个没相遇
  21. while (i != j) {
  22. //里边还得判断一下i<j,因为外边的大前提,里边可能会破坏掉
  23. while (a[j] >= a[left]&&i<j) {
  24. j--;
  25. }
  26. while(a[i] <= a[left]&&i<j){
  27. i++;
  28. }
  29. //若i,j还未相遇
  30. if (i < j) {
  31. swap(&a[i], &a[j]);
  32. }
  33. }
  34. //出来时i,j必然相遇
  35. swap(&a[i],&a[index]);
  36. //将i赋值给基准
  37. index = i;
  38. quicksort(left, index - 1);
  39. quicksort(index + 1, right);
  40. }

先取到mid,然后让左边跟mid交换

由于是单边搜索,后两个点都TLE

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <stdlib.h>
  4. #include <math.h>
  5. #include <ctype.h>
  6. #include <time.h>
  7. void swap(int arr[], int i, int j)
  8. {
  9. int temp;
  10. temp = arr[i];
  11. arr[i] = arr[j];
  12. arr[j] = temp;
  13. }
  14. void QuickSort(int arr[], int left, int right)
  15. {
  16. int i, pivot;
  17. if (left >= right)
  18. return;
  19. pivot = left;
  20. swap(arr, left, (left + right) / 2);
  21. for (i = left + 1; i <= right; i++) //单边搜索,可以该为双向搜索
  22. if (arr[i] < arr[left])
  23. swap(arr, i, ++pivot);
  24. swap(arr, left, pivot);
  25. QuickSort(arr, left, pivot - 1);
  26. QuickSort(arr, pivot + 1, right);
  27. }
  28. int main()
  29. {
  30. int n;
  31. int *arr;
  32. int i;
  33. scanf("%d", &n);
  34. arr = (int*)malloc(sizeof(int) * (n + 1));
  35. for (i = 1; i <= n; i++)
  36. scanf("%d", arr + i);
  37. QuickSort(arr, 1, n);
  38. for (i = 1; i <= n; i++)
  39. printf("%d ", arr[i]);
  40. return 0;
  41. }

改进成双边搜索

只是最后一个点TLE了,而最后一个点,刚好就是常数列的情况

  1. #include <iostream>
  2. #include<cstdio>
  3. int a[100010];//定义全局变量,这两个变量需要在子函数中使用
  4. void swap(int* a, int* b) {
  5. int temp;
  6. temp = *a;
  7. *a = *b;
  8. *b = temp;
  9. }
  10. //递归方法
  11. void quickSort(int left, int right) {
  12. //举例子可得出若左边大于等于右边就返回?
  13. //大于的情况是:index刚好==right,然后还要再去(index+1,right)
  14. if (left >= right) {
  15. return;
  16. }
  17. //默认基准index是最左边那个,且i从左边开始,j从右边开始
  18. int i = left, j = right, index ;
  19. int mid=(left+right)/2;
  20. swap(&a[left],&a[mid]);
  21. index=left;
  22. //重构方法
  23. //大前提,两个没相遇
  24. while (i != j) {
  25. //里边还得判断一下i<j,因为外边的大前提,里边可能会破坏掉
  26. while (a[j] >= a[left]&&i<j) {
  27. j--;
  28. }
  29. while(a[i] <= a[left]&&i<j){
  30. i++;
  31. }
  32. //若i,j还未相遇
  33. if (i < j) {
  34. swap(&a[i], &a[j]);
  35. }
  36. }
  37. //出来时i,j必然相遇
  38. swap(&a[i],&a[index]);
  39. index = i;
  40. quickSort(left, index - 1);
  41. quickSort(index + 1, right);
  42. }
  43. int main() {
  44. int n;
  45. scanf("%d", &n);
  46. for (int i = 0; i < n; i++) {
  47. scanf("%d", &a[i]);
  48. }
  49. quickSort(0, n - 1);
  50. for (int i = 0; i < n; i++) {
  51. i == n - 1 ? printf("%d", a[i]) : printf("%d ", a[i]);
  52. }
  53. }

复盘mid(考虑常数列)

还没三数取中,取到的mid是最小的话还是会退化到冒泡n平方

  1. #include<cstdio>
  2. #include<iostream>
  3. using namespace std;
  4. int a[100010];
  5. //交换元素位置
  6. void swap(int* a, int* b) {
  7. int temp;
  8. temp = *a;
  9. *a = *b;
  10. *b = temp;
  11. }
  12. void quickSort(int* arr, int low, int high) {
  13. //若长度已经不符合要求,直接返回
  14. if (low >= high) {
  15. return;
  16. }
  17. int left = low;
  18. int right = high;
  19. //选取中间为基准,注意是取中间的值,而不是下标,因为下标可能在后续交换中会改变
  20. //比如left走到了mid这里,而right停在了mid右边,这时会去交换,arr[mid]就变了
  21. int mid = arr[(low + high) >> 1];
  22. while (left <= right) {
  23. //注意是小于,如果是小于等于的话,常数列就会一直left移动,而right不移动,沦为n平方
  24. while (arr[left] < mid) {
  25. left++;
  26. }
  27. //同样注意是大于
  28. while (arr[right] > mid) {
  29. right--;
  30. }
  31. //这里要注意是小于或等于,以便于left和right直接跳到枢纽点的左右
  32. if (left <= right) {
  33. swap(&arr[left], &arr[right]);
  34. left++;
  35. right--;
  36. }
  37. }
  38. //递归调用
  39. //right走到了区间一的尾部
  40. quickSort(arr,low, right);
  41. //left走到了区间二的头部
  42. quickSort(arr, left, high);
  43. }
  44. int main()
  45. {
  46. int n, i;
  47. cin >> n;
  48. for (i = 1; i <= n; i++)
  49. {
  50. cin >> a[i];
  51. }
  52. quickSort(a,1,n);
  53. for (i = 1; i <= n; i++)
  54. {
  55. cout << a[i] << " ";
  56. }
  57. cout << endl;
  58. }

流程图

此处取到的mid是60

此时left和right已经相遇了

  • 然后left发现不满足条件,86>mid(60),left还在86

    • right满足条件,right去--,走到了15那里停下来
  • 然后left不满足<=right,直接就要开始继续递归了
    • 递归的区间①是: [42,15] (都小于等于60)
    • 区间②是 : [86,68] (都大于等于60)
  1. while (arr[left] < mid) {
  2. left++;
  3. }
  4. //同样注意是大于
  5. while (arr[right] > mid) {
  6. right--;
  7. }
  8. //这里要注意是小于或等于,以便于left和right直接跳到枢纽点的左右
  9. if (left <= right) {
  10. swap(&arr[left], &arr[right]);
  11. left++;
  12. right--;
  13. }
  14. //递归调用
  15. //right走到了区间一的尾部
  16. quickSort(arr,low, right);
  17. //left走到了区间二的头部
  18. quickSort(arr, left, high);

问题

因为没有去把枢纽放到正确的位置,导致最后其实分出来的区间长度会多出一个元素:枢轴(这样会很影响时间效率吗)

自行用一定数据量测试的话,时间效率上差距还算不上很大的。

**结合三数取中(暂时的神)

  1. #include<cstdio>
  2. #include<iostream>
  3. #include"RcdSqList.h"
  4. using namespace std;
  5. //int a[100010];
  6. //交换元素位置
  7. void swap(int* a, int* b) {
  8. int temp;
  9. temp = *a;
  10. *a = *b;
  11. *b = temp;
  12. }
  13. //三数取中
  14. int getmid(int* array, int left, int right)
  15. {
  16. int mid = left + ((right - left) >> 1);
  17. if (array[left] <= array[right])
  18. {
  19. if (array[mid] < array[left])
  20. return left;
  21. else if (array[mid] > array[right])
  22. return right;
  23. else
  24. return mid;
  25. }
  26. else
  27. {
  28. if (array[mid] < array[right])
  29. return right;
  30. else if (array[mid] > array[left])
  31. return left;
  32. else
  33. return mid;
  34. }
  35. }
  36. void quickSort(int* arr, int low, int high) {
  37. //若长度已经不符合要求,直接返回
  38. if (low >= high) {
  39. return;
  40. }
  41. int left = low;
  42. int right = high;
  43. //选取中间为基准,注意是取中间的值,而不是下标,因为下标可能在后续交换中会改变
  44. //比如left走到了mid这里,而right停在了mid右边,这时会去交换,arr[mid]就变了
  45. //调用三数取中,得到中间数
  46. int mid = arr[getmid(arr, low, high)];
  47. while (left <= right) {
  48. //注意是小于,如果是小于等于的话,常数列就会一直left移动,而right不移动,沦为n平方
  49. while (arr[left] < mid) {
  50. left++;
  51. }
  52. //同样注意是大于
  53. while (arr[right] > mid) {
  54. right--;
  55. }
  56. //这里要注意是小于或等于,以便于left和right直接跳到枢纽点的左右
  57. if (left <= right) {
  58. swap(&arr[left], &arr[right]);
  59. left++;
  60. right--;
  61. }
  62. }
  63. //递归调用
  64. //right走到了区间一的尾部
  65. quickSort(arr, low, right);
  66. //left走到了区间二的头部
  67. quickSort(arr, left, high);
  68. }
  69. int main()
  70. {
  71. int n, i;
  72. /*cin >> n;*/
  73. /*for (i = 1; i <= n; i++)
  74. {
  75. cin >> a[i];
  76. }*/
  77. int a[100] = { 0,42 ,90,30,86,42,15,57,20 };
  78. quickSort(a, 1, 8);
  79. for (i = 1; i <= 8; i++)
  80. {
  81. cout << a[i] << " ";
  82. }
  83. cout << endl;
  84. }

总结

最后是下载了测试点,然后看了博客,去找超时的原因,其实是完全有序

然后拿标准答案debug一下,终于发现了区别,就是常数列的时候他两个都移动,我只是一个移动,就退化成n平方了

注意

要<号而不是<=

**要去放大问题,比如j一直往左走,要放大到一直走走走走到i那里去了!

看黑马视频得出的感悟....

这个问题很严重,会变成n平方

如果把基准放最左边,而本身有序的话就会超时了

xxxx枢轴要选大小为中间的值,才能解决完全有序的情况(得三数取中.)

  • 注意这里的取中间值不是说取区间中间的值,而是大小在首尾区间中间数三个值排列居中的那个

    • 如果取的是区间中间的值,并不能解决完全有序的问题,比如 5 4 1 2 3 ,取mid等于1,又是取到了最小的情况,最后mid放到正确的位置(即第一个位置),递归他的左右,并没有起到把原区间化成两半的效果,只是得到了右边那一段,相当与只是减少了一个元素(类似冒泡的把一个元素放到正确的位置而已)

让left++的条件应该是<号! , 交换完要顺便让left++,right--,这样才能解决常数列的情况

如果我们不在交换完做移动的话,那>就得改成>=,这样才会移动,但就变成单指针移动了,还是退化成n平方了

交换完移动后,left和right刚好就是两个区间的首跟尾

**三向切割法

https://www.luogu.com.cn/problem/P1177(依旧是模板题)

先来看代码

  1. #include<cstdio>
  2. #include<iostream>
  3. using namespace std;
  4. int a[100010];
  5. //交换元素位置
  6. void swap(int* a, int* b) {
  7. int temp;
  8. temp = *a;
  9. *a = *b;
  10. *b = temp;
  11. }
  12. //三数取中
  13. int getmid(int* array, int left, int right)
  14. {
  15. int mid = left + ((right - left) >> 1);
  16. if (array[left] <= array[right])
  17. {
  18. if (array[mid] < array[left])
  19. return left;
  20. else if (array[mid] > array[right])
  21. return right;
  22. else
  23. return mid;
  24. }
  25. else
  26. {
  27. if (array[mid] < array[right])
  28. return right;
  29. else if (array[mid] > array[left])
  30. return left;
  31. else
  32. return mid;
  33. }
  34. }
  35. void quickSort_2(int rcd[], int low, int high) {
  36. if (low >= high) {
  37. return;
  38. }
  39. //调用三数取中,获取枢纽位置
  40. int pivot=getmid(rcd,low,high);
  41. //把枢纽值放到low位置(交换)
  42. swap(&rcd[low],&rcd[pivot]);
  43. //枢纽值就等于rcd[low]
  44. int pivotVal = rcd[low];
  45. //i用来遍历一趟区间
  46. int left, right, i;
  47. //直接从枢纽的下一位开始遍历区间
  48. left = low, right = high, i = low + 1;
  49. //遍历整个区间
  50. while (i <= right) {
  51. //若小于枢纽值
  52. if (rcd[i] < pivotVal) {
  53. //得放到前边,跟left交换
  54. swap(&rcd[i], &rcd[left]);
  55. //交换完,i换来了一个i前边的值,肯定比较过了,所以i++
  56. i++;
  57. //left换来了一个i原来位置的值,也比较过了,所以left++
  58. left++;
  59. }
  60. //若大于枢纽值
  61. else if(rcd[i]>pivotVal)
  62. {
  63. //得放到后边,跟right交换
  64. swap(&rcd[i], &rcd[right]);
  65. //right换来了一个i原来位置的值,也比较过了,所以right++
  66. right--;
  67. //i不动,因为换过来一个i后边的新的值,还没比较过
  68. }
  69. //等于的情况
  70. else
  71. {
  72. i++;
  73. }
  74. }
  75. quickSort_2(rcd, low, left - 1);
  76. quickSort_2(rcd, right + 1, high);
  77. }
  78. int main() {
  79. int n;
  80. scanf("%d", &n);
  81. for (int i = 0; i < n; i++) {
  82. scanf("%d", &a[i]);
  83. }
  84. quickSort_2(a,0, n - 1);
  85. for (int i = 0; i < n; i++) {
  86. i == n - 1 ? printf("%d", a[i]) : printf("%d ", a[i]);
  87. }
  88. }

粗糙的流程图

这里我们先默认枢轴就是最左边的元素就好了,方便理解(实际情况再按上边代码取中后跟最左边交换即可)

用一个i去遍历这个区间,还需要一个left和一个right指针

  • 当 rcd[i]大于枢纽值时,比如图中的90
  • 当rcd[i]小于枢纽值时,比如图中的20
  • 当rcd[i]等于枢纽值时,比如图中的42

具体看图中的文字说明

总结一下就是,如果该位置是一个已经跟枢纽值比较过了的值,或者换过来一个已经跟枢纽值比较过了的值(那就需要更新一下他的指针位置)

优势

  • 减少了对重复元素的比较操作,因为重复元素在一次排序中就已经作为单独一部分排好了,之后只需要对不等于该重复元素的其他元素进行排序。

写在最后

  • 到了快排这里,其实已经涉及到一些递归的知识,跟递归相关的其实还有“折半查找”、“归并排序”等,本专栏也还还会持续更新相关的知识,欢迎关注一起学习!

快速排序--洛谷卡TLE后最终我还是选择了三向切割的更多相关文章

  1. 洛谷 P1119 灾后重建 最短路+Floyd算法

    目录 题面 题目链接 题目描述 输入输出格式 输入格式 输出格式 输入输出样例 输入样例 输出样例 说明 思路 AC代码 总结 题面 题目链接 P1119 灾后重建 题目描述 B地区在地震过后,所有村 ...

  2. 洛谷——P1119 灾后重建

    P1119 灾后重建 题目背景 B地区在地震过后,所有村庄都造成了一定的损毁,而这场地震却没对公路造成什么影响.但是在村庄重建好之前,所有与未重建完成的村庄的公路均无法通车.换句话说,只有连接着两个重 ...

  3. 洛谷 1119 灾后重建 Floyd

    比较有趣的Floyd,刚开始还真没看出来....(下午脑子不太清醒) 先考虑一下Floyd本身的实现原理, for(k=1;k<=n;k++) for(i=1;i<=n;i++) for( ...

  4. 洛谷P1119 灾后重建[Floyd]

    题目背景 B地区在地震过后,所有村庄都造成了一定的损毁,而这场地震却没对公路造成什么影响.但是在村庄重建好之前,所有与未重建完成的村庄的公路均无法通车.换句话说,只有连接着两个重建完成的村庄的公路才能 ...

  5. 洛谷 [P1119] 灾后重建

    我们发现每次询问都是对于任意两点的,所以这是一道多源最短路径的题,多源最短路径,我们首先想到floyd,因为询问的时间是不降的,所以对于每次询问,我们将还没有进行松弛操作的的点k操作. #includ ...

  6. 洛谷P1119灾后重建

    题目 做一个替我们首先要明确一下数据范围,n<=200,说明n^3的算法是可以过得,而且这个题很明显是一个图论题, 所以我们很容易想到这个题可以用folyd, 但是我在做这个题的时候因为没有深刻 ...

  7. 洛谷P1119 灾后重建 Floyd + 离线

    https://www.luogu.org/problemnew/show/P1119 真是有故事的一题呢 半年前在宁夏做过一道类似的题,当时因为我的愚昧痛失了金牌. 要是现在去肯定稳稳的过,真是生不 ...

  8. 洛谷P1119 灾后重建

    传送门 题目大意:点被破坏,t[i]为第i个点修好的时间,且t[1]<t[2]<t[3].. 若干询问,按时间排序,询问第t时刻,u,v的最短路径长度. 题解:floyed 根据时间加入点 ...

  9. 洛谷P1119灾后重建——Floyd

    题目:https://www.luogu.org/problemnew/show/P1119 N很小,考虑用Floyd: 因为t已经排好序,所以逐个加点,Floyd更新即可: 这也给我们一个启发,如果 ...

随机推荐

  1. Layui的落幕,是否预示一个时代的结束?

    1.今天,看到LayUi(读音类UI)官方说,LayUI官网将关闭,多少有些伤感. 或许,有人会所,通知里也说了,"新版下载.文档和示例等仍会在Github 和 Gitee" 但, ...

  2. 重启ubuntu系统VMware tools失效处理方法

    1) sudo apt-get autoremove open-vm-tools 2) Install VMware Tools by following the usual method (Virt ...

  3. django 常用教程网址

    第一:url中反向解析教程网址 https://docs.djangoproject.com/zh-hans/2.2/ref/templates/builtins/#url

  4. 几分钟就能学会的Python虚拟环境教程

    什么是虚拟环境 我们在使用Python的时候,通常用pip来进行包管理.比如我们要安装一个叫requests的库,那么我们就会采用以下命令去安装: pip install requests 那你知道, ...

  5. easy-rule 学习

    Easyrule是个规则引擎,类似于drools,我们来熟悉一下这个东西 [ ] 一个简单实例规则,这个规则会被一直触发,然后行为是打印helloWorld @Rule(name="hell ...

  6. 我的Python学习记录

    Python日期时间处理:time模块.datetime模块 Python提供了两个标准日期时间处理模块:--time.datetime模块. 那么,这两个模块的功能有什么相同和共同之处呢? 一般来说 ...

  7. Python爬虫:通过做项目,小编了解了酷狗音乐的加密过程

    1.前言 小编在这里讲一下,下面的内容仅供学习参考,切莫用于商业活动,一经被相关人员发现,本小编概不负责!读者切记切记. 2.获取音乐播放列表 其实,这就是小编要讲的重点,因为就是这部分用到了加密. ...

  8. logback日志入门超级详细讲解

    基本信息 日志:就是能够准确无误地把系统在运行状态中所发生的情况描述出来(连接超时.用户操作.异常抛出等等): 日志框架:就是集成能够将日志信息统一规范后输出的工具包. Logback优势 Logba ...

  9. 现代 C++ 对多线程/并发的支持(上) -- 节选自 C++ 之父的 《A Tour of C++》

    本文翻译自 C++ 之父 Bjarne Stroustrup 的 C++ 之旅(A Tour of C++)一书的第 13 章 Concurrency.用短短数十页,带你一窥现代 C++ 对并发/多线 ...

  10. 微服务Cloud整体聚合工程创建过程

    1.父工程创建及使用 使用idea开发工具,选择File-new- project ,在选项中选择Maven工程,选择jdk版本1.8,勾选maven-archetype-site,点击next,输入 ...