八大排序算法(五)——快速排序

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wuqingdeqing/article/details/80280811

快速排序可能是应用最广泛的排序算法。快速排序流行的原因是因为它实现简单、适用于各种不同的输入数据且在一般应用中比其他排序算法都要快的多。快速排序的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为n的数组排序所需的时间和nlogn成正比。快速排序的内循环比大多数排序算法都要短小,这意味着无论是理论上还是实际上都要更快。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能,许多错误都致使它在实际中的性能只有平方级别。

5.1 基本算法

快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况之中,递归调用发生在处理整个数组之后。在归并排序中,一个数组被等分切成两半;在快速排序中,切分的位置取决于数组的内容。

public class QuickSort {
	public static void sort(Comparable[] 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);
	}

	private static int partition(Comparable[] a, int lo, int hi) {
		int i = lo, j = hi + 1;
		Comparable v = a[lo];
		while() {
			while (a[++i] < v) {
				if (j == hi) {
					break;
				}
			}
			while (v < a[--j]) {
				if (j == lo) {
					break;
				}
			}
			if (i >= j) {
				break;
			}
			swap(a, i, j);
		}
		swap(a, lo, j);
		return j;
	}
}
5.1.1 原地切分

如果使用一个辅助数组,可以很容易地实现切分,但产生了将切分后的数组复制回去的开销。将空数组放在递归的切分方法中,这会极大地降低排序效率。

5.1.2 别越界

如果切分元素是数组中最大或最小的元素,就要小心别让扫描指针跑出数组的边界。partition()实现可明确进行明确的检测来预防这种情况。

5.1.3 保持随机性

在partition()中随机选择一个切分元素。

5.1.4 切分元素有重复

左侧扫描最好是在遇到大于等于切分元素时停下,右侧扫描则是遇到小于等于切分元素时停下。尽管这样可能会造成一些不必要的等值交换,但在某些典型应用中,它能避免算法的运行时间变成平方级别。

5.1.5 终止递归

需要保证递归总是能够结束。

5.2 性能特点

快速排序切分方法的内循环会用一个递增的索引将数组元素和一个定值比较。这种简洁些是快排的一个优点,排序算法中很难还能有比这更小的内循环了。

快排另一个速度优势在于它的比较次数很少。排序效率最终还是依赖切分数组的效果,而这依赖于切分元素的值。

快速排序的最好情况是每次都能正好将数组对半分。

将长度为n的无重复数组排序,快速排序平均需要~2nlogn此比较(以及1/6的交换)。

尽管快速排序有很多优点,它的基本实现仍有一个缺点:在切分不平衡是这个程序可能会极为低效。

快速排序最多需要约n^2/2次比较,但随机打乱数组能够预防这种情况。

总的来说,可以肯定的是对于大小为n的数组,此算法的运行时间在1.39nlogn的某个因子的范围之内。归并排序也能做到这一点,但快速排序一般会更快,因为它移动数据的次数更少。

5.3 算法改进

5.3.1 切换到插入排序

基于以下两点:对于小数组,快速排序比插入排序慢;因为递归,快速排序的sort()在小数组中也会调用自己。

5.3.2 三切分取样

使用子数组的一小部分元素的中位数来切分数组。这样做的切分效果更好,但是需要计算中位数。人们发现将取样大小设为3并用大小居中的元素切分效果最好。还可以将取样元素放在数组末尾作为“哨兵”来去掉partition()中的数组边界测试。

5.3.3 熵最优的排序

在有大量元素的情况下,快速排序的递归性会使元素全部重复的子数组经常出现,这就有很大的改进潜力,将当前实现的线性对数级的性能提高到线性级。

一个简单的想法就是将数组切为3部分,分别对应小于、等于、大于切分元素的数组元素。代码如下:

public class Quick3way {
	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) {
				swap(a, lt++, i++)
			} else if (cmp > 0) {
				swap(a, i, gt--)
			} else {
				i++;
			}
		}
		sort(a, lo, lt - 1);
		sort(a, gt + 1, hi);
	}
}

三向切分的最坏情况正是所有组件均不相同。当存在重复组件时,它的性能会比归并排序好得多。三向切分是信息量最优的,即对于任意分布的输入,最优的基于比较的算法平均所需的比较次数和三向切分的快速排序平均所需的比较次数相互处于常数因子范围内。

对于标准的快速排序,随着数组规模的增大其运行时间会趋于平均运行时间,大幅偏离的情况是很少见的,因此可以肯定三向切分的快速排序运行时间和输入的信息量的N倍是成正比的。在实际应用中这个性质很重要,因为对于包含大量重复元素的数组,它将排序时间从线性对数级降到对数级。

猜你喜欢

转载自blog.csdn.net/wuqingdeqing/article/details/80280811