C#算法系列(7)——快速排序

       快速排序其实是前面提到的最慢的冒泡排序的升级,它们都属于交换排序。它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动次数。本文将实现快速排序,且针对几种方案,对快排进一步优化。

一、基本思想

       通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

二、实现过程

       (1)选择枢轴:枢轴的选择将直接影响快速排序的效率
       (2)求解子问题:按选择枢轴划分,再分别对枢轴的左右两边分别进行求解。

三、具体实现

(1)选择枢轴

       首先,通过三数取中法,确定枢轴,然后再通过循环,将从low到high之间的元素,比枢轴小的,放在枢轴的左边,比枢轴大的放在枢轴的右边。具体代码如下:

private int Partition(int[] datas, int low, int high)
{
    int pivotkey,temp; //temp用于存放枢轴

    //1.优化选区枢轴时,偏大或者偏小,采用三数取中的方法来解决
    //对于非常大的待排序列可以考虑采用九数取中,先从数组中分3次取样,每次取三个数,3个样品各取出中数
    //然后,从这三个中数当中再取出一个中数作为枢轴

    /* 三数取中法*/
    //即取三个关键字先进行排序,将中间数作为枢轴,一般是取左端,右端和中间三个数
    //计算数组中间的元素的下标
    int m = low + (high-low)/2;
    //与右端相比,保证左端数据最小
    if (datas[low] > datas[high])
    {
        Swap(datas,low,high);
    }
    //与右端相比,保证中间数据最小
    if (datas[m] > datas[high])
    {
        Swap(datas, high, m);
    }
    //与中间相比,保证左端数据最小
    if (datas[m] > datas[low])
    {
        Swap(datas,m,low);
    }

    //经过三数取中法,datas[low]已经为整个序列左中右三个关键字的中间值
    pivotkey = datas[low];
    temp = pivotkey; //备份枢轴

    //使得局部有序
    while (low < high)
    {
        while (low < high && datas[high] >= pivotkey)
        {
            high--;
        }
        //将比枢轴记录小的记录交换到低端
        //Swap(datas,low,high);
        //2.优化枢轴交换:采用替换而不是交换的方式进行
        datas[low] = datas[high];
        while (low < high && datas[low] <= pivotkey)
        {
            low++;
        }
        //将比枢轴记录大的记录交换到高端
        //Swap(datas,low,high);
        datas[high] = datas[low];
    }
    datas[low] = temp;//将枢轴替换回datas[low]
    return low;
}

       上述代码,是经过优化后的代码。主要优化了两点:(1)通过三数取中法,优化了枢轴选择,避免枢轴的过大或过小。(2)优化了不必要的交换。通过增设一个枢轴的临时变量,采用替换的方法来代替交换数值。数值交换代码在这里就不再赘述,有兴趣的,可以参考我前面写的文章C#算法系列(4)——简单排序算法小结
       再来看下求解子问题,按照递归的方式来实现,这种问题就很好解决了。具体代码如下:

//快速排序
public void QuickSort(int[] datas)
{
    QSrot(datas,0,datas.Length-1);
}

private void QSrot(int[] datas, int low, int high)
{
    int pivot;
    if (low < high)
    {
        //选择合理枢轴
        pivot = Partition(datas, low, high);
        //对低子表递归排序;
        QSrot(datas,low,pivot-1);
        //对高子表进行递归排序
        QSrot(datas,pivot+1,high);
    }
}

       看似简洁的代码,其实还可以做进一步的优化。比如,若数组非常小,其实快速排序还不如直接插入排序来的更好,在大量数据排序时,这点性能影响相对于快速排序的整体算法优势而言是可以忽略的。因此,可以考虑针对待排序列的长度来进行优化。具体代码如下:

private int MAX_LENGTH_INSERT_SORT = 7;//数组长度阈值

private void QSrot(int[] datas, int low, int high)
{
    int pivot;
    if ((high-low)>MAX_LENGTH_INSERT_SORT)
    {
        //选择合理枢轴
        pivot = Partition(datas, low, high);
        //对低子表递归排序;
        QSrot(datas,low,pivot-1);
        //对高子表进行递归排序
        QSrot(datas,pivot+1,high);
    }
    else
    {
        //直接插入排序
        StragihtInsertionSort(datas);
    }
}

       增加了一个判断,当high-low不大于某个常数时,就用直接插入排序(具体实现方法参看C#算法系列(4)——简单排序算法小结)。因为直接插入排序在简单排序算法中性能表现最好,这样就能保证最大化的利用两种排序的优势来完成排序工作。
       最后,再来考虑一个问题,在上述代码中的递归是否能进一步优化。因为,大家知道,递归对性能是有一定的影响的,QSort函数在其尾部有两次递归操作。如果待排序的序列划分极端不平衡,递归深度将趋近于n,而不是平衡时的log2(n),这就不仅仅是速度快慢的问题了。栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数的参数越多,每次递归耗费的空间也越多。因此如果能减少递归,将会大大提高性能。具体优化代码如下:

private int MAX_LENGTH_INSERT_SORT = 7;//数组长度阈值

private void QSrot(int[] datas, int low, int high)
{
    int pivot;
    if ((high-low)>MAX_LENGTH_INSERT_SORT)
    {
        while(low<high)
        {
            //选择合理枢轴
            pivot = Partition(datas, low, high);
            //对低子表递归排序;
            QSrot(datas,low,pivot-1);
            //对高子表进行递归排序
            low = pivot+1; //尾递归
        }
    }
    else
    {
        //直接插入排序
        StragihtInsertionSort(datas);
    }
}

       通过添加一个while循环,因为第一次递归以后,变量low就没什么用了,所以可以将pivot+1赋值给low,再循环后,来一次Partition(datas, low, high),其效果等同于QSrot(datas,pivot+1,high)。结果相同,但因采用迭代而不是递归的方法可以缩减堆栈深度,从而提高了整体性能。
       以上就是快速排序的主要思想以及优化。如果有不对的地方还请指出,欢迎留言,谢谢!!!

猜你喜欢

转载自blog.csdn.net/qq_24642743/article/details/78704193
今日推荐