快速排序(QuickSort)(二)

2015-01-22 20:59:17 · 作者: · 浏览: 10
均的东西,但直觉上肯定是能理解的,下面我把测试数据发出来(和上面一样元素互异)?
提升还是不错的,但没有什么完美的东西,因为三数取中的“取”这一步又得花不少额外的时间。不过这一缺点可以被弥补一些,后面会讲使用插入排序。

尾递归优化

在快速排序的递归树中节点的信息是不需要被保存的,所以可以让一个父节点来代替其中一个孩子节点继续递归下去:
/***************************************
    函数:尾递归优化版快速排序
    功能:对[low, high)范围内的数据排序
    期望运行时间:O(2nlgn)
***************************************/
void quickSortRoutine(int* low, int* high)
{
    int range;      ///区间元素个数
    ///当区间有不止1个元素时才进行分区
    while((range = high - low) > 1)    ///改为循环语句这样父节点就可以在下一次运算中充当孩子节点
    {
        int* pivot = hoarePartition(low, high, range);  ///返回主元地址
        quickSortRoutine(low, pivot); ///对左区间进行快速排序
        low = pivot;    ///注意区间范围
    }
}
可以通过选择替代“较大”的孩子递归使得递归树的深度控制在lgn之内,不过快速排序本来就是靠概率吃饭的,这样会增加比较开销,我就不写了。 这里还有一点点优化的空间,那就是维护一个保存区间信息的数组,里面保存的数据为待分区的区间,这样就完全不用递归了。不过会额外增加空间上的消耗。

使“叶子”变粗

就像前面说的三数取中的花销很大,并且递归开销也很大,所以我们可以把快速排序的“触底”情况的限制放宽,最后对整个基本有序的数组应用插入排序。 于是得到完整快速排序:
#define FACTOR 37   ///叶子的宽度

/***************************************************************
    函数:三数取中的霍尔分区算法
    参数:对[low, high)范围内的数据分区,range为区间的元素个数
***************************************************************/
int* hoarePartition(int* low, int* high, int range)
{
    ///三数取中
    int a = *((int)(((double)rand() / RAND_MAX) * range) + low);
    int b = *((int)(((double)rand() / RAND_MAX) * range) + low);
    int c = *((int)(((double)rand() / RAND_MAX) * range) + low);
    int pivot = a > b ? (b > c ? b : (a > c ? c : a)) : (a > c ? a : (b > c ? c : b));
    low--;
    while(true)
    {
        while(*(++low) < pivot);    ///扫描左区间
        while(*(--high) > pivot);   ///扫描右区间
        ///当两边都扫描到不属于自己区间的元素时
        if(low < high)
        {
            ///当两区间还没有交集说明有两个元素分别落在错误的区间,交换即可
            int tmp = *low;
            *low = *high;
            *high = tmp;
        }
        ///当区间相交时,Hoare分区只保证[0....pivot)中的元素 <= [pivot....n)中的元素
        ///并没有确定主元的位置,所以只得到两个确定大小关系的区间,中间没有主元相隔
        else return low;
    }
}

/***************************************
    函数:尾递归优化版快速排序
    功能:对[low, high)范围内的数据排序
    期望运行时间:O(nlgn)
***************************************/
void quickSortRoutine(int* low, int* high)
{
    int range;      ///区间元素个数
    ///当区间有不止1个元素时才进行分区
    while((range = high - low) > FACTOR)    ///改为循环语句这样父节点就可以在下一次运算中充当孩子节点
    {
        int* pivot = hoarePartition(low, high, range);  ///返回主元地址
        quickSortRoutine(low, pivot); ///对左区间进行快速排序
        low = pivot;    ///注意区间范围
    }
}

/********************************************************************
    函数:优化一点点的插入排序
    功能:对[low, high)内的数据进行排序
********************************************************************/
void improvedInsertionSort(int* low, int* high)
{
    ///因为最小值在第一位,所以直接从第三个元素开始插入
    ++low;
    while(++low < high)
    {
        int tmp = *low; ///把待插入元素保存到临时变量中
        int* destPos = low;  ///计算插入位子
        ///把第一次测试单独提出来
        if(*(--destPos) > tmp)
        {
            do
            {
                *(destPos + 1) = *destPos;
            }while(*(--destPos) > tmp);     ///测试上一个是否是目标位置
            *(destPos + 1) = tmp;     ///最后一次测试失败使得destPos比实际小1
        }
    }
}

/**********************************
    函数:完整版快速排序
**********************************/
void quickSort(int* low, int* high)
{
    ///设置随机数种子
    srand(time(nullptr));
    ///进行快速排序,叶子宽度为FACTOR
    quickSortRoutine(low, high);
    ///找出最小值放到最开始作为插入排序的哨兵节点
    int* minPos = low;   ///最小值的位置
    int* lastPos = low + FACTOR;
    for(int* i = low + 1; i < lastPos; i++)
        if(*i < *minPos) minPos = i;
    int tmp = *low;
    *low = *minPos;
    *minPos = tmp;
    ///最后进行插入排序
    improvedInsertionSort(low, high);
}
那么叶子的宽度该如何去选择呢。一般来说是8到20都不错,不过这里用了三数取中所以常数比普通随机选取主元的快速排序大,所以这里是个两位数就可以了。 上面那个37也是我主观选的,就像生活大爆炸里sheldon说:73是最美妙的数字,因为73是第21个素数,反过来37正好又是第12个素数,并且21正好又等于7和3的积, 另外把73转换为二进制后可以得到1001001,正读倒读都一样。感觉这样一写就很高端啊。

?

?