快速排序一步一步优化

一、快速排序介绍

  快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。

  算法思想:1.先从数组中取出一个数组作为枢轴,一般情况下选取数组的第一个或者最后一个元素作为枢轴,当然可以选取其他的,在后面的优化措施里面,我会慢慢介绍。

       2.双向遍历,从左边选取一个比枢轴大的数,从右边选择一个比枢轴小的数,然后交换这两个数;

       3.重复步骤2,直到在枢轴的左边都比枢轴小,枢轴右边的数都比枢轴大。

  算法的时间复杂度:O(nlogn)

二、内容

  示例数组:arr = {1,4,2,5,6,7,9,3};

   

  我们选取第一个数作为枢轴。

  下面,我们来看看第一趟遍历过程:

    

  我们从左循环了3次找到了比枢大的数5,从右循环找到了比枢轴小的数3,接下来,我们要交换这两个数:

    

  至此,第一趟遍历结束,但是这并没有达到要求。我们来看看第二趟遍历的结果:

    

  交换:

    

  由于,上述已经满足了条件,因此不必进行再次交换。

    直到最后一趟,我们枢轴归位:

    

  代码实现:

int qsort(int *a,int left,int right){
    if (right <= left)
        return -1;
    int pivot = a[right];
    int i = left;
    int j = right - 1;

    //从前向后扫描,不需要判断是否会出现越界问题
    while(true){
        while(a[i++] < pivot);

        //从后向前扫描,要防止越界
        while(a[j] > pivot && j >= left){
            j--;
        }
        if (i < j)
            swap(a[i++],a[j--]);
        else{
            break;
        }
    }
    swap(a[i],pivot); // 最后一趟是将a[i]与pivot交换
    qsort(a,left,i -1);
    qsort(a,i+1,right);
    return 0;
}

三、优化 

  我们都知道,快速排序的效率高低主要在于枢轴的选取,无论选取首个元素还是最后一个元素作为枢轴,我们都要对数组进一次遍历。因此,要想优化快排,还得从枢轴的选取下手。

  1.随机选取法

  引入原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴

  思路:使用随机数生成函数生成一个随机数rand,随机数的范围为[left, right],并用此随机数为下标对应的元素a[rand]作为中轴,并与最后一个元素a[right]交换,然后进行

与选取最后一个元素作为中轴的快排一样的算法即可。

  优点:这是一种相对安全的策略。由于枢轴的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。

  代码实现:

int random(int left,int right){
    return rand() % (right - left + 1) + left;
}

void Qsort(int *a,int left,int right){
    if (left >= right)
    {
        return;
    }
    //随机选取一个元素作为枢轴,并与最后一个元素进行交换
    int ic = random(left,right);
    swap(a[ic],a[right]);

    int midIndex = data[right];
    int i = left;
    int j = right - 1;

    while(true){
        //找大于枢轴的数据
        while(a[i++] < midIndex);

        //找到小于枢轴的数据
        while(a[j] > midIndex && j >= left){
            j--;
        }
        //数据已经找到,准备交换
        if (i < j)
        {
            swap(a[i++],a[j--]);
        }
        else{
            break;
        }
    }
    swap(a[i],midIndex); //将枢轴放在正确的位置
    Qsort(a,left,i -1);
    Qsort(a,i+1,right);
}

  2.三数取中(median-of-three)

  引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴

  思路:假设数组被排序的范围为left和right,center=(left+right)/2,对a[left]、a[right]和a[center]进行适当排序,取中值为中轴,将最小者放a[left],最大者放在a[right],把中轴元与a[right-1]交换,并在分割阶段将i和j初始化为left+1和right-2。然后使用双向描述法,进行快排。

  分割好处:      

    1.将三元素中最小者被分到a[left]、最大者分到a[right]是正确的,因为当快排一趟后,比中轴小的放到左边,而比中轴大的放到右边,这样就在分割的时候把它们分到了正确的位置,减少了一次比较和交换。

    2.在前面所说的所有算法中,都有双向扫描时的越界问题,而使用这个分割策略则可以解决这个问题。因为i向右扫描时,必然会遇到不小于中轴的数a[right-1],而j在向左扫描时,必然会遇到不大于中轴的数a[left],这样,a[right-1]和a[left]提供了一个警戒标记,所以不需要检查下标越界的问题。

  分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数

  例子:

    初始数组:6  1  8  9  4  3  5  2  7  0

    选取三个中间数:6  1  8  9  4  3  5  2  7  0

    对这三个数进行排序:0  1  8  9  4  3  5  2  7  6

    最后中轴与a[right-1]交换:0  1  8  9  7  3  5  2  4  6

  实例代码:

