分而治之的典型算法-快速排序算法

一、前言

快排是体现了分治法的经典算法,那么我们能从中获取的绝对不止是学到了一个排序算法,更重要的是分治法的核心思想—分。
快排的核心是如何分,然后才是治之。即分而治之。

二、快排的实现

快排的核心在于对待排序数组的划分,然后把小的都放在左边,大的都放在右边,不断的缩小划分的范围,最后出来的就是一个升序的数组。
一个元素是大还是小是相对于一个比较的值而言的,那这个值就叫主元。

例如:{4,3,1}
我们定3为主元,那么1应该放到3左边,4应该放到3的右边,
最后出来的就是 {1,3,4} 这样一个有序的数组。

一个长度为20的数组,第一次划分出左10个右10个元素(划分主元理想状态下)
然后左边的10个再5、5划分,右边的元素再5、5划分,依次进行,每次划分的范围都越来越小,重复着划分排序这一步骤,最后进行合并即可(其实都不用合并,因为当划分完后就是一个有序的数组了)。

很明显这是一个递归调用。划分,每次除了规模大小不一样之外,其他都是一样的。注意:主元也是会动态改变的,并不是固定的。

重点:划分

快排的核心在于划分,划分有三种算法。
掌握划分算法与思想,才是我们学习快排的目的。
划分算法有三种:
1、单向扫描法
思路:三定一循环两交换
步骤
定义一个主元p,默认为左边界第一个元素
定义一个左指针 left(起点,p+1)
定义一个右指针 right(终点,不一定是length-1)
while循环(左指针 <= 右指针)
左指针不断的拿当前指向元素与主元进行对比,
如果:当前元素<=主元
则:左指针右移,右指针不动
否则:左指针不动, 交换左指针与右指针所指向元素,右指针往左移
循环结束后。右指针所指向的就是位置就是主元应该在的位置。
循环结束
两个位置上的元素交换
返回右指针所在位置(此时就是就是划分的左右界限)

简单来说:定义好主元后,左指针不断的往右边走,取出所指向的元素,如果比主元小,就继续走,如果比主元大,就停下来,左指针指向的元素跟右指针指向的元素交换(不管右边过来的元素是多少),右指针往左走一步,然后左指针继续比较过来的元素,小了就继续走,大了就继续和右指针交换元素,右指针往左走,如此反复,知道左指针和右指针擦肩而过了,就停止了。

代码:

// 快速排序
public class Demo26 {
	public static void main(String[] args) {
		int[] arr = Util.RandomIntArr(14, 1, 20); // 自定义方法,生成长度为15的1-20的随机数组
		System.out.print("排序前:");
		Util.printlnArr(arr); // 自定义方法,打印数组
		quickSort(arr, 0, arr.length-1);  // 排序
		System.out.print("排序后:");
		Util.printlnArr(arr);
	}
	
