算法 —— 排序 —— 快速排序

快速排序

快速排序是应用最广泛的排序算法了。原因是实现简单,且在一般应用中比其他排序算法要快得多。

快速排序内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快。但是它的缺点是非常脆弱,在实现中要非常小心才能避免低劣的性能。

归并排序这篇文章中有讲解到“分治策略”,该策略一般是将一个大问题,(递归的)分为 n 个小问题处理解决。

快速排序也是一种分治的排序算法,它将一个数组分成 2 个子数组,将两部分独立地排序。

快速排序和归并排序是互补的 :

归并排序 : 将数组平分成 n 个子数组分别排序,并将排序后的子数组层层归并,从而将整个数组排序。(由小到大)

快速排序 : 处理原则是当两个子数组都有序时整个数组也自然有序了


基本思想

快速排序的基本思想是 :

通过一趟排序将要排序的数组分成 2 个独立的数组,其中左半部分的数组都比右半部分的数组小。再依此递归对这2个数组再排序。

通俗来说,快速排序是基于“轴”的排序算法,在数组中挑选一个元素作为轴,比轴小的元素在轴的左边,比轴大的元素放置轴的右边。

这里写图片描述
这里写图片描述



上图阐述的是快速排序的算法思想。

在代码中,首先将数组 a [ lo … hi ] 排序,先用 partition()方法将 a [ j ] 放到一个合适位置,(将数组切分成 2 个子数组)然后在用递归调用将其他位置的元素排序。


切分

	private static int partition(int a[], int lo, int hi) {//该算法过程仍可优化
		int i = lo, j = hi + 1;//i 和 j两个指针
		int v = a[lo];//轴
		while (true) {
			while (a[++i] < v) {//从左至右,找到大于轴的元素
				if (i >= hi) {break;}
			}
			
			while (a[--j] > v) {//从右至左,找到小于轴的元素
				if (j <= lo) {break;}
			}

			if (i >= j) {break;}
			
			exch(a, i, j);

		}
		//轴原本在lo下标,移至j下标
		//此时a[lo .. j-1]的元素都小于轴 ,a[j+1 .. hi]的元素都大于轴
		exch(a, lo, j);

		return j;
	}

这段代码按照 a[lo] 的值 v 作为轴进行切分。当指针 i 和 j 相遇时主循环退出。在循环中, a[ i ] 小于 v 时增大 i ( i++ ),a[ j ] 大于 v 时我们减小 j 。然后交换 a[ i ] a[ j ] 来保证 i 的左侧元素都小于 v ,j 的右侧元素都大于 v 。当指针相遇时交换 a[lo] 和 a[ j ],切分结束。(这样切分值就留在a[ j ]中了)

这里写图片描述

对于切分,这个过程会使得数组满足下面三个条件 :

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

选取【算法4】图解增加理解
算法4



排序算法

捋清了快速排序的切分过程之后,整体的算法过程便清晰起来 :

public static void main(String[] args) {

		int[] a = new int[] { 9, 1, 2, 8, 7, 3, 5, 4 };
		sort(a, 0, a.length - 1);
	}

	private static void sort(int a[], int lo, int hi) {
		if (lo >= hi) {
			return;
		}
		int j = partition(a, lo, hi);//切分
		sort(a, lo, j - 1);//轴左边数组排序
		sort(a, j + 1, hi);//轴右边数组排序 
	}


算法改进

快速排序是由C.A.R Hoare在1960年发明提出的,至今仍有很多改进的方法。

  1. 小数组切换到插入排序
    和大多数递归排序算法一样,因为递归,快速排序的sort()方法在小数组中也会调用自己。所以在小数组中,快速排序比插入排序慢。

  2. 数组中存在重复元素
    如果要排序的子数组中元素都是重复的那就不需要继续排序了,但上面的算法还会继续将它切分为更小的数组。针对这一问题,我们使用三向切分的快速排序。



三向切分(改进重复元素排序)

这里写图片描述

三向切分主要是解决上图问题,即减少重复数组中的递归次数。

	private static void sort(int a[], int lo, int hi) {
		if (lo >= hi) {
			return;
		}
		int j = partition(a, lo, hi);//切分
		sort(a, lo, j - 1);//轴左边数组排序
		sort(a, j + 1, hi);//轴右边数组排序 
	}

在代码中我们看到,快速排序会将 a [ lo … j ] ,a[ j … hi ] 的数组继续递归排序,此时我们可以引入一个指针 mid (可自行命名)用来指向重复区域,使得 a [ i … j ] 区域的元素都为重复元素,此时我们只要将 a [ lo … i] , a [ j … hi ] 排序即可。

这里写图片描述

我们新引入指针 mid :

  • a[ mid ] 等于 v,mid +1;
  • a[ mid ] 小于 v,交换 a[ i ] 和 a[ mid ]的值,并将 i 和 mid + 1;
  • a[ mid ] 大于 v,交换 a[ j ] 和 a[ mid ]的值,并将 j -1;
  • 当 mid 和 j 指针相遇时退出循环,a [ i … mid ] 区间的值都为和v相同的元素集。

这里写图片描述
这里写图片描述
这里写图片描述


上图可以说是比较清晰的表达了三向切分快速排序的这一过程,它和我们第一个介绍的快速排序实现有所不同,但是基本思想都是将数组分成 2 部分,左部分的元素均小于 轴,右部分的元素均大于 轴。

//代码也略有不同,不过三向切分的代码相对来说更容易理解些。
public static void main(String[] args) {

		int[] a = new int[] { 9, 1, 2, 8, 7, 3, 5, 4 };
		sort(a, 0, a.length - 1);
	}

	private static void sort(int a[], int lo, int hi) {
		if (lo >= hi) {
			return;
		}
		int i = lo, mid = lo + 1, j = hi;
		int v = a[lo];
		while (mid <= j) {
			int cmp = a[mid] - v;
			if (cmp < 0) {
				exch(a, mid++, i++);
			} else if (cmp > 0) {
				exch(a, mid, j--);
			} else { 
				mid++;
			}
		}
		sort(a, lo, i - 1);
		sort(a, j + 1, hi);
	}

该算法遵守上面我们提到的 :

  • a[ mid ] 等于 v,mid +1;
  • a[ mid ] 小于 v,交换 a[ i ] 和 a[ mid ]的值,并将 i 和 mid + 1;
  • a[ mid ] 大于 v,交换 a[ j ] 和 a[ mid ]的值,并将 j -1;
  • 当 mid 和 j 指针相遇时退出循环,a [ i … mid ] 区间的值都为和v相同的元素集。

猜你喜欢

转载自blog.csdn.net/qian520ao/article/details/80154720