用决策树正面比较型算法的极限

第八章使用决策树对比较型算法进行了分析. 比较型算法如果抽调中间过程的话, 其实可以抽象为一颗决策树(一颗二叉树, 书中用的是full binary tree, 感觉有点不对), 每一次比较都产生两种结果, 即产生两条分支. 叶子节点表示完成了排序. 那么对于输入规模为n的排序算法, 结果总共有n!种, 这也就意味着, 二叉树的叶子节点总共有n!个, 最好的情况是, 这些叶子节点出现在同一层, 此时即为满二叉树, 也就是下图表示的情况. 此时树的高度(代表最坏情况下的比较的次数)即为Ω(log(n!)), 用一个什么定理之后可以得到Ω(nlogn).

                                    n
                               n        n
                        l          l    l        l

如果不理解为什么这种情况下树的高度最低可以尝试着把上一层的左边一个node改为leaf, 那么为了得到n!个叶子我们必须延长右边高度. 这也就说明了一件事, 所有的比较型算法的运行时间的下界就是Ω(nlogn).

几种常见的线性排序算法

现在来看看几种线性的排序算法, 使用这些算法往往需要掌握输入数据的更多信息才行.

counting sort

这种排序必须事先知道所排序数据的可能出现的情况, 也就是说有哪几种可能性. 对于数组来说, 比如事先知道数组中所有的数据的范围是[1, 10]. 那么它的做法是, 建立一个10的数组, 遍历输入数据并记录每个数据出现的次数, 然后进行排序. 代码实现如下:

template <typename T>
void sort(T array[], int size, T lowBound, T highBound){
    // init count array
    int boundSize = highBound - lowBound + 1;
    int count[boundSize];
    memset(count, 0, boundSize * 4);

    for(int i = 0; i < size; ++i){
        ++count[array[i] - lowBound];
    }

    for(int i = 1; i < boundSize; ++i){
        count[i] += count[i - 1];
    }

    T temp[size];
    for(int i = size - 1; i >= 0; --i){
        temp[count[array[i] - lowBound] - 1] = array[i];
        --count[array[i] - lowBound];
    }

    for(int i = 0; i < size; ++i){
        array[i] = temp[i];
    }
}

这里值得注意的是, 这里真正利用每个值出现的次数将原数组中的数据写入temp数组是倒序进行的(第三个for循环), 这样做的原因在于保证排序是稳定排序.

radix sort

基数排序利用的原理其实是多关键字对于排序的影响不同. 比如年月日中的年和月和日, 比如一个十进制三位数中的个位十位和百位. 这里分为两种方式, 从高关键字开始排序称之为MSD排序, 而从低关键字开始排序称之为LSD排序. 如果从高关键字开始排序, 比如年月日, 先用年将数据分割成多个小块, 然后在每个小块中再分别用月进行排序, 最后再用日排序, 排好序之后按照关键字大小顺序把数据拼接起来即可. 另一方面, 如果从低关键字开始排序, 则并不进行分割, 但必须使用稳定排序(否则可能会打乱上一次排序的结果), 用各个关键字分别排序后, 原数据已排序, 所以基数排序也是稳定排序. 算法导论上只提到了LSD排序, 下面是使用LSD排序对自然数进行排序的代码(对于每一个关键字的排序使用的是计数排序) :

int findMax(int array[], int size){
    int maxIndex = -1;
    for(int i = 0; i < size; ++i){
        if(maxIndex == -1 || array[maxIndex] < array[i]){
            maxIndex = i;
        }
    }
    return maxIndex;
}

int findDigits(int num){
    int digits = 1;
    while(num / 10 != 0){
        ++digits;
        num /= 10;
    }
    return digits;
}

int findDigitNumber(int num, int digit){
    while(digit != 1){
        num /= 10;
        --digit;
    }
    return num % 10;
}


void sort(int array[], int size, int digit){
    // init count array
    static int boundSize = 10;
    int count[boundSize];
    memset(count, 0, boundSize * 4);

    for(int i = 0; i < size; ++i){
        ++count[findDigitNumber(array[i], digit)];
    }

    for(int i = 1; i < boundSize; ++i){
        count[i] += count[i - 1];
    }

    int temp[size];
    for(int i = size - 1; i >= 0; --i){
        temp[count[findDigitNumber(array[i], digit)] - 1] = array[i];
        --count[findDigitNumber(array[i], digit)];
    }

    for(int i = 0; i < size; ++i){
        array[i] = temp[i];
    }
}

void sort(int array[], int size){
    int digits = findDigits(array[findMax(array, size)]);

    for(int i = 1; i <= digits; ++i){
        sort(array, size, i);
    }
}

桶排序

个人感觉桶排序就像是特殊的MSD排序. 桶排序它假设输入数据会在范围内均匀分布, 然后把该范围分为k部分(k个桶), 然后遍历输入数据将它们划分到各个桶内, 然后分别对每个桶使用排序算法(算法导论上使用的是插入排序)排序后, 将各个桶中的数据连接起即可. 所以可以看出来他的时间复杂度取决于对于每个桶使用的排序算法. 不过在算法导论中, 如果数据相对于桶是均匀分布的话, 即使使用的是插入排序, 最终算出来的平均时间复杂度也是θ(n). 具体实现这里我也懒得实现了, 感兴趣的同学可以看下这篇 文章.

posted on 2016-11-30 21:34  内脏坏了  阅读(528)  评论(0编辑  收藏  举报