	public static void quickSort(int[] arr, int p, int r) {	
		if (p < r) {
			int mid = partition(arr, p, r);
//			注意理解左右边界的变化
			quickSort(arr, p, mid-1);  // 划分主元左边
			quickSort(arr, mid+1, r); // 划分主元右边
		}
	}
	// 单向扫描法
	public static int partition(int[] arr, int p, int r) {
		// 定义主元、左指针、右指针
		int prvValue = arr[p];
		int left = p+1;
		int right = r;
		while (left <= right) {
			if (arr[left] < =prvValue ) {
				left ++;    // 如果左指针指向的元素小于或等于主元就继续往右走
			}else {
				Util.swap(arr, left, right);   // 否则,和右指针交换元素,右指针往左走一位
				right --;
			}
		}
		// 循环结束,将主元与右指针交换元素
		Util.swap(arr, right, p);
		return right;
	}

运行结果:
在这里插入图片描述

2、双向扫描法
思路:三定三循环两交换
双向扫描法与单向扫描法的原理基本一致,
区别在于,双向扫描法是左右指针同时往中间走,当左指针遇到比主元大的值就停下来,当右指针遇到比主元小的值也停下来,
当两个指针都停下来后,就进行交换元素操作,交换后符合条件了就继续走,直至两指针交错。
步骤

三定
大循环
左指针循环 --->停下来
右指针循环 ---> 停下来
交换两元素
大循环结束
交换右指针与主元的元素
返回右指针

代码实现

// 快速排序
public class Demo26 {
	public static void main(String[] args) {
		int[] arr = Util.RandomIntArr(14, 1, 20); // 自定义方法,生成长度为15的1-20的随机数组
		System.out.print("排序前:");
		Util.printlnArr(arr); // 自定义方法,打印数组
		quickSort(arr, 0, arr.length-1);  // 排序
		System.out.print("排序后:");
		Util.printlnArr(arr);
	}	
	public static void quickSort(int[] arr, int p, int r) {
		if (p < r) {
			int mid = partition2(arr, p, r);  // 划分的界限
//			注意理解左右边界的变化
			quickSort(arr, p, mid-1);  // 划分主元左边
			quickSort(arr, mid+1, r); // 划分主元右边
		}
	}
// 快排划分算法:双向扫描法
	public static int partition2(int arr[], int p, int r) {
		// 初始化主元、left、right
		int prv = arr[p];
		int left = p + 1;
		int right = r;
		// 两指针没有交错
		while (left <= right) {
			// 两指针没有交错,在<主元的情况下,left一直向右走
			// left <= right是防止进来之后越界了
			while (left <= right && arr[left] <= prv) {
				left ++;
			}
			// 两指针没有交错,在>主元的情况下,right一直向左走
			while (left <= right && arr[right] > prv) {
				right --;
			}
			// 两指针没有交错,且两指针都已经停下了,就交换两个的元素。继续走下去。
			if (left <= right) {
				Util.swap(arr, left, right);
			}
		}
		// 走完之后,right停下来的位置就是最后一个小于主元的位置,与其交换元素
		// 此时,right左边的都是小于或等于主元的元素,右边都是大于主元的元素
		// 中间值下标就是right
		Util.swap(arr, p, right);
		return right;
	}

运行结果:
在这里插入图片描述
3、三分法
思路:四定两交换一循环

一共有三个指针
分别指向
小于主元的(left),
等于主元的(mid),
大于主元的位置(right)
初始化时:left、mid指针指向同一个位置
当小于主元时,left、mid交换元素,left、mid一起++    
(交换元素的意义在于当上次循环是等于的时候,将mid指向等于的那个元素,
此时可以加一个判断,mid != left 的时候才交换)
当等于主元时,left++,mid不动(这是理解上面交换的意义的关键)
当大于主元时,交换left、right的元素,right--
循环结束
交换主元与right的元素
return right

实现代码:

// 快速排序
public class Demo26 {
	public static void main(String[] args) {
		int[] arr = Util.RandomIntArr(14, 1, 20); // 自定义方法,生成长度为15的1-20的随机数组
		System.out.print("排序前:");
		Util.printlnArr(arr); // 自定义方法,打印数组
		quickSort(arr, 0, arr.length-1);  // 排序
		System.out.print("排序后:");
		Util.printlnArr(arr);
	}
	
	public static void quickSort(int[] arr, int p, int r) {
		if (p < r) {
			int mid = partition3(arr, p, r);
//			注意理解左右边界的变化
			quickSort(arr, p, mid-1);  // 划分主元左边
			quickSort(arr, mid+1, r); // 划分主元右边
		}
	}
	// 快排划分的算法:三分法
	public static int partition3(int[] arr, int p, int r) {
		// 定义主元、左指针,中指针,右指针
		int prv = arr[p]; // 主元
		int left = p+1;  // 指向小于主元的元素
		int mid = p+1;   // 指向等于主元的元素
		int right = r;   // 指向大于主元的元素
		
		while (left <= right) {
			if(arr[left] < prv) {    
				/**
				 *交换一次的意义是:
				 *当上一次是相等的时候,mid不动,left ++
				 *然后遇到小于的时候就交换位置,mid与left交换元素,
				 *因为mid肯定是指向的元素等于主元,肯定大于当前left指向的元素
				 */
				if (mid != left) {
					Util.swap(arr, mid, left); // 自定义方法,用于数组元素的交换
				} 
				mid ++;
				left ++ ;
			}
			// 当遇到等于的主元的元素时,mid指针就不动了,left继续往右走
			else if (arr[left] == prv) {
				left ++;
			}
			// left 遇到大于主元的元素时
			else{
				Util.swap(arr, left, right);
				right -- ;
			}
		}
		Util.swap(arr, p, right);
		return right;
	}

运行结果:
在这里插入图片描述

三、优化

以上代码还有可优化的空间,例如使用三点中值法绝对中值法来确定主元的值,使的分区更加的合理。
可以自己去研究研究。
最总要的还是掌握分区的思想,即分治的思想。

四、总结

文章写给自己,也写给有需要的人,水平不高,希望大佬们不要吐槽,有什么不对的地方,请指正,谢谢。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

super--Yang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值