算法——排序:快速排序

##快速排序
采用分治思想divide-and-conquer进行排序,将数组分成两部分,并分别排序。
切分过程是快排的关键,切分使数组满足如下三个条件:

  • The entry a[j] is in its final place in the array, for some j.对某个j,a[j]被排到最终位置上。
  • No entry in a[lo] through a[j-1] is greater than a[j].a[lo] 到 a[j-1] 都不大于 a[j]
  • No entry in a[j+1] through a[hi] is less than a[j].a[j+1] 到 a[hi] 都不小于 a[a]
    然后递归地调用切分过程,将子数组排序。
    这里写图片描述

###切分过程
可以选择 a[lo] 作为切分元素 v(切分完成时被排定)。然后从左端开始扫描,直到大于等于切分元素 v ,从右端开始扫描,直到小于等于切分元素 v。
这里写图片描述
内循环结束时,两个索引元素处在相反的两个子数组中,交换之。
如果两个索引交叉,意味着整个数组已遍历完,此时 j 指向左子数组最右,i 指向右子数组最左,j即是切分元素的最终位置,交换 a[lo] 和 a[j],并返回 j,完成切分。

###实现细节

  • 使用原地排序
    使用辅助数组,使切分的实现更简单,但把元素复制回原数组的开销也很可观。
  • 注意不要越界
    如果最小或最大的元素作为切分元素,注意不要越界。
  • 保持随机性
    通过shuffle将数组打乱,来保证快排的性能。也可以随机选择一个元素作为切分元素。
  • 适时终止循环
    尤其是含有与切分元素的值相同的元素的情况。
  • 处理切分元素值重复
    左侧循环最好在遇到大于等于切分元素时停止,右侧循环最好在遇到小于等于切分元素时停止。尽管可能会不必要地移动与切分元素相同值的元素,但可以避免算法消耗平方级的时间。
  • 适时终止递归
    尤其是切分元素恰好是最大或最小值时,避免陷入无尽的递归循环中。
    ###实现
    当指针 i 和 j 相遇时主循环退出。在循环中,当 a[i] 小于 v 时我们增大 i。a[j] 大于 v 时我们减小 j,然后交换 a[i] 和 a[j] 来保证 i 左侧的元素都不大于 v,j 右侧的元素都不小于 v。当指针相遇时,交换 a[low] 和 a[j] ,切分值就留在 a[j] 中了,切分结束。
public class Quick extends Sort {
    @Override
    public void sort(Comparable[] a) {
        if (a == null || a.length < 2) {
            return;
        }
        shuffle(a);
        sort(a, 0, a.length - 1);
        assert isSorted(a);
    }

    private void sort(Comparable[] a, int low, int high) {
        if (low >= high) {
            return;
        }
        int j = partition(a, low, high);// 切分
        sort(a, low, j - 1);// 左半部分排序
        sort(a, j + 1, high);// 右半部分排序
    }

    private int partition(Comparable[] a, int low, int high) {
        Comparable index = a[low];// 切分标志
        int i = low + 1, j = high;// i:左侧索引,j:右侧索引
        while (true) {
            while (less(a[++i], index)) {// a[i] < index时,i向中间移动,否则,i将被交换
                if (i >= high) {// 边界
                    break;
                }
            }
            while (less(index, a[--j])) {// index < a[j]时,j向中间移动,否则,j将被交换           
                if (j <= low) {// 边界
                    break;
                }
            }
            if (i >= j) break;// 已经有序
            exch(a, i, j);// 经过内循环后,有a[i] >= index >=a [j],交换i,j
        }
        exch(a, low, j);// 经过循环,j已经走到左侧部分的最右,此时交换low和j,保证切分标志在中间被排好。
        return j;
    }
}

###性能特点
快速排序的内循环只做比较,不移动数据,较之其他排序更快。
最好的情况是每次都能正好将数组对半分。对应的,最差的情况就是第一次从最小的元素切分,第二次从第二小的元素切分。。。每次调用只会确定一个元素。可以在排序前将数组随机,即shuffle。
###改进

  1. 小数组改用插入排序。对于小数组,插入排序要比快排更块,也减少了sort的递归调用。小数组就是 high - low 小于某个值的数组。通常5-15都会work well。
  2. 三中位切分。使用一部分的中位数作为切分元素,性能更佳。一般将取样大小设为3效果最好。
  3. Entropy-optimal sorting。含有大量重复元素的数组,有潜力从线性对数级提升到线性级。a。将数组切分成三部分,分别小于、等于、大于切分元素。即Dijkstra的荷兰旗问题,从左到右遍历数组,对于指针 lt,a[lo, …, lt-1]均小于切分元素 v,对于指针 gt ,a[gt+1,…,hi]均大于切分元素v,对于指针 i,a[lt, …, i-1]均等于v,a[i, …, gt]未确定。
    这里写图片描述
    初始 i 等于 lo,之后三向比较:
  • 如果 a[i] 小于 v,交换 a[lt] 和 a[i], lt+1, i+1.
  • 如果 a[i] 大于 v,交换 a[gt] 和 a[i], gt-1.
  • 如果 a[i] 等于 v,i+1。
private static void sort(Comparable[] a, int lo, int hi) { 
        if (hi <= lo) return;
        int lt = lo, gt = hi;
        Comparable v = a[lo];
        int i = lo + 1;
        while (i <= gt) {
            int cmp = a[i].compareTo(v);
            if      (cmp < 0) exch(a, lt++, i++);
            else if (cmp > 0) exch(a, i, gt--);
            else              i++;
        }

        // a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]. 
        sort(a, lo, lt-1);
        sort(a, gt+1, hi);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值