int Median(int *a,int left,int right){
    int midIndex = (left + right)>>1;
    if (a[left] > a[midIndex])
    {
        swap(a[left],a[midIndex]);
    }
    if (a[left] > a[right])
    {
        swap(a[left],a[right]);
    }
    if (a[midIndex] > a[right])
    {
        swap(a[midIndex],a[right]);
    }
    swap(a[midIndex],a[right-1]);
    return a[right-1]; //返回中轴
}
void qSort(int *a,int left,int right){
        //如果需要排序的数据大于3个则使用快速排序
        if (right - left >=3)
        {
            int midIndex = Median(a,left,right);
            int begin = left;
            int end = right - 1;
            while (true){
                while(a[++begin] < midIndex);
                while(a[--end]<midIndex);
                if (begin < end)
                {
                    swap(a[begin],a[end]);
                }
                else{
                    swap(a[begin],a[right -1]);//将枢轴移动到何时位置
                    break;
                }
            }
            qSort(a,left,begin -1);
            qSort(a,begin + 1,right);
        }
        else{
            BubbleSort(a,left,right);//当数据小于3个,直接用冒泡排序
        }//当要排序的数据很少时(小于3个),则不能进行三数取中值,此时直接使用简单的排序(例如冒泡)即可,而且从效率的角度来考虑这也是合理的,因为可以避免函数调用的开销。
    }

四、进一步优化

  上述三种快排,在处理重复数的时候,效率并没有很大提高,因此,我们可以想办法优化。

  1.当待排序序列长度分割到一定大小后,使用插入排序。

   原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排

if (high - low + 1 < 10)
{
    InsertSort(arr,low,high);
    return;
}//else时,正常执行快排  

  2.在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割(处理重复效率极高) 

  举例:

    待排序序列 1 4 6 7 6 6 7 6 8 6

    三数取中选取枢轴:下标为4的数6

    转换后,待分割序列:6 4 6 7 1 6 7 6 8 6  枢轴key:6

    本次划分后,未对与key元素相等处理的结果:1 4 6 6 7 6 7 6 8 6

    下次的两个子序列为:1 4 6 和 7 6 7 6 8 6

    本次划分后,对与key元素相等处理的结果:1 4 6 6 6 6 6 7 8 7

    下次的两个子序列为:1 4 和 7 8 7

    经过对比,我们可以看出,在一次划分后,把与key相等的元素聚在一起,能减少迭代次数,效率会提高不少

  具体过程:在处理过程中,会有两个步骤

    第一步,在划分过程中,把与key相等元素放入数组的两端

    第二步,划分结束后,把与key相等的元素移到枢轴周围

  举例:

    待排序序列 1 4 6 7 6 6 7 6 8 6

    三数取中选取枢轴:下标为4的数6

    转换后,待分割序列:6 4 6 7 1 6 7 6 8 6  枢轴key:6

    第一步,在划分过程中,把与key相等元素放入数组的两端

    结果为:6 4 1 6(枢轴) 7 8 7 6 6 6

    此时,与6相等的元素全放入在两端了

    第二步,划分结束后,把与key相等的元素移到枢轴周围

    结果为:1 4 66(枢轴)  6 6 6 7 8 7

    此时,与6相等的元素全移到枢轴周围了

    之后,在1 4 和 7 8 7两个子序列进行快排

  代码示例:

