从根本切入理解快速排序!

实现原理

核心思想

快速排序的核心思想是分治。

快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立的排序。 ——《算法(第四版)》

分治,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。 ——百度百科

与归并排序不同的是,快排的分治是治分治分…,而归并的分治则是分分…治治…

算法思路

在这篇博客中,默认规定为从小到大排序。

创建一个包含有随机数据的数组arr。
在这里插入图片描述
快排是通过治来分出两个子数组的。 快排“治”的部分又叫做切分,是整个快排实现的关键。同时,每一次切分都应该做到以下三点:

  1. 对于每个index,arr[index]已经排定
  2. 从arr[left]到arr[index - 1]都不会有元素大于arr[index]
  3. 从arr[index + 1]到arr[right]都不会有元素小于arr[index]

具体治(切分)的方法如下

  1. 创建两个辅助指针,left初始化为数组首元素,right初始化为数组尾元素
  2. 创建一个辅助变量temp,将当前待排序数组的第一个元素arr[left]存放在temp中
  3. 通过②的操作,我们就可以将数组看成(注意:这里这里能是看成,实际上并没有从数组中删除left指向的元素)是有6个数据在7个内存空间内排序,其中left指针指向待插入位置。

在这里插入图片描述
5. 从这一步开始,就要进行切分的具体操作了,前面的只是为切分做准备工作。
1. 从右边开始搜索,查询到如果arr[right]比temp小,则将arr[right]插入到arr[left]指向的位置,否则向左移动right指针
2. 将left指针向右移动一位在这里插入图片描述什么?图没看懂?没事有动图版的。
在这里插入图片描述
4. 此时的数组就可以看成下面这个样子,right指向待插入位置在这里插入图片描述
5. 开始从left开始搜索,搜索原理同4.1->4.2,将比temp值大的放到待插入位置
6. 重复4.1->4.4操作,直至如下图在这里插入图片描述
7. right==left时,说明切分已经完成,将一开始存入temp的数据放到arr[left]中,得到如下数组在这里插入图片描述

递归部分如下

观察当前数组,可以知道此次切分将比temp值小的放在了temp的前面,比temp值大的放在了temp的后面,实现了大体上的“有序”,但实际排序并没有完成。因此,需要通过递归,将用pivot(中轴值)所切分开的两个子数据继续使用该方法分别继续切分直至整体完全有序。

说明: 一开始被存入到temp中的值叫做pivot(中轴值),在有些实现中采取的是当前待排序数组的中间下标对应的值,但实现原理和效果相同(似),实现会在文章的最后稍微提一下。

代码实现

切分方法

写在前面:在while循环中寻找下标的条件语句中,一定要把left<right的约束条件放在&&的前面,否则有可能会造成数组越界异常!
原因:&&的方向为从左向右。

public static int partition(int[] arr, int left, int right){
    //存储中轴值
    int temp = arr[left];
    while(left < right) {
    	//从右开始寻找比temp值大的位置,找到即退出循环
        while(left < right && arr[right] >= temp) {
            //没找到就移动一次指针
            right--;
        }
        //将搜索到的值插入到待插入位置
        arr[left] = arr[right];
        while(left < right && arr[left] <= temp) {
            left++;
        }
        arr[right] = arr[left];
    }
    //退出while循环时,只可能是left == right情况,即找到pivot的待插入下标
    arr[left] = temp;
    //将中轴值下标返回给调用方
    return left;
}

递归方法

初步实现如下(不用递归)

public static void quickSort(int[] arr){
	//获取第一次切分的中轴值
	int pivotIndex1 = partition(arr, 0, arr.length - 1);
	//向左切分,获取第二次切分的中轴值
	int pivotIndex2_left = partition(arr, 0, pivotIndex1 - 1);
	//向右切分,获取第二次切分的中轴值
	int pivotIndex2_right = partition(arr, pivotIndex1 + 1, arr.length - 1);
	//向左切分,获取第三次切分的中轴值
	int pivotIndex3_left = partition(arr, 0, pivotIndex2_1 - 1);
	......
}

