快速排序 java实现 原理讲解 三种方式

起因

在看到自己两年前写的文章 https://blog.csdn.net/SUNbrightness/article/details/79251452 后,看了半天看不懂,想不起当年的思路了。甚至连快速排序是什么都忘记了。

此时的我意识到,认认真真写一篇博客有多重要,不光为了分享,也是为了自己将来能够回顾。

这篇文章可能文字较多,主要讲解自己的理解,可能会花费您1个小时左右的时间。

快速排序步骤

  1. 随便选一个【基准数】(一般就是第一个数,方便代码实现)
  2. 根据这个 【基准数】进行【划分排列】: 使左边的数都小于【基准数】右边的数都大于【基准数】
  3. 第二步完成后【基准数】有了【新坐标】,根据这个【新坐标】,我们可以划分出两个【区间】(左边都是比它小的,右边都是比他大的)
  4. 对【左区间】进行1-2-3-4操作,对【右区间】进行1-2-3-4操作,递归操作,直到【区间】中只有一个数为止 (这是个不断二分的递归过程)

上述中的【划分排列】实现

这里会花点时间理解【划分排列实现】,重点,非常重要,理解这个基本就能自己编码实现了

指针交换法

  1. 设定两个【哨兵】:最左端的【左哨兵】(一开始的位置是与【基准数】一致的)和最右端的【右哨兵】
  2. 【右哨兵】 向左找 找到第一个 【小于基准数】,【左哨兵】向右找 找到第一个 【大于基准数】,交换两个数的位置。
  3. 重复第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;
	}
}

猜你喜欢

转载自blog.csdn.net/SUNbrightness/article/details/110877598