1.定义
快速排序有三要素:
- 分区算法【如何将一个无序的数组分区,才是快速排序的关键】
- 递归【分治法解决问题】
- 合并问题【其实不用合并数组,因为数组在递归的时候就已经完全有序了】
2.算法思想
- 利用分治的方法【表现形式是递归】将一个数组由一个枢轴元素分成两部分。枢轴的左部分比枢轴元素小,枢轴元素的右部分比枢轴元素大。 【正确的描述应该是:枢轴左边的元素不比枢轴大,枢轴右边的元素不比枢轴小。】
- 取某无序数组区间的第一个元素【下标为 0 (不一定是0,枢轴元素下标是low,只有第一次递归时,才是0)开始计算】为枢轴元素,记为pivotKey。
- 初始时:low = 0,high = array.length-1【是否减一,则要看数组的存储是从0开始,还是1开始。】;
- 从high开始依次向左依次遍历数组,如果low<high && array[high] > pivotKey,则执行high--;反之,退出while循环;
- 从low开始依次向右遍历数组,如果low<high && array[low] < pivotKey,则执行low++;反之,退出while循环;
- 判断low是否小于等于high,若是,则将array[low]与array[high]交换;否则,不交换。
- 当low==high时,一轮排序结束,返回当前枢轴元素所在的下标。
- 枢轴元素下标左部分和右部分的无序数组再次进行排序。直至全部完成。
3.代码
3.1 伪代码
因为伪代码好理解,方便记忆,所以先给出文字版的伪代码,然后再给出具体代码。
- 伪代码
#include<iostream> using namespace std; int arr[maxN];//初始数组 //0.待排序的区间是 [low,high] void quickSort(int low,int high){ /*step 1.临界返回条件 01.如果只有一个元素(low==high),就不用排序了,直接返回就可以了。 02.如果区间low甚至比high还要大,肯定也是要返回的。出现这种情况的原因是: 每次排序只将一个元素正确的放到位置上。如果对于原本就有序的序列,在将high变成i-1时,就可能比low小了 (同样的道理,i+1可能比high大) */ if(low >= high) return ; /* step 2.移动元素,做区间的划分 01.选枢轴元素,做左右区间的划分 02.【重要】不能污染low,high,因为后面还要递归调用。所以赋值为i,j即可。 03.【重要】使用三个while循环,每次循环成立的条件都必须有 i<=j。 同时需要注意,左侧不小于时停止while循环,右侧不大于时停止while循环。 在代码中的体现就是,while循环中的条件没有等于号。 04.只有在i <= j时,才交换两个数 (等于号可以不要) */ /* step 3.递归对左右子区间排序 */ quickSort(low,i-1); quickSort(i+1,high); }
- 具体代码模板【记下来!】
#include<iostream> using namespace std; const int maxN = 100; int arr[maxN];//初始数组 void quickSort(int low,int high){ if(low >= high) return ; int pivotKey = arr[low]; int i = low,j = high; while( i < j) { while(arr[i] < pivotKey && i<=j ) { i++; } while(arr[j] > pivotKey && i<=j ){ j--; } if(i<j){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } quickSort(low,i-1); quickSort(i+1,high); } int main(){ int n; cin >> n; for(int i = 0;i< n;i++) cin >> arr[i]; quickSort(0,n-1); for(int i = 0;i<n;i++) cout << arr[i]<<" "; cout <<"\n"; }
沉痛的面试教训
- 【20200621】 我大概在20200518号面试了一家公司,当时面试官让我手写快排,那真是一个字的尬!我写成出了shit一样的代码。这样的事情发生不止一次了,在我记忆里至少有两个面试官问过我快排的问题了,而我答得都不是很好。希望自己基础知识能够牢牢掌握。
#include <iostream> using namespace std; const int N = 5; int arr[N] ={3,4,1,2,5}; void quickSort(int left, int right){ if(left > right) return ; int mid = (left+right)/2; int tl = left,tr = right;//temp while(tl <= tr){ //1!!!! while(arr[tl] < arr[mid] ) tl++;//不一定执行 while(arr[tr] > arr[mid] ) tr--; if(tr >= tl){//swap int temp; temp = arr[tl]; arr[tl] = arr[tr]; arr[tr]=temp; } } quickSort(left,mid); quickSort(mid+1,right); } int main() { quickSort(0,4); for(int i = 0;i< 5;i++) cout << arr[i] << " "; return 0; }
上面这段程序至少存在如下几个问题:
bug1
快排是根据枢轴值将区间分成两个子区间。
int mid = (left+right)/2; …… while(arr[tl] < arr[mid]) tl++; if(tr >= tl){ //交换操作... }
这段代码里会不会出现:本身的arr[mid] 被换到了另外一个位置? 那么问题就显现出来:我们根据某个值划分区间,结果这个值是个变量!!。这样就无法把枢轴元素放到正确的位置上了。
bug2
程序会陷入死循环。
while(tl <= tr){ while(arr[tl] < arr[mid] ) tl++;//不一定执行 while(arr[tr] > arr[mid] ) tr--; if(tr >= tl){//swap int temp; temp = arr[tl]; arr[tl] = arr[tr]; arr[tr]=temp; } }
这段while 代码会让程序陷入死循环。假设现在有如下区间
1 4 3,枢轴元素取1,tl = 0, tr = 2。 按照上面这个代码,会得到tl=tr=1处的死循环,原因就在于 while(tl<=tr) 这个条件
bug3
对与分好的区间,我们接着分其子区间即可。
quickSort(left,mid-1); quickSort(mid+1,right);
但是上面的代码则暴露了一个问题:我并不是真正了解快速排序。我竟然把左区间的右端点写成了mid-1,天理难容!! 经过上面这个while循环,我们已经将区间根据枢轴值 pivot 分成了左右两个部分,这个while终止的条件也一定是 tl=tr,即arr[tl]的值就是枢轴变量的值。 那么我们的区间应该分成:
quickSort(left,tl-1); quickSort(tl+1,right);
至此,运行一下修改后的快排,貌似可以得到正确答案了。
#include <iostream> using namespace std; const int N = 5; int arr[N] ={3,4,1,2,5}; //int arr[N] ={3,4,1,2,1}; void quickSort(int left, int right){ if(left > right) return ; int mid = (left+right)/2; int tl = left,tr = right;//temp int pivot = arr[mid];//随机一下=>取中间的值 while(tl < tr){ while(arr[tl] < pivot ) tl++;//不一定执行 while(arr[tr] > pivot ) tr--; if(tr >= tl){//swap int temp; temp = arr[tl]; arr[tl] = arr[tr]; arr[tr]=temp; } } quickSort(left,tl-1); quickSort(tl+1,right); } int main() { quickSort(0,4); for(int i = 0;i< 5;i++) cout << arr[i] << " "; return 0; }
但是如果将arr[]数组替换成 int arr[N] ={5,4,1,2,1};,则发现程序又进入了死循环!!又是为什么呢?
看看下面这组测试数据,其就会得到一个死循环的结果:
这里死循环的原因在于有相同的数字1,为什么有相同的数字就导致死循环呢? 因为我们的第二重while循环存在问题。
while(tl<tr) { ... if(){ ... // 程序就是在这里出现了问题!! } }
上面程序的问题就是:在相等的情况下,如果只做交换,那么“指针”位置相当于没有移动,所以就导致指针不再往希望方向移动,基于此,可以把代码修改为如下
while(tl<tr) { ... if(){ ... // 程序就是在这里出现了问题!! tr--; tl++; //往后移动一下 } }
最后再给出一份没有bug的代码:
========== // Created by lawson on 20-6-21. #include<iostream> using namespace std; int arr[5] = {3,4,1,2,1}; //int arr[5] = {4,3,1,2,5}; //int arr[3] = {1,2,1}; int partition(int left,int right){ int pivot = arr[left];//01.使用区间左端点作为枢轴 //02.递归边界是left < right => 说明最后在退出while循环时会得到:left=right while(left < right){ /*03.思考为什么去掉等于号就不行了? 原因是:会导致死循环。 如果不在while()循环中越过相同值,那么就应该在交换值的时候越过它;[上面那份代码] 如果在while()循环中越过相同值,那么后面在交换指的时候就可以不用执行right-- 和 left++了【本代码】 04.必须保证先从右往左,因为arr[left]已经保存放到pivot中了,可以覆盖; 但是arr[right]却是不能覆盖的 * */ while(left < right && arr[right]>=pivot) //从右往左找第一个比它小的数 right--; arr[left] = arr[right]; /* 01.程序运行到这里的时候,说明 right 这个位置的数已经可以被覆盖了,因为它已经把数给arr[left]了, 所以后面可以开始对 left 进行一个 while 操作。直到找到一个 “合适的位置” 将left 赋值到right 中。 此时left 要么小于right ,要么等于right。无论在哪种条件下都是满足交换条件的。 */ while(left < right && arr[left]<=pivot)//从左往右找第一个比它大的数 left++; arr[right] = arr[left]; // => 会默认保证right > left } arr[left] = pivot; //将pivot 的值放在一个正确的位置上 return left;//这个位置就是一个后面区间的分界点 } void quickSort(int left,int right){ if(right <= left) return ; int mid = partition(left,right); quickSort(left,mid-1); quickSort(mid+1,right); } int main(){ int n = 5; quickSort(0,n-1); for(int i = 0;i< n;i++){ cout << arr[i]<<" "; } }