algorithm-base/animation-simulation/数据结构和算法/快速排序.md
2021-03-20 16:30:29 +08:00

431 lines
18 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

> 如果阅读时发现错误或者动画不可以显示的问题可以添加我微信好友 **[tan45du_one](https://raw.githubusercontent.com/tan45du/tan45du.github.io/master/个人微信.15egrcgqd94w.jpg)** ,备注 github + 题目 + 问题 向我反馈
>
> 感谢支持该仓库会一直维护希望对各位有一丢丢帮助
>
> 另外希望手机阅读的同学可以来我的 <u>[**公众号袁厨的算法小屋**](https://raw.githubusercontent.com/tan45du/test/master/微信图片_20210320152235.2pthdebvh1c0.png)</u> 两个平台同步,想要和题友一起刷题,互相监督的同学,可以在我的小屋点击<u>[**刷题小队**](https://raw.githubusercontent.com/tan45du/test/master/微信图片_20210320152235.2pthdebvh1c0.png)</u>进入。
### 快速排序
今天我们来说一下快速排序这个排序算法也是面试的高频考点原理很简单我们一起来扒一下他吧
我们先来说一下快速排序的基本思想
1.先从数组中找一个基准数
2.让其他比它大的元素移动到数列一边比他小的元素移动到数列另一边从而把数组拆解成两个部分
3.再对左右区间重复第二步直到各区间只有一个数
见下图
![快速排序](https://cdn.jsdelivr.net/gh/tan45du/test1@master/20210122/微信截图_20210214212239.5rco4z7idoc0.png)
上图则为一次快排示意图下面我们再利用递归分别对左半边区间也就是 [3,1,2] 和右半区间 [7,6,5,8] 执行上诉过程直至区间缩小为 1 也就是第三条则此时所有的数据都有序
简单来说就是我们利用基准数通过一趟排序将待排记录分割成独立的两部分其中一部分记录的关键字均比基准数小另一部分记录的关键字均比基准数大然后分别对这两部分记录继续进行排序进而达到有序
我们现在应该了解了快速排序的思想那么大家还记不记得我们之前说过的归并排序他们两个用到的都是分治思想那他们两个有什么不同呢见下图
快速排序我们以序列的第一个元素作为基准数
![对比](https://cdn.jsdelivr.net/gh/tan45du/test1@master/20210122/微信截图_20210218143843.7l004l5cya00.png)
虽然归并排序和快速排序都用到了分治思想但是归并排序是自下而上的先处理子问题然后再合并将小集合合成大集合最后实现排序而快速排序是由上到下的先分区然后再处理子问题归并排序虽然是稳定的时间复杂度为 O(nlogn) 的排序算法但是它是非原地排序算法我们前面讲过归并之所以是非原地排序算法主要原因是合并函数无法在原地执行快速排序通过设计巧妙的原地分区函数可以实现原地排序解决了归并排序占用太多内存的问题
我们根据思想可知排序算法的核心就是如何利用基准数将记录分区这里我们主要介绍两种容易理解的方法一种是挖坑填数另一种是利用双指针思想进行元素交换
下面我们先来介绍下挖坑填数的分区方法
基本思想是我们首先以序列的第一个元素为基准数然后将该位置挖坑下面判断 nums[hight] 是否大于基准数如果大于则左移 hight 指针直至找到一个小于基准数的元素将其填入之前的坑中 hight 位置会出现一个新的坑此时移动 low 指针找到大于基准数的元素填入新的坑中不断迭代直至完成分区
大家直接看我们的视频模拟吧一目了然
为了便于理解所以采取了挖坑的形式展示
![快速排序](https://img-blog.csdnimg.cn/20210317190017344.gif)
是不是很容易就理解啦下面我们直接看代码吧
```java
class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
public void quickSort (int[] nums, int low, int hight) {
if (low < hight) {
int index = partition(nums,low,hight);
quickSort(nums,low,index-1);
quickSort(nums,index+1,hight);
}
}
public int partition (int[] nums, int low, int hight) {
int pivot = nums[low];
while (low < hight) {
//移动hight指针
while (low < hight && nums[hight] >= pivot) {
hight--;
}
//填坑
if (low < hight) nums[low] = nums[hight];
while (low < hight && nums[low] <= pivot) {
low++;
}
//填坑
if (low < hight) nums[hight] = nums[low];
}
//基准数放到合适的位置
nums[low] = pivot;
return low;
}
}
```
下面我们来看一下交换思路原理一致实现也比较简单
见下图
![快速排序](https://cdn.jsdelivr.net/gh/tan45du/test1@master/20210122/微信截图_20210218153208.5wn3lgpbljg0.png)
其实这种方法算是对上面方法的挖坑填坑步骤进行合并low 指针找到大于 pivot 的元素hight 指针找到小于 pivot 的元素然后两个元素交换位置最后再将基准数归位两种方法都很容易理解和实现即使是完全没有学习过快速排序的同学理解了思想之后也能自己动手实现下面我们继续用视频模拟下这种方法的执行过程吧
![](https://img-blog.csdnimg.cn/20210317190153677.gif#pic_center)
两种方法都很容易实现对新手非常友好大家可以自己去 AC 一下啊
```java
class Solution {
public int[] sortArray (int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
public void quickSort (int[] nums, int low, int hight) {
if (low < hight) {
int index = partition(nums,low,hight);
quickSort(nums,low,index-1);
quickSort(nums,index+1,hight);
}
}
public int partition (int[] nums, int low, int hight) {
int pivot = nums[low];
int start = low;
while (low < hight) {
while (low < hight && nums[hight] >= pivot) hight--;
while (low < hight && nums[low] <= pivot) low++;
if (low >= hight) break;
swap(nums, low, hight);
}
//基准值归位
swap(nums,start,low);
return low;
}
public void swap (int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
```
**快速排序的时间复杂度分析**
快排也是用递归来实现的所以快速排序的时间性能取决于快速排序的递归树的深度如果每次分区操作都能正好把数组分成大小接近相等的两个小区间那么此时的递归树是平衡的性能也较好递归树的深度也就和之前归并排序求解方法一致然后我们每一次分区需要对数组扫描一遍 n 次比较所以最优情况下快排的时间复杂度是 O(nlogn)
但是大多数情况下我们不能划分的很均匀比如数组为正序或者逆序时 [1,2,3,4] [4,3,2,1] 此时为最坏情况那么此时我们则需要递归调用 n-1 此时的时间复杂度则退化到了 O(n^2)
**快速排序的空间复杂度分析**
快速排序主要时递归造成的栈空间的使用最好情况时其空间复杂度为O (logn),对应递归树的深度最坏情况时则需要 n-1 次递归调用此时空间复杂度为O(n).
**快速排序的稳定性分析**
快速排序是一种不稳定的排序算法因为其关键字的比较和交换是跳跃进行的见下图
![稳定性](https://cdn.jsdelivr.net/gh/tan45du/test1@master/20210122/微信截图_20210218165440.17ovoc8246gw.png)
此时无论使用哪一种方法第一次排序后黄色的 1 都会跑到红色 1 的前面所以快速排序是不稳定的排序算法
![性能分析](https://cdn.jsdelivr.net/gh/tan45du/test1@master/20210122/微信截图_20210218170306.6oe0mbb5gr40.png)
好啦快速排序我们掌握的差不多了下面我们一起来看看如何对其优化吧
**快速排序的迭代写法**
该方法实现也是比较简单的借助了栈来实现很容易实现主要利用了先进后出的特性这里需要注意的就是入栈的顺序这里算是一个小细节需要注意一下
```java
class Solution {
public int[] sortArray(int[] nums) {
Stack<Integer> stack = new Stack<>();
stack.push(nums.length - 1);
stack.push(0);
while (!stack.isEmpty()) {
int low = stack.pop();
int hight = stack.pop();
if (low < hight) {
int index = partition(nums, low, hight);
stack.push(index - 1);
stack.push(low);
stack.push(hight);
stack.push(index + 1);
}
}
return nums;
}
public int partition (int[] nums, int low, int hight) {
int pivot = nums[low];
int start = low;
while (low < hight) {
while (low < hight && nums[hight] >= pivot) hight--;
while (low < hight && nums[low] <= pivot) low++;
if (low >= hight) break;
swap(nums, low, hight);
}
swap(nums,start,low);
return low;
}
public void swap (int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
```
**快速排序优化**
**三数取中法**
我们在上面的例子中选取的都是 nums[low] 做为我们的基准值那么当我们遇到特殊情况时呢见下图
![特殊举例](https://cdn.jsdelivr.net/gh/tan45du/test1@master/20210122/微信截图_20210218172418.338n8fxuqka0.png)
我们按上面的方法选取第一个元素做为基准元素那么我们执行一轮排序后发现仅仅是交换了 2 7 的位置这是因为 7 为序列的最大值所以我们的 pivot 的选取尤为重要选取时应尽量避免选取序列的最大或最小值做为基准值则我们可以利用三数取中法来选取基准值
也就是选取三个元素中的中间值放到 nums[low] 的位置做为基准值这样就避免了使用最大值或最小值做为基准值
所以我们可以加上这几行代码实现三数取中法
```java
int mid = low + ((hight-low) >> 1);
if (nums[low] > nums[hight]) swap(nums,low,hight);
if (nums[mid] > nums[hight]) swap(nums,mid,hight);
if (nums[mid] > nums[low]) swap(nums,mid,low);
```
其含义就是让我们将中间元素放到 nums[low] 位置做为基准值最大值放到 nums[hight],最小值放到 nums[mid], [4,2,3] 经过上面代码处理后则变成了 [3,2,4].此时我们选取 3 做为基准值这样也就避免掉了选取最大或最小值做为基准值的情况
**三数取中法**
```java
class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
public void quickSort (int[] nums, int low, int hight) {
if (low < hight) {
int index = partition(nums,low,hight);
quickSort(nums,low,index-1);
quickSort(nums,index+1,hight);
}
}
public int partition (int[] nums, int low, int hight) {
//三数取中,大家也可以使用其他方法
int mid = low + ((hight-low) >> 1);
if (nums[low] > nums[hight]) swap(nums,low,hight);
if (nums[mid] > nums[hight]) swap(nums,mid,hight);
if (nums[mid] > nums[low]) swap(nums,mid,low);
//下面和之前一样,仅仅是多了上面几行代码
int pivot = nums[low];
int start = low;
while (low < hight) {
while (low < hight && nums[hight] >= pivot) hight--;
while (low < hight && nums[low] <= pivot) low++;
if (low >= hight) break;
swap(nums, low, hight);
}
swap(nums,start,low);
return low;
}
public void swap (int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
```
**和插入排序搭配使用**
我们之前说过插入排序在元素个数较少时效率是最高的所以当元素数量较少时快速排序反而不如插入排序好用所以我们可以设定一个阈值当元素个数大于阈值时使用快速排序小于等于该阈值时则使用插入排序我们设定阈值为 7
**三数取中+插入排序**
```java
class Solution {
private static final int INSERTION_SORT_MAX_LENGTH = 7;
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
public void quickSort (int[] nums, int low, int hight) {
if (hight - low <= INSERTION_SORT_MAX_LENGTH) {
insertSort(nums,low,hight);
return;
}
int index = partition(nums,low,hight);
quickSort(nums,low,index-1);
quickSort(nums,index+1,hight);
}
public int partition (int[] nums, int low, int hight) {
//三数取中,大家也可以使用其他方法
int mid = low + ((hight-low) >> 1);
if (nums[low] > nums[hight]) swap(nums,low,hight);
if (nums[mid] > nums[hight]) swap(nums,mid,hight);
if (nums[mid] > nums[low]) swap(nums,mid,low);
int pivot = nums[low];
int start = low;
while (low < hight) {
while (low < hight && nums[hight] >= pivot) hight--;
while (low < hight && nums[low] <= pivot) low++;
if (low >= hight) break;
swap(nums, low, hight);
}
swap(nums,start,low);
return low;
}
public void insertSort (int[] nums, int low, int hight) {
for (int i = low+1; i <= hight; ++i) {
int temp = nums[i];
int j;
for (j = i-1; j >= 0; --j) {
if (temp < nums[j]) {
nums[j+1] = nums[j];
continue;
}
break;
}
nums[j+1] = temp;
}
}
public void swap (int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
```
我们继续看下面这种情况
![](https://cdn.jsdelivr.net/gh/tan45du/test1@master/20210122/微信截图_20210220134731.3i6nhxhivo80.png)
我们对其执行一次排序后则会变成上面这种情况然后我们继续对蓝色基准值的左区间和右区间执行相同操作也就是 [2,3,6,3,1,6] [8,6] 我们注意观察一次排序的结果数组中是含有很多重复元素的
那么我们为什么不能将相同元素放到一起呢这样就能大大减小递归调用时的区间大小见下图
![三向切分](https://cdn.jsdelivr.net/gh/tan45du/test1@master/20210122/微信截图_20210220134928.1sx24i3fkc8w.png)
这样就减小了我们的区间大小将数组切分成了三部分小于基准数的左区间大于基准数的右区间等于基准数的中间区间
下面我们来看一下如何达到上面的情况我们先来进行视频模拟模拟之后应该就能明白啦
![三向切分](https://img-blog.csdnimg.cn/20210317190456320.gif#pic_center)
我们来剖析一下视频其实原理很简单我们利用探路指针也就是 i遇到比 pivot 大的元素则和 right 指针进行交换此时 right 指向的元素肯定比 pivot right--但是此时我们的 nums[i] 指向的元素并不知道情况所以我们的 i 指针不动如果此时 nums[i] < pivot 则与 left 指针交换注意此时我们的 left 指向的值肯定是 等于 povit的所以交换后我们要 left++,i++, nums[i] == pivot 仅需要 i++ 即可继续判断下一个元素 我们也可以借助这个思想来解决经典的荷兰国旗问题
好啦我们下面直接看代码吧
**三数取中+三向切分+插入排序**
```java
class Solution {
private static final int INSERTION_SORT_MAX_LENGTH = 7;
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
public void quickSort(int nums[], int low, int hight) {
//插入排序
if (hight - low <= INSERTION_SORT_MAX_LENGTH) {
insertSort(nums,low,hight);
return;
}
//三数取中
int mid = low + ((hight-low) >> 1);
if (nums[low] > nums[hight]) swap(nums,low,hight);
if (nums[mid] > nums[hight]) swap(nums,mid,hight);
if (nums[mid] > nums[low]) swap(nums,mid,low);
//三向切分
int left = low, i = low + 1, right = hight;
int pvoit = nums[low];
while (i <= right) {
if (pvoit < nums[i]) {
swap(nums,i,right);
right--;
} else if (pvoit == nums[i]) {
i++;
} else {
swap(nums,left,i);
left++;
i++;
}
}
quickSort(nums,low,left-1);
quickSort(nums,right+1,hight);
}
public void insertSort (int[] nums, int low, int hight) {
for (int i = low+1; i <= hight; ++i) {
int temp = nums[i];
int j;
for (j = i-1; j >= 0; --j) {
if (temp < nums[j]) {
nums[j+1] = nums[j];
continue;
}
break;
}
nums[j+1] = temp;
}
}
public void swap (int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
```
好啦一些常用的优化方法都整理出来啦还有一些其他的优化算法九数取中优化递归操作等就不在这里进行描述啦感兴趣的可以自己看一下好啦这期的文章就到这里啦我们下期见拜了个拜