【数据结构】基础算法:排序——快速排序

快速排序

快速排序是一种分治的排序算法。它将一个数组分成两个子数组,之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

快排的基本思想:由左子数组(有序且没有任何元素大于切分元素)、切分元素和右子数组(有序且没有任何元素小于切分元素)组成的结果数组一定是有序的

快速排序可以与归并排序对比理解:
1、归并排序:递归调用发生在处理整个数组之前;快速排序:递归调用发生在处理整个数组之后。
2、归并排序中,一个数组被等分为两半;在快速排序中,切分(partition)的位置取决于数组的内容。

性能评估:

将长度为 N 的无重复数组排序,快速排序平均需要 2 N l n N 2NlnN 2NlnN 次比较(以及 1/6 的交换),最多需要约 N 2 / 2 N^{2}/2 N2/2 次比较

算法介绍

快速排序的三个步骤:
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)
(2)分割操作(partition):以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大
(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。

快速排序的大致过程:
在这里插入图片描述

public class Quick {
// 快速排序的实现算法
	public static void sort(Comparable[] a) {
		//随机打乱数组
		shuffle(a);
		sort(a,0,a.length-1);
	}
	
	private static void sort(Comparable[] a, int lo,int hi) {
		//递归思想
		if(hi<=lo) return;
		//切分,对数组进行处理
		int j = partition(a,lo,hi);
		sort(a,lo,j-1);
		sort(a,j+1,hi);
	}

切分方法

该方法的关键在于切分,切分结果要求数组满足下面三个条件:

  • 对于某个 j,a[j] 已经排定;
  • a[lo] 到 a[j-1] 中的所有元素都不大于 a[j];
  • a[j+1] 到 a[hi] 中的所有元素都不小于 a[j]

快速排序就是递归的调用切分,来进行排序。 由左子数组(有序且没有任何元素大于切分元素)、切分元素和右子数组(有序且没有任何元素小于切分元素)组成的结果数组也一定是有序的。

一般策略:

1、先随意地取 a[lo] 作为切分元素,即那个将会被排定的元素
2、然后我们从数组的左端开始向右扫描直到找到一个大于等于它的元素,再从数组的右端开始向左扫描直到找到一个小于等于它的元素。
3、这两个元素显然是没有排定的,因此我们交换它们的位置。如此继续,我们就可以保证左指针i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。
4、指针接着之间位置扫描,重复以上步骤。
5、当两个指针相遇时,我们只需要将切分元素 a[lo] 和左子数组最右侧的元素(a[j])交换然后返回 j 即可。
在这里插入图片描述

	private static void shuffle(Comparable[] a) {
		List<Comparable> list= new ArrayList<Comparable>();
		for(int i=0;i<a.length; i++) {
			list.add(a[i]);
		}
		System.out.println(Arrays.toString(a));
		
		//使用Collections
		Collections.shuffle(list);
		for(int i=0;i<a.length; i++) {
			a[i]=list.get(i);
		}
	}


	//对于某个 j,a[j] 已经排定;
	// a[lo] 到 a[j-1] 中的所有元素都不大于 a[j];
	// a[j] 到 a[hi] 中的所有元素都不小于 a[j]。
	private static int partition(Comparable[] a, int lo, int hi) {
		// 将数组切分为a[lo..i-1], a[i], a[i+1..hi]
		int i=lo,j=hi+1;   //左右指针
		Comparable v = a[lo];
		while(true) {
			//扫描左右
			while(less(a[++i],v)) if(i==hi) break; //找到一个>= v的值
			while(less(v,a[--j])) if(j==lo) break; //找到一个<= v的值
			if(i>=j) break;
			exch(a,i,j); //交换	
		}
		exch(a,lo,j);  // 将v与a[j]位置交换
		return j;     // a[lo..j-1] <= a[j] <= a[j+1..hi] 达成
	}
	
	// 判断元素 v 是否比 w 小
	public static boolean less(Comparable v,Comparable w) {
		return v.compareTo(w)<0;
	}
	// 交换数组a中指定位置的元素
	public static void exch(Comparable[] a, int i, int j) {
		Comparable t= a[i];a[i]=a[j];a[j]=t;
	}
}

选择基准的方式

对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。 也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响**。最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列**

三种选择基准的方法:
方法(1):固定位置
思想:取序列的第一个或最后一个元素作为基准

方法(2):随机选取基准
引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴
思想:取待排序列中任意一个元素作为基准

方法(3):三数取中(median-of-three)
引入的原因: 虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴
分析: 最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数。

优化方式

1. 切换插入排序

当待排序序列的长度分割到一定大小后(5~15),使用插入排序

原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排

和大多数递归排序算法一样,改进快速排序性能的一个简单办法基于以下两点:

  • 对于小数组,快速排序比插入排序慢;
  • 因为递归,快速排序的 sort() 方法在小数组中也会调用自己。

因此,在排序小数组时应该切换到插入排序。

将sort() 中的语句
if (hi <= lo) return;
替换成下面这条语句来对小数组使用插入排序:
if (hi <= lo + M) { Insertion.sort(a, lo, hi); return; }

2. 三向切分

在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割

三向切分的快速排序: 它从左到右遍历数组一次,维护一个指针 lt 使得 a[lo…lt-1] 中的元素都小于 v,一个指针 gt 使得 a[gt+1…hi] 中的元素都大于 v,一个指针 i 使得 a[lt…i-1] 中的元素都等于 v,a[i…gt] 中的元素都还未确定,如图 2.3.4 所示。一开始 i 和 lo 相等,我们使用 Comparable 接口(而非 less())对 a[i] 进行三向比较来直接处理以下情况:

  • a[i] 小于 v,将 a[lt] 和 a[i] 交换,将 lt 和 i 加一;
  • a[i] 大于 v,将 a[gt] 和 a[i] 交换,将 gt 减一;
  • a[i] 等于 v,将 i 加一。

这些操作都会保证数组元素不变且缩小 gt-i 的值(这样循环才会结束)。另外,除非和切分元素相等,其他元素都会被交换。

在这里插入图片描述
代码:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

//三向切分的快速排序
public class Quick3Way {
	//共有方法
	public static void sort(Comparable[] a) {
		//随机打乱数组
		shuffle(a);
		sort(a,0,a.length-1);
	}
	
	private static void sort(Comparable []a,int lo, int hi) {
		//递归结束条件
		if(hi<=lo) return;
		//实现三向切分 
		int lt=lo,i=lo+1,gt=hi; //初始化三个指针
		Comparable v =a[lo];
		while(i<=gt) {
			int cmp=a[i].compareTo(v);
			
			if(cmp<0) exch(a,lt++,i++);       //指针lt 使得 a[lo..lt-1] 中的元素都小于 v
			else if (cmp>0) exch(a,i,gt--);   //指针 gt 使得 a[gt+1..hi] 中的元素都大于 v
			else  i++;                        //指针 i 使得 a[lt..i-1] 中的元素都等于 v
		} // 现在 a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]成立
		sort(a,lo,lt-1);
		sort(a,gt+1,hi);
	}

性能:

为对于包含大量重复元素的数组,它将排序时间从线性对数级降低到了线性级别。

但是在数组中重复元素不多的普通情况下它比标准的二分法多使用了很多次交换。有一种更优的三向切分方法,使得三向切分的快速排序比归并排序和其他排序方法在包括重复元素很多的实际应用中更快。

在这里插入图片描述

3. 优化递归操作

快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化

优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。

4. 使用并行或多线程处理子序列

参考文章:
1、https://www.nowcoder.com/ta/review-test/review?page=196
2、《算法 第4版》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值