• 算法与数据结构——排序(九)快速排序


          终于轮到我们的终极算法了,快速排序顾名思义,速度肯定是很快的。它的基本思想是:把一个待排序列分隔成两个独立的序列,其中一个序列中的数比关键字小,另一个序列中的数比关键字大,然后对这两个序列再进行排序,最后使整个序列有序。

         这个算法里面有两个关键点,一是怎样选择这个关键字?二是怎样把一个序列分成两个子序列,使一个子序列比关键字小,另一个子序列比关键字大?

         我们首先来看第二个问题,怎样用程序实现,在这里面我们假设关键字就是待排序列的第一个数。代码如下:

    private int Partition(List<int > sortList,int low,int high)
    {
        int pivotkey = sortList[low];
     
        while (low < high)
        {
            //1.1从高位向低位循环,找出一个数,比pivotkey小的,把它与pivotkey交换,此时pivotkey的值是sortList[low]
            while (low < high && sortList[high] >= pivotkey)
            {
                high--;
            }
            Swap(sortList,low,high);
            //1.2从低位向高位循环,找出一个数,比pivotkey大的,把它与pivotkey交换,如果上面的while循环有交换,那么此时pivotkey的值是sortList[high]
            while (low < high && sortList[low] <= pivotkey)
            {
                low++;
            }
            Swap(sortList, low, high);
        }
        return low;//把pivotkey数的下标返回
    }
    这个算法比较经典,要仔细的理解一下,里面用到的swap方法就是交换两个数,这里就不详细叙述。我个人开始做的时候,没想到这个算法,自己实现了一个比较笨的,但理解起来相对比较容易,代码如下:
    private int MyPartition(List<int> sortList, int low, int high)
    {
        int position;
        int pivotkey = sortList[low];//默认pivotkey的值是sortList[low]
     
        List<int> tempList = new List<int>();
        //1.1 循环遍历整个序列,把比pivotkey小的值加入到tempList中去
        for (int i = low; i <= high; i++)
        {
            if (sortList[i] < pivotkey)
            {
                tempList.Add(sortList[i]);
            }
        }
        //1.2 把pivotkey的值加入到tempList中去,此时tempList中的数都是比pivotkey的值小的数
        tempList.Add(pivotkey);
        //1.3记录下pivotkey值的位置,这个值需要返回去
        position = tempList.Count - 1 + low;
     
        //1.4 循环遍历整个序列,把比pivotkey大的值加入到tempList中去
        for (int i = low; i <= high; i++)
        {
            if (sortList[i] > pivotkey)
            {
                tempList.Add(sortList[i]);
            }
        }
     
        //1.5此时tempList中的数已经分成了两个序列了,从0到position的数都比pivotkey小,后面的都比pivotkey大
        //把tempList中的数重新放入到sortList中去
        int k = 0;
        for (int i = low; i <= high; i++)
        {
            sortList[i] = tempList[k++];
        }
        return position;
    }

        下面来看循环调用的方法:

    private void QSort(List<int> sortList, int low, int high)
    {
        if (low < high)
        {
            int pivotkey = MyPartition(sortList, low, high);
            QSort(sortList, low, pivotkey - 1);
            QSort(sortList, pivotkey + 1, high);
        }
    }

         首先是产生一个关键字,然后把把序列分成两个序列,low到pivotkey-1的数都比关键字小,pivotkey+1到high的数都比关键字大,然后递归对这两个序列又进行排序,最后整个序列就是有序的了。

         真正调用的方法是:

    public List<int> QuickSort(List<int> sortList)
    {
        QSort(sortList, 0, sortList.Count - 1);
        return sortList;
    }

         快速排序在最优的情况下时间复杂度为O(nLogn)。关键字的选取对快速排序性能有比较重要的影响。从上面的代码中可以知道,我们把数组的第一数当作关键字,但是如果这个数是最小的一个数,那么快速排序的时间复杂度就为O(n^n)了,现在就加到第一个问题上,怎样选择关键字。

    关键字的选取,有随机选取法,三数取中法,九数取中法。随机选取就是随机产生一个数,把这个数当作关键字,这种办法效果也不是很好,另外一种就是三数取中,也就是在待排序列中的前面,中间,后面各取一个数,把这三个数进行排序,把它们中间的一个数当作关键字,这种方法选择出来的关键字,比随机产生的关键字要好的多。但是如果数据量非常多,那么三数取中效果也不好,这时就又有了九数取中了,九数取中其实就是做三次取样,也就是分别做三次三数取中,把每次的中间的数拿出来进行排序,最后取一次中,把这个数当作关键字。这样的取法比三数取中要更好一些,当然对于关键字的选取,还有其他一些方法,有兴趣的可以在网上查询相关资料。

         除了关键字的选择,上面的算法还有没有地方可以进行优化呢,肯定是有的。

         我们在选取关键字的时候,在不断的进行交换,使一边的数比关键字小,另一边比关键字大,不管是大的一边还是小的一边,里面的数字都不要求是有序的,所以在这个过程中其实有很多交换是不必要的,我们可以对产生关键字的函数进行改进,代码如下(有注释的地方是改进的地方):

    private int PartitionNew(List<int> sortList, int low, int high)
    {
        int pivotkey = sortList[low];
     
        int temp = pivotkey;//用temp记录pivotkey的值
        while (low < high)
        {
            while (low < high && sortList[high] >= pivotkey)
            {
                high--;
            }
            sortList[low] = sortList[high];//采用替换操作,而不是交换操作
            while (low < high && sortList[low] <= pivotkey)
            {
                low++;
            }
            sortList[high] = sortList[low];//采用替换操作,而不是交换操作
        }
        sortList[low] = temp;//最后把pivotkey的值赋给sortList[low]
        return low;
    }

         还有一种改进的就是减少递归的次数,可以把QSort方法改为:

    private void QSortNew(List<int> sortList, int low, int high)
    {
        while (low < high)//把if改成while
        {
            int pivotkey = MyPartition(sortList, low, high);
            QSort(sortList, low, pivotkey - 1);
            low = pivotkey + 1;//这里low=pivotkey+1,再循环调用一次,相当于 QSort(sortList, pivotkey + 1, high);
        }
    }
    除了这些外,还有一些需要知道的,快速排序在对大数据量排序时会有很好的效率,但是在对很小的数据排序时,就有些大材小用了,所以要视情况来使用,有时别人使用时,加一个判断,如果数据个数小于7就使用插入排序,如果大于7就用快速排序,有人也说这个临界点为50。所以要根据实际情况来决定使用什么方法。
  • 相关阅读:
    包和模块的导入问题
    第9.6节 Python使用read函数读取文件内容
    第9.5节 Python的readlines读取文件内容及其参数hint使用分析
    第9.4节 Python中用readline读取二进制文件方式打开文件
    第9.3节 Python的文件行读取:readline
    第9.2节 Python的文件打开函数open详解
    第9.1节 Python的文件打开函数open简介
    第九章 Python文件操作
    第8.34节 《Python类中常用的特殊变量和方法》总结
    第8.33节 Python中__getattr__以及__getattr__与__ getattribute__的关系深入剖析
  • 原文地址:https://www.cnblogs.com/xiaoxiangfeizi/p/2776146.html
Copyright © 2020-2023  润新知