显然这么操作不现实,因为我并不知道要切分几次才能保证完全有序,因此用到了递归

改进后的实现

/**
 * 比改进前的代码块多了两个形参:left和right,将排序改进为从left到right位置进行快排,为递归实现创造条件
 */
public static void quickSort(int[] arr, int left, int right){
	//当left >=high时,排序完成,结束递归
	if (left < right){
		//获取中轴下标值
		int pivotIndex = partition(arr, left, right);
		//向左递归
		quickSort(arr, left, pivotIndex - 1);
		//向右递归
		quickSort(arr, pivotIndex + 1, right);
	}
}

创建主函数测试

public static void main(String[] args) {
    int[] arr = {23, 46, 0, 8, 11, 13, 5};
    quickSort(arr, 0, arr.length - 1);
    System.out.println(Arrays.toString(arr));
}

完整代码区

public class QuickSort {
	public static void main(String[] args) {
	    int[] arr = {23, 46, 0, 8, 11, 13, 5};
	    quickSort(arr, 0, arr.length - 1);
	    System.out.println(Arrays.toString(arr));
	}

	public static int partition(int[] arr, int left, int right){
	    //存储中轴值
	    int temp = arr[left];
	    while(left < right) {
	    	//从右开始寻找比temp值大的位置,找到即退出循环
	        while(left < right && arr[right] >= temp) {
	            //没找到就移动一次指针
	            right--;
	        }
	        //将搜索到的值插入到待插入位置
	        arr[left] = arr[right];
	        while(left < right && arr[left] <= temp) {
	            left++;
	        }
	        arr[right] = arr[left];
	    }
	    //退出while循环时,只可能是left == right情况,即找到pivot的待插入下标
	    arr[left] = temp;
	    //将中轴值下标返回给调用方
	    return left;
	}

	public static void quickSort(int[] arr, int left, int right){
		//当left >=high时,排序完成,结束递归
		if (left < right){
			//获取中轴下标值
			int pivotIndex = partition(arr, left, right);
			//向左递归
			quickSort(arr, left, pivotIndex - 1);
			//向右递归
			quickSort(arr, pivotIndex + 1, right);
		}
	}
}

延申部分

使用数组中间值作为pivot

附加判断较多,不推荐。有需求我再更新本文。

public static void quickSort2(int[] arr, int left, int right) {
    //l指向数组最左侧 r指向数组最右侧
    int l = left;
    int r = right;
    int temp;
    //pivot中轴
    int pivot = arr[(left + right) / 2];
    while(l < r) {
        //while循环的目的是让比pivot值小的放到左边,大的放右边
        while(arr[l] < pivot) {
            //再pivot左边一直找 直到找到比pivot大的值停止
            l++;
            //在退出时 一定会找到比pivot大的一个值,最坏最坏的情况下也会找到和pivot的下标相等
        }
        while(arr[r] > pivot) {
            r--;
        }
        if(l >= r) {
            //左右两个指针的大小相等,即都指向pivot时,说明第一步交换已经完成,在pivot前的数都比pivot小 在pivot后的数都比pivot大
            break;
        } else {
            temp = arr[r];
            arr[r] = arr[l];
            arr[l] = temp;
        }
        if (arr[l] == pivot) {
            //保持左索引一直在中轴左侧
            r--;
        }
        if(arr[r] == pivot){
            l++;
        }
    }
    //在递归之前需要做一个判断,如果l==r需要执行if语句,否则会栈溢出
    if (l == r) {
        l++;
        r--;
    }
    //开始向左递归
    if (left < r) {
        quickSort2(arr, left, r);
    }
    //向右递归
    if (right > l) {
        quickSort2(arr, l, right);
    }
}

参考资料

  1. 《算法(第四版)》
  2. nrsc大佬的博客 快速排序—(面试碰到过好几次)
  3. bilibili up 秒懂算法的视频图解 快速排序算法
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值