1.定义

快速排序有三要素:


  • 分区算法【如何将一个无序的数组分区,才是快速排序的关键】
  • 递归【分治法解决问题】
  • 合并问题【其实不用合并数组,因为数组在递归的时候就已经完全有序了】

2.算法思想

2.1 思想

利用分治的方法【表现形式是递归】将一个数组由一个枢轴元素分成两部分,直到该部分区间满足条件(​​right >= left​​)时再终止返回。经过不断的递归排序,最后就可以得到一个有序的区间。需要注意枢轴左边的元素不比枢轴大,枢轴右边的元素不比枢轴小。

2.1 步骤


  • 取某无序数组区间的第一个元素【注意不一定是 ​​arr[0]​​,枢轴元素下标是 ​​arr[low]​​,只有第一次递归时,才有 ​​low=0​​】为枢轴元素,记为​​pivotKey​​。
  • ​low = 0,high = n-1​​【是否减一,则要看数组的存储是从0开始,还是1开始,n 是数组中的个数】;
  • 缓存pivotkey的值,​​pivotkey = array[left]​​ 【这个很关键,否则后面array[left]被重写,就导致无法找出​​pivotkey​​了】
  • 开始执行​​while(left < right)​​ 循环,循环中的事情如下:
    从​​high​​开始依次向左依次遍历数组,如果​​low<high && array[high] >= pivotKey​​,则执行​​high--​​;反之,退出while循环,退出时需赋值​​array[left]=array[right]​​ 从​​low​​开始依次向右遍历数组,如果​​low<high && array[low] <= pivotKey​​,则执行​​low++​​;反之,退出while循环,退出时需赋值 ​​array[right]=array[left]​
  • 当​​low==high​​时,一轮排序结束,需要将​​pivotkey​​放到分界点位置,执行​​array[left] = pivotkey​​,同时返回当前枢轴元素所在的下标。
  • 递归排枢轴元素下标左半部分和右半部分的无序数组

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]<<" ";
}
}