void QSort(int arr[],int low,int high)  //三数中值+聚集相等元素
{
    int first = low;
    int last = high;

    int left = low;
    int right = high;

    int leftLen = 0;
    int rightLen = 0;

    if (high - low + 1 < 10)
    {
        InsertSort(arr,low,high);
        return;
    }

    //一次分割
    int key = SelectPivotMedianOfThree(arr,low,high);//使用三数取中法选择枢轴

    while(low < high)
    {
        while(high > low && arr[high] >= key)
        {
            if (arr[high] == key)//处理相等元素
            {
                swap(arr[right],arr[high]);
                right--;
                rightLen++;
            }
            high--;
        }
        arr[low] = arr[high];
        while(high > low && arr[low] <= key)
        {
            if (arr[low] == key)
            {
                swap(arr[left],arr[low]);
                left++;
                leftLen++;
            }
            low++;
        }
        arr[high] = arr[low];
    }
    arr[low] = key;

    //一次快排结束
    //把与枢轴key相同的元素移到枢轴最终位置周围
    int i = low - 1;
    int j = first;
    while(j < left && arr[i] != key)
    {
        swap(arr[i],arr[j]);
        i--;
        j++;
    }
    i = low + 1;
    j = last;
    while(j > right && arr[i] != key)
    {
        swap(arr[i],arr[j]);
        i++;
        j--;
    }
    QSort(arr,first,low - 1 - leftLen);
    QSort(arr,low + 1 + rightLen,last);
}

  原因:在数组中,如果有相等的元素,那么就可以减少不少冗余的划分。这点在重复数组中体现特别明显啊。

  3.优化递归操作 

  快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化

  优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。

void QSort(int arr[],int low,int high)
{
    int pivotPos = -1;
    if (high - low + 1 < 10)
    {
        InsertSort(arr,low,high);
        return;
    }
    while(low < high)
    {
        pivotPos = Partition(arr,low,high);
        QSort(arr,low,pivot-1);
        low = pivot + 1;
    }
} 

  

参考文献

  http://blog.sina.com.cn/s/blog_5a3744350100jnec.html

  http://www.blogjava.net/killme2008/archive/2010/09/08/331404.html

  http://www.cnblogs.com/cj723/archive/2011/04/27/2029993.html

  http://blog.csdn.net/zuiaituantuan/article/details/5978009

  http://blog.csdn.net/ljianhui/article/details/16797431

  

时间: 2024-10-08 19:34:56

快速排序一步一步优化的相关文章

大流量网站性能优化:一步一步打造一个适合自己的BigRender插件(转)

BigRender 当一个网站越来越庞大,加载速度越来越慢的时候,开发者们不得不对其进行优化,谁愿意访问一个需要等待 10 秒,20 秒才能出现的网页呢? 常见的也是相对简单易行的一个优化方案是 图片的延迟加载.一个庞大的页面,有时我们并不会滚动去看下面的内容,这样就浪费了非首屏部分的渲染,而这些无用的渲染,不仅包括图片,还包括其他的 DOM 元素,甚至一些 js/css(某些js/css 是根据模块请求的,比如一些 ajax),理论上,每增加一个 DOM,都会增加渲染的时间.有没有办法能使得

一步一步实现listview加载的性能优化

