起因
在看到自己两年前写的文章 https://blog.csdn.net/SUNbrightness/article/details/79251452 后,看了半天看不懂,想不起当年的思路了。甚至连快速排序是什么都忘记了。
此时的我意识到,认认真真写一篇博客有多重要,不光为了分享,也是为了自己将来能够回顾。
这篇文章可能文字较多,主要讲解自己的理解,可能会花费您1个小时左右的时间。
快速排序步骤
- 随便选一个【基准数】(一般就是第一个数,方便代码实现)
- 根据这个 【基准数】进行【划分排列】: 使左边的数都小于【基准数】右边的数都大于【基准数】
- 第二步完成后【基准数】有了【新坐标】,根据这个【新坐标】,我们可以划分出两个【区间】(左边都是比它小的,右边都是比他大的)
- 对【左区间】进行1-2-3-4操作,对【右区间】进行1-2-3-4操作,递归操作,直到【区间】中只有一个数为止 (这是个不断二分的递归过程)
上述中的【划分排列】实现
这里会花点时间理解【划分排列实现】,重点,非常重要,理解这个基本就能自己编码实现了
指针交换法
- 设定两个【哨兵】:最左端的【左哨兵】(一开始的位置是与【基准数】一致的)和最右端的【右哨兵】
- 【右哨兵】 向左找 找到第一个 【小于基准数】,【左哨兵】向右找 找到第一个 【大于基准数】,交换两个数的位置。
- 重复第2步,直至【左哨兵】【右哨兵】碰在一起时,将他们【碰撞位置数】与【基准数】交换(此时,【基准数】左边小于他,右边大于等于他)【划分排列】完成
可以这样理解,不断将左范围边的【大与基准数】与右范围的【小与基准数】交换位置,最后【基准数】到中间。
这里用了两种颜色代表两个哨兵 黄:左哨兵,绿:右哨兵
细节思考 为什么要【右哨兵】先走?
因为指针交换法最后一步要【碰撞位置数】与【基准】交换, 我们的算法中默认第一个数 为【基准数】所以: 要保证【正确的碰撞点】:最后的【碰撞位置数】是一个【小于基准数】,这样交换后才符合【划分排列】,左边的数都小于【基准数】
正确示范↓↓↓:交换后将会符合【划分排列】
错误示范↓↓↓: 交换后不符合划分排列,因为会将一个【大于基准数】交换到了最左边。
要保证碰撞位置:肯定是一个【小于基准数】
如果【左哨兵】先行,会发生什么情况?
1.极端情况:【右哨兵】还没有移动过,【左哨兵】没有找到任何【小于基准数】,直接与【右哨兵】碰撞。此时不能确定【碰撞位置数】是一个【小于基准数】
2.正常情况:【右哨兵】在上一轮确定了一个【小于基准数】并与【左哨兵】交换过了,此时,因为【左哨兵】先行,【碰撞位置数】肯定是一个【大于基准数】
如果【右哨兵】先行
1.极端情况:【左哨兵】没有移动,【右哨兵】没有找到任何【大于基准数】,直接与【左哨兵】碰撞。
2.正常情况:【左哨兵】在上一轮,确定了一个【大于基准数】并与【右哨兵】交换过了,此时,因为【右哨兵】先行,【碰撞位置数】肯定是一个【小于基准数】
根据两种情况模拟得出
如果我们设计的算法为【右哨兵】先行,极端的情况下也会出现【基准数】与【碰撞位置】一致,这样交换后依旧符合【划分排列】
填坑法
填坑的本质就是一个 temp=A,A=B,……,B=temp;务必带这个这个思路去看下文,看代码可能比较好理解。
1.将【基准数】保存到【临时变量】,称【基准数】的位置为【坑】
2.【右哨兵】 向左找 找到第一个 【小于基准数】,挖走这个数,填入【坑】中,并在原地留下一个新【坑】
3.【左哨兵】 向右找 找到第一个 【大于基准数】,挖走这个数,填入【坑】中,并在原地留下一个新【坑】
4.重复2-3步,不断的挖【坑】,填【坑】,直至【左哨兵】【右哨兵】碰在一起时,将第1步保存下的 【基准数】填充到这个【碰撞位置】
可理解为,把右的【小于基准书】挖走填到左边,把左边的【大于基准数】挖走填到右边,想一下这个过程。
双指针划分法
在 a[i…j] 中利用【k指针】和【m指针】 划分出三个区间
a[i] 一直存放着【基准数】
【S1】=a[i+1...m] 存放所有【小于基准数】
【S2】=a[m+1...k-1] 存放所有【大于基准数】
【S3】=a[k...j] 存放所有的【未知数】
一开始 【S1】 和 【S2】 区域都是空的,除了【基准数】之外其他的数都属于【S3】(未知区域)。 我们要做的是用 【k】指针去 探索【未知的区域】(循环一遍),如果 a[k]<【基准数】,将 a[k] 放入【S1】中 否则放入 【S2】中
具体做法
a[k] <【基准数】m+1, 将a[m]与a[k] 进行交换,k++
a[k] >=【基准数】k++
建议按具体做法自己在本上画一遍,这种方式利用了两个指针,【k指针】负责遍历一遍(同时充当S2右边界),【m指针】用于划分S1、S2
import java.util.Arrays;
/**
* @Auther yiliang
* @Date 2020/12/8 11:07
* @Description $
*/
public class QuickSort {
public static void main(String[] args) {
//注意这组数据中第一次数据划分就会出现一个【等于基准数】
//在本代码中应对这种情况是把这个数当成一个【大于基准数】,右哨兵直接跳过
// int[] arr = {0,-4, -1, 6, -2, 8, -3,0,20,-8};
int[] arr = {0, -4, -1, 6, -2, 8, -3,20,-8};
recursionSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
//因为这个是一个二分的过程,使用递归是最方便的
public static void recursionSort(int[] arr, int l, int r) {
//在不断划分 分区】的过程中,当【l==r】(分区中只有一个元素)时就不需要递归了
if (l < r) {
//划分排列
int middle = partition2(arr, l, r);
//既然【基准数】的位置已经确定,那么接下来的分区就不要包含【基准数】了
recursionSort(arr, l, middle-1);
recursionSort(arr, middle+1, r);
// recurtionSort(arr, middle, r);
//!!!!!!!【【重点】】如果右分区包含了【基准数】那么会发生如下死循环(可以自己试试)
// 例:5 8 9 10 6 (5右边的都大于它)
//以 5 为【基准数】进行【划分排列】(partition),返回 坐标的坐标还是【5】的坐标,
//当再次以 5 进行划分,由于我们的【右区间】包括了【基准数】,划分的 右区间为 5 8 9 10 6
//下一次递归: 【5】8 9 10 6 进行【划分排列】
//再次给出【5】的坐标
//死循环!
}
}
//双指针交换法
public static int partition(int[] arr, int lIndex, int rIndex) {
//默认第一个数就是【基准数】
int baseIndex = lIndex;
while (lIndex < rIndex) {
// 【右哨兵】找到第一个【小于基准数】的位置就停下来:while:排除【大于等于基准数】
while (lIndex < rIndex && arr[rIndex] >= arr[baseIndex]) {
--rIndex;
}
//【左哨兵】找到第一个【大于基准数】的位置就停下来:while:排除【小于等于基准数】
//边界问题:这里必须使用 <= ,否则:一开始【左哨兵】和【基准数】重叠,每一次循环【左哨兵】都不会动
while (lIndex < rIndex && arr[lIndex] <= arr[baseIndex]) {
lIndex++;
}
//这里有可能有两种情况
//case1: 双方碰撞了.
// 坐标相等,可以判断下:节省一次交换
// if (lIndex==rIndex)continue;
//case2:双方没有碰撞
swap(arr, lIndex, rIndex);
}
//此时也有两种case
//这个【区间】类似于:5 8 9 10 6 (纯天然的完美的序列)
//当上面的双指针交换结束,应该都停留在了5的位置,可以判断下:节省一次交换
// if (lIndex==baseIndex) return lIndex;
swap(arr, lIndex, baseIndex);
return lIndex;
}
//填坑法
//【坑】的本质就是一个【临时变量】
//temp=A,A=B,……,B=temp 这不就是一个填坑的过程吗,用这个思路去看下面的代码
public static int partition2(int[] arr, int lIndex, int rIndex) {
//把【基准数】保存进临时变量,这个位置就变成了一个【坑】,等待用于其他数填充。
int base = arr[lIndex];
while (lIndex < rIndex) {
//【右哨兵】找到第一个【小于基准数】的位置就停下来:while:排除【大于等于基准数】
while (lIndex < rIndex && arr[rIndex] >= base) {
rIndex--;
}
//将【坑】中的数替换成一个【小于基准数】
//从这个角度可以很好理解为什么一开始【右哨兵】要先走,【初始坑】的位置在最左边,第一个填进来的肯定是【小于基准数】
arr[lIndex] = arr[rIndex];
//因为将arr[rIndex]填入arr[lIndex],此时称 【rIndex 位置】,为新【坑】,用于下次填充
//【左哨兵】找到第一个【大于基准数】的位置就停下来:while:排除【小于等于基准数】
while (lIndex < rIndex && arr[lIndex] <= base) {
lIndex++;
}
arr[rIndex] = arr[lIndex];
//此时 lIndex 位置为新【坑】,用于下次填充
//注意代码,无论是 再进入下一次循环,或者跳出循环,我们利用还是这个【坑】
//再次提醒!!!! 这就是一个 temp=A,A=B,……B=temp 的过程.
}
//将一开始保存的【基准数】入【坑】
arr[lIndex] = base;
return lIndex;
}
//区域划分法
public static int partition3(int a[], int i, int j) {
//将【基准数】保存到变量p中
int p = a[i];
//m作为 S1,S2的分界线
int m = i;
//S1 = a[i+1..m],S2 = a[m+1..k-1],S3 = a[k..j]
//S1,S2都是空的,我们要做的是 移动k 从i+1到j 将所有的 S3(未知)数据交换到 S1、S2 区间中。
for (int k = i+1; k <= j; k++) { // k去探索未知的区域
//a[k]确定是一个【小于基准数】,将要把他放到S1区间中
if (a[k] < p) {
//S1 区间长度+1
m++;
//将【小于基准数】 交换到【新增加的 S1区间位置】
//此时m的位置,不用管它原本是什么,只管将其与【小于基准数】交换
swap(a,k, m);
}
//当 a[k] >= p 的时候什么都不用做,下一个循环 k会+1,当前的a[k]就被加入了S2区间
}
//因为 S1= a[i+1,m]
//我们确定 a[m]肯定是一个【小于基准数】
//将原本处于 i 位置的【基准数】与 分界线【m】交换,将【基准数】作为分界点
swap(a,i,m);
//返回此时【基准数】的位置
return m;
}
public static void swap(int[] arr, int i, int j) {
int tem = arr[i];
arr[i] = arr[j];
arr[j] = tem;
}
}