快速排序笔记

March 15, 2016
Algorithm

[TOC]

快排最引人注目的特点是原地排序(只需要一个很小的辅助栈),且将长度为 N 的数组排序所需的时间和 NlgN 成正比。 而快排的主要缺点是非常脆弱,在实现时必须非常小心才能避免性能低下。

同前述一致:以 C++ 实现,仅针对 vector 进行操作, 并且遵循 C++ 左闭右开的区间标准: [a,b)。

基本算法

快排也是一种分治的排序算法,快排与归并排序是互补的:

快排的关键在划分,划分过程需要使得数组满足以下三个条件:

quick

据此,快排的基本思想是:

快排中有若干细节问题值得注意,否则会导致实现错误或者性能下降:

算法改进

  1. 切换到插入排序

    和大多数递归排序算法一样,改进快排性能的一个简单办法是在排序小树组时切换到插入排序,从而避免递归调用,并且对于小数组来说快排比插入排序慢。

    经验表明,在大多数情况下 CUTOFF 取值 5~15 能够取得比较好的性能。

    if (high - low <= CUTOFF) {
        Insertion(vec, low, high);
        return;
    }
    
  2. 三取样切分

    待补充

  3. 熵最优快排——三向切分

    所谓的“熵最优”是指:对于任意分布的输入,最优的基于比较的排序算法平均所需的比较次数与三向切分快排平均所需的比较次数相比,处于常数因子范围之内。当然前提是需要将数组进行随机化。

进一步使用信息论来解释快排性能可参考 Algs4 中的快排章节,Mackayd的Heapsort, Quicksort, and Entropy。刘未鹏的数学之美番外篇:快排为什么那样快也值得一看,浅显易懂。

三向切分快排的运行时间和输入的信息量的 N 倍成正比。对于含有大量重复元素的数组,它将快排的排序时间从线性对数级降低到线性级别

基本想法是将数组切分为三部分,分别对应小于、等于和大于切分元素的数组元素。

Dijkstra 三向切分

Dijkstra 提出的“三向切分快速排序”极为简洁。这里的额外交换用于和切分元素不等的元素。 其从左到右遍历数组一次:

3-wayquick

void OptimalQuick(std::vector<T> &vec, int low, int high) {
    if (high - low <= CUTOFF){
        Insertion(vec, low, high);
        return;
    }
    int left = low, nMid = low + 1, right = high - 1;
    T key = vec[low];
    while (nMid <= right)
    {
        if (vec[nMid] < key){
            std::swap(vec[left], vec[nMid]);
            left++; nMid++;
        }
        else if (key < vec[nMid]){
            std::swap(vec[nMid], vec[right]);
            right--;
        }
        else nMid++;
    }
    OptimalQuick(vec, low, nMid);
    OptimalQuick(vec, right + 1, high);
}

快速三向切分(J.Bently, D.McIlroy)

通过将重复元素置于子数组两端,从而实现一个信息量最优的排序算法。该方法与上述方法是等价的,只是快速三向切分中的额外交换用于和切分元素相等的元素。过程如下:

bmquick

void QuickX(std::vector<T> &vec, int low, int high) {
    if (high - low <= CUTOFF){
        Insertion(vec, low, high);
        return;
    }

    int p = low, q = high, i = low, j = high;
    T key = vec[low];
    while (true){
        while (vec[++i] < key)if (i == high - 1)break;
        while (key < vec[--j])if (j == low)break;
        if (i == j && key == vec[i]){
            std::swap(vec[++p], vec[i]);
        }
        if (i >= j)break;
        std::swap(vec[i], vec[j]);
        // exchange only when equal to key
        if(vec[i] == key)std::swap(vec[++p], vec[i]);
        if(vec[j] == key)std::swap(vec[--q], vec[j]);
    }
    // exchange to right position
    i = j + 1;
    while (p >= low)std::swap(vec[p--], vec[j--]);
    while (q < high)std::swap(vec[q++], vec[i++]);
    // recursive
    QuickX(vec, low, j + 1);
    QuickX(vec, i, high);
}   

Reference:

Algs4

Heapsort, Quicksort, and Entropy

数学之美番外篇:快排为什么那样快

微软2016校招在线笔试题解

April 15, 2016
Algorithm

归并排序笔记

March 10, 2016
Algorithm
comments powered by Disqus