算法——排序之快速排序

快速排序可能是应用最为广泛的排序算法了。应用广泛的主要原因是因为它实现简单,并且在一般应用中比其他算法要快得多。并且它具有良好的特性,他的时间复杂度是O(nlogn)。空间复杂度是O(1),也就是它可以进行原地排序,他只有递归调用产生的空间。

原理:

快速排序是一种分治的排序算法。他根据一个标杆元素,将一个数组分成两个部分,使得左边的部分的元素小于等于标杆元素,而右边的部分大于标杆元素。然后在左右两边的数组中重复这个步骤。这样递归下去,最终,左边的部分是有序的,右边的部分也是有序的,加上中间的标杆,所以,整个数组就是有序的了。



我们发现,按照各个思路,快速排序算法的标杆的选择非常的重要。如果标杆选择不恰当,选择了当中最小最大或者偏最小偏最大的数,都会导致算法非常浪费时间。如果每次都选到了最小的或者最大值,那么快速排序和选择排序没有什么区别了,都是每次只能固定一个数字的位置。


代码如下:

public static void sort(Comparable[] a) {
  // 随机打乱数组
  for (int i = 0; i < a.length / 2; i++) {
    swap(a, i, (int) (Math.random() * a.length));
  }

  sort(a, 0, a.length - 1);
}

public static void sort(Comparable[] a, int low, int high) {
  if (low >= high)
    return;
  int partition = partition(a, low, high);
  sort(a, low, partition - 1); // 排序标杆的左边数组
  sort(a, partition + 1, high); // 排序标杆的右边数组
}

public static int partition(Comparable[] a, int low, int high) {
  int i = low;
  int j = high + 1;
  Comparable compare = a[low];
  while (true) {
    while (less(a[++i], compare)) // 找到左边数组应该放到右边的元素
      if (i == high)
        break;
    while (less(compare, a[--j])) // 找到右边数组应该放到左边的元素
      if (j == low)
        break;
    if (i >= j)
      break;
    swap(a, i, j); // 将找到的元素交换位置
  }
  swap(a, j, low); // 最后将标杆放到中间的部分
  return j;
}
这里重点的代码就是如何划分。我们使用两个指针,一个在左边的最左开始,从左到右遍历。一个在右边最右开始,从右到左遍历。当左边找到比标杆大的元素,右边找到比标杆小的元素,就将这两个元素互相交换位置。直到两个指针相遇,就将标杆元素值和左边最右的元素交换位置。

这里需要注意的是,左边最右的元素是右边遍历过来的,当右指针遇到左边最右的元素就会停下来。

而我们在开头为什么需要打乱数组呢?因为我们不知道这个数组的实际情况,如果输入的数组是已排好序的,我们用第一个元素作为标杆的话,会使得快排树全部分散到一边,退化成交换排序,就达到最差的情况了,这并不是我们希望看到的。所以我们会先打乱数组。

我们可以对快排进行一些小小的优化:

1.对于小规模的数组,采取插入排序的方法。

2.采用三采样,选取3个点,可以随机的选取,也可以固定的选取开头,中间,结尾。选择三个点中中间的值作为标杆,可以对数组的拆分有一个不错的提升。

3.采用随机数选取标杆的办法。


代码修改如下:

public static void sort(Comparable[] a) {
  sort(a, 0, a.length - 1);
}

public static void sort(Comparable[] a, int low, int high) {
    if (high - low <= 7) { // 改进1
      InsertSort.sort(a, low, high + 1);
      return;
    }
    int partition = partition2(a, low, high);
    sort(a, low, partition - 1);
    sort(a, partition + 1, high);
  }

public static int partition(Comparable[] a, int low, int high) {
    // 改进3
    swap(a, low, (int)(Math.random() * (high - low) + low));
    
    int i = low;
    int j = high + 1;
    Comparable compare = a[low];
    while (true) {
      while (less(a[++i], compare))
        if (i == high)
          break;
      while (less(compare, a[--j]))
        if (j == low)
          break;
      if (i >= j)
        break;
      swap(a, i, j);
    }
    swap(a, j, low);
    return j;
  }
这上面的代码采用了第一种和第三种改进方法。并且我们注意到,在这里我们并不需要打乱数组了。因为我们的标杆是随机选择的,所以打乱数组其实并没有必要,只有当你标杆获取的位置是固定的时候,才需要打乱数组。因为打乱数组的成本其实并不廉价。

而对于改进二和改进三。我个人认为随机选取标杆已经是非常不错的方法了。使用三采样的方法选取的标杆并不一定比随机选取的标杆要好多少。而花费的比较次数却是大大增加了。而对于JAVA来说,Comparable的比较开销是比较大的,如果是内置类型,采取三采样的办法也不错。而对于C来说,就更不是问题了。


我们来比较一下改进前和改进后,已经前篇中比较的改进后的Merge排序所花费的时间:

public static void main(String[] args) {
	// Scanner sc = new Scanner(System.in);
	// String[] a = sc.nextLine().split(" ");
	final int NUM = 10000000;
	Integer[] a1 = new Integer[NUM];
	Integer[] a2 = new Integer[NUM];
	Integer[] a3 = new Integer[NUM];
	Integer[] a4 = new Integer[NUM];
	for (int i = 0; i < NUM; i++) {
		// a1[i] = (int) (Math.random() * NUM);
		double temp = Math.random();
		if (temp > 0.5) {
			a1[i] = i;
		} else {
			a1[i] = (int) (Math.random() * NUM);
		}
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
	}

	long startTime;
	long endTime;

	startTime = System.currentTimeMillis(); // 获取开始时间
	QuickSort.sort1(a1);
	assert isSorted(a1);
	endTime = System.currentTimeMillis();
	System.out.println("快速排序cost: " + (endTime - startTime) + " ms");

	startTime = System.currentTimeMillis(); // 获取开始时间
	QuickSort.sort2(a2);
	assert isSorted(a2);
	endTime = System.currentTimeMillis();
	System.out.println("快速排序改良cost: " + (endTime - startTime) + " ms");

	startTime = System.currentTimeMillis(); // 获取开始时间
	MergeSort.sort2(a3);
	assert isSorted(a3);
	endTime = System.currentTimeMillis();
	System.out.println("Merge排序改良cost: " + (endTime - startTime) + " ms");
}
这里排序的数组是一部分有序,一部分随机的。如果是全部随机的话,快速排序就更快了。而如果接近有序的情况下,归并排序比快速排序是快很多的。

部分有序,部分随机:

快速排序cost: 5709 ms
快速排序改良cost: 3048 ms
Merge排序改良cost: 4366 ms
完全随机:

快速排序cost: 6168 ms
快速排序改良cost: 4490 ms
Merge排序改良cost: 5544 ms
有序:

快速排序cost: 3826 ms
快速排序改良cost: 634 ms
Merge排序改良cost: 114 ms


可以发现,对于实际情况来说,快排确实比归并排序外部体现要好一些。


对于存在大量重复元素的情况来说,快排和归并排序的速度都不是让人满意的,虽然他们花费的时间并不长,但是相对来说也不是非常快。例如上面完全有序的情况,快排还需要600多ms,这就有了很大的改进空间。我们可以使用三向切分的方式来进行partition操作。


显然,这样就能快速的减小递归的次数,因为递归两端的数组的长度减小了很多。

但是在平常的没有大量重复的元素的应用中,这样做反而会变慢,因为交换的次数多了很多。

这里我们就不展开了。


快排最大的问题就在于标杆的选择,只要能够在很快的时间内将标杆选择好,那么快排的性能是让人惊喜的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值