快速排序逐行代码精析

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