listview加载的核心是其adapter,本文针对listview加载的性能优化就是对adpter的优化,总共分四个层次: 0.最原始的加载 1.利用convertView 2.利用ViewHolder 3.实现局部刷新 [转载请保留本文地址:http://www.cnblogs.com/goagent/p/5158064.html]  〇.最原始的加载 这里是不经任何优化的adapter,为了看起来方便,把listview的数据直接在构造函数里传给adapter了,代码如下: 1 priv

一步一步写算法(之快速排序)

原文:一步一步写算法(之快速排序) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 快速排序是编程中经常使用到的一种排序方法.可是很多朋友对快速排序有畏难情绪,认为快速排序使用到了递归,是一种非常复杂的程序,其实未必如此.只要我们使用好了方法,就可以自己实现快速排序. 首先,我们复习一下,快速排序的基本步骤是什么: 1. 判断输入参数的合法性 2.把数组的第一个数据作为比较的原点,比该数据小的数据排列在左边,比该数据大的数据排列在右边 3

android-----带你一步一步优化ListView(三)

前两篇我们介绍了一般的优化ListView方法以及DiskLruCache优化ListView,见android-----带你一步一步优化ListView(一)和android-----带你一步一步优化ListView(二),这一篇我们将从内存缓存的角度来完成ListView的优化,使用的是LruCache,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除,并没有一个固定的缓存大小是符合所有应用程序的,

一步一步跟我学习lucene(6)---lucene索引优化之多线程创建索引

这两天工作有点忙,博客更新不及时,请大家见谅: 前面了解到lucene在索引创建的时候一个IndexWriter获取到一个读写锁,这样势在lucene创建大数据量的索引的时候,执行效率低下的问题: 查看前面文档一步一步跟我学习lucene(5)---lucene的索引构建原理可以看出,lucene索引的建立,跟以下几点关联很大: 磁盘空间大小,这个直接影响索引的建立,甚至会造成索引写入提示完成,但是没有同步的问题: 索引合并策略的选择,这个类似于sql里边的批量操作,批量操作的数量过多直接影响执

Rhythmk 一步一步学 JAVA (21) JAVA 多线程

1.JAVA多线程简单示例 1.1 .Thread  集成接口 Runnable 1.2 .线程状态,可以通过  Thread.getState()获取线程状态: New (新创建) Runnable (可以运行) Blocked  (被阻塞) Waiting  (等待) Timed waiting (计时等待) Terminated  (被终止) ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

C#进阶系列——一步一步封装自己的HtmlHelper组件:BootstrapHelper(二)

前言:上篇介绍了下封装BootstrapHelper的一些基础知识,这篇继续来完善下.参考HtmlHelper的方式,这篇博主先来封装下一些常用的表单组件.关于BootstrapHelper封装的意义何在,上篇评论里面已经讨论得太多,这里也不想过多纠结.总之一句话:凡事有得必有失,就看你怎么去取舍.有兴趣的可以看看,没兴趣的权当博主讲了个“笑话”吧. 本文原创地址:http://www.cnblogs.com/landeanfen/p/5746166.html BootstrapHelper系列

【转】朱兆祺带你一步一步学习嵌入式(连载)

原文网址:http://bbs.elecfans.com/jishu_357014_2_1.html#comment_top  从最初涉及嵌入式Linux开始到现在,深深的知道嵌入式的每一步学习都是举步维艰.从去年11月份开始,我就着手整理各种学习资料,希望推动嵌入式学习的前进贡献自己微不足道的一份力量.从去年到现在,将C语言的学习经验整理成<攻破C语言笔试与机试陷阱及难点>(现在仍在更新),这份资料已经在电子发烧友论坛的单片机论坛连载(http://bbs.elecfans.com/jish

一步一步造个IoC轮子(三):构造基本的IoC容器

一步一步造个Ioc轮子目录 一步一步造个IoC轮子(一):Ioc是什么 一步一步造个IoC轮子(二):详解泛型工厂 一步一步造个IoC轮子(三):构造基本的IoC容器 定义容器 首先,我们来画个大饼,定义好构造函数,注册函数及获取函数这几个最基本的使用方法 /// <summary> /// IoC容器 /// </summary> public class Container { /// <summary> /// 构造函数 /// </summary>

一步一步造个Ioc轮子(二),详解泛型工厂

一步一步造个Ioc轮子目录 .net core发布了,一步一步造个Ioc轮子,弄点.net魔法,近new的速度(一) 一步一步造个Ioc轮子(二),详解泛型工厂 详解泛型工厂 既然我说Ioc容器就是一个豪华版工厂,自动化装配的工厂,那我们就从工厂入手吧,先造个工厂,然后升级成Ioc容器 首先我们来写一个最最最简单的抽象工厂类,还是以前一篇的短信为例 public class SMSFactory { public static ISMS Get() { return new XSMS(); }