思路
之前我们从选择排序,到选择排序的稳定性优化,到冒泡排序,到插入排序,到插入排序的提前截止时间,到希尔排序,虽然逐步一直都在优化,但是时间复杂度还是N得平方,力扣提交的结果一直都是时间超时,因为时间复杂度并没有发生量级级别的减少,都是通过常规的优化思维,说白一点,就是没有创新的优化点,都是一步一步,一点一点优化,想方设法能能提高一点效率就提高一点。
那么从快速排序就是要开始坐飞机了往前冲了,直接打破一个量级的时间复杂度,从N的平方,到Nlogn,N就是二叉树的深度,logn就是每一层的时间复杂度。
为什么说快速排序是结合了二叉树前序递归思想的排序,后面我把快速排序的代码写出来吗,你对比一下二叉树的前序遍历,结构基本都是差不多的。
快速排序的解题思路就是:一般默认选择第一个数组元素作为起点,将第一个元素处理一下,使得左边的元素都小于它,右边的元素都大于它。第二就开最左右的数组继续处理,左边的数据选择当前第一个元素再左边找到位置是的左边都小于它,右边的都大于它,右边同理。你看这不就是二叉树的前序递归遍历吗?
接下来直接看代码,如果说你明白了在快排中使用到了二叉树的前序遍历思想,那你成功的解决了50的问题,剩下的50%是看你嫩不能解决数组中的一个点,如何找到它所在的位置,并且交换好数据吗,这个还需要主要的是一个数组的边界问题(如果数组题好好刷了,知道数组边界的处理,不混乱),那第二个难点也就不是什么难点,我还是讲一下找到中间位置的函数的思路吧,但是边界为就不讲了,这块儿还是有点技巧的,回头再来刷这个题目的时候再看吧
代码
class Solution {public int[] sortArray(int[] nums) {sort(nums,0,nums.length-1);return nums;}//定义一个递归遍历的函数sortvoid sort(int[] nums,int lo,int hi){if(lo > hi){return;}int p = partition(nums, lo, hi);sort(nums,lo,p-1);sort(nums,p+1,hi);}//定义一个找到位置的partition函数int partition(int[] nums, int lo, int hi){int pivot = nums[lo];// 关于区间的边界控制需格外小心,稍有不慎就会出错// 我这里把 i, j 定义为开区间,同时定义:// [lo, i) <= pivot;(j, hi] > pivot// 之后都要正确维护这个边界区间的定义int i = lo + 1, j = hi;// 当 i > j 时结束循环,以保证区间 [lo, hi] 都被覆盖while (i <= j) {while (i < hi && nums[i] <= pivot) {i++;// 此 while 结束时恰好 nums[i] > pivot}while (j > lo && nums[j] > pivot) {j--;// 此 while 结束时恰好 nums[j] <= pivot}if (i >= j) {break;}// 此时 [lo, i) <= pivot && (j, hi] > pivot// 交换 nums[j] 和 nums[i]swap(nums, i, j);// 此时 [lo, i] <= pivot && [j, hi] > pivot}// 最后将 pivot 放到合适的位置,即 pivot 左边元素较小,右边元素较大swap(nums, lo, j);return j;}// 原地交换数组中的两个元素private static void swap(int[] nums, int i, int j) {int temp = nums[i];nums[i] = nums[j];nums[j] = temp;}
}
这个运行后还是超出运行限制,到底是哪里还可以继续优化呢,肯定是可以的,我们先不纠结了,能和面试官谈到这里,说不定他还没有懂得深的。差不多了,优化的点我先提一下,就是在上面的基础上加了一个洗牌算法
直接看代码:
class Solution {public int[] sortArray(int[] nums) {// 为了避免出现耗时的极端情况,先随机打乱shuffle(nums);sort(nums,0,nums.length-1);return nums;}//定义一个递归遍历的函数sortvoid sort(int[] nums,int lo,int hi){if(lo > hi){return;}int p = partition(nums, lo, hi);sort(nums,lo,p-1);sort(nums,p+1,hi);}//定义一个找到位置的partition函数int partition(int[] nums, int lo, int hi){int pivot = nums[lo];// 关于区间的边界控制需格外小心,稍有不慎就会出错// 我这里把 i, j 定义为开区间,同时定义:// [lo, i) <= pivot;(j, hi] > pivot// 之后都要正确维护这个边界区间的定义int i = lo + 1, j = hi;// 当 i > j 时结束循环,以保证区间 [lo, hi] 都被覆盖while (i <= j) {while (i < hi && nums[i] <= pivot) {i++;// 此 while 结束时恰好 nums[i] > pivot}while (j > lo && nums[j] > pivot) {j--;// 此 while 结束时恰好 nums[j] <= pivot}if (i >= j) {break;}// 此时 [lo, i) <= pivot && (j, hi] > pivot// 交换 nums[j] 和 nums[i]swap(nums, i, j);// 此时 [lo, i] <= pivot && [j, hi] > pivot}// 最后将 pivot 放到合适的位置,即 pivot 左边元素较小,右边元素较大swap(nums, lo, j);return j;}// 原地交换数组中的两个元素private static void swap(int[] nums, int i, int j) {int temp = nums[i];nums[i] = nums[j];nums[j] = temp;}// 洗牌算法,将输入的数组随机打乱private static void shuffle(int[] nums) {Random rand = new Random();int n = nums.length;for (int i = 0 ; i < n; i++) {// 生成 [i, n - 1] 的随机数int r = i + rand.nextInt(n - i);swap(nums, i, r);}}
}
加上洗牌算法果然就通过了
接下来就继续分析一下,为什么加了一个洗牌算法就能提交通过呢,这里面到底优化什么?
这个问题要从快速排序的构造思想和极端情况两方面进行分析:
首先是上面我们提到的快速排序的构造思想就是二叉数的前序遍历构造思想,不是非常极端的二叉树情况,那么快速排序正常的复杂度就是NlogN,但是如果运气不好,出现了前序遍历后的二叉树呈现一边倒的趋势,这样的话,复杂度就又变成最坏的情况,N的平方了。所以提交才没有过。
之后我们又采用了洗牌算法,先把数组打乱,这样就不会出现极端情况了,至于洗牌算法是什么思想,怎么实现,后续慢慢道来。