zoukankan      html  css  js  c++  java
  • 排序算法——快速排序

    四、快速排序(Quick Sort) 

      快速排序是一种使用很广泛的排序算法,属于交换排序类。

    思路:

      使用“分治”的递归算法进行排序

    步骤:

      1)基准情形:N=0或1,无需排序,返回

      2)N>1,从数组S中任意取一个元素ν,称之为枢纽元(pivot)

      3)分割:

      将比ν大的元素全部放到v的右边,S1

      将比v小的元素全部放到v的左边,S2

      4)对左右两个区间递归地快速排序,直到序列中只有0或1个元素

      quicksort(S1)

      quicksort(S2)

      注意:应避免创建新的数组,占据额外的内存空间

    分割方法:

      第 3)步分割的方法不是唯一的,因此成为了一种设计决策。

           (1)常见的,将第一个元素作为枢纽元

      如果输入是随机的,是可以的;

      如果输入是预排序或反序的,则会产生一个劣质的分割,所有元素都在S1或S2中,且对所有递归情形都一样。

      花费的时间是二次的,且枢纽元是无效的。

      类似的,还有选取前2个互异的元素中的较大者作为枢纽元

      (2)随机选取基准数

      安全,不会总发生劣质的分割。

      对绝大多数输入数据达到O(NlogN)的期望时间复杂度。

      (3)三数中值分割法(Median-of-Three)

      最佳枢纽元:一个数组N个数的中值,即第N/2个最大的数。但很难算出,且明显减慢快排的速度

      一般做法:使用左端、右端、中心位置上的三个元素的中值作为基准数

      消除了预排序输入的不好情形,且减少了大约14%的比较次数。避免“元素当初输入时不够随机”带来的恶化效应。   

    /**
    * 快速排序的驱动程序
    */
    void quicksort(vector<int> &a)
    {
        quicksort(a, 0, a.size() - 1);
    }
    
    void quicksort(vector<int> &a, int left, int right) 
    void quicksort(vector<int> &a, int left, int right)
    {
         if (left >= right) return;
    
         /* 找到枢纽元 */
         int base = division(a, left, right);
         int pivot = a[base];
            
         /* 开始分割 */
         int i = left;
         int j = right;
         while (i < j)
         {
             /* 先从右向左找到小于枢纽元的数 */
            while (a[j] >= pivot && i < j)  j--;
                   
             /* 再从左向右找到大于枢纽元的数 */
            while (a[i] <= pivot && i < j)  i++;
    
             /*如果两者没有交会,则交换两数在数组中的位置 */  
            if (i < j)
               std::swap(a[i], a[j]);
         }
    
         /* 将枢纽元归位 */
         std::swap(a[base], a[i]);
    
          /* 递归地对左、右两半部分进行快速排序 */
         quicksort(a, left, i - 1);
         quicksort(a, i + 1, right);
    }
    View Code
    /**
    * 使用第一个元素或[left,right]内的随机数作为枢纽元 
    */
    int division(vector<int> &a, int left, int right)
    {
            /* 以下方案二选一 */
    
            /* 随机选取[left,right]范围内的数作为枢纽元 */
        int i = rand() % (right-left+1) + left; // [left,right]内的随机数
        std::swap(a[left], a[i]);
        return left;
    
         /* 使用第一个数作为枢纽元 */
         return left;
    }    
    View Code

    • 使用三数中值分割法(Median-of-Three)
    void quicksort(vector<int> &a, int left, int right)
    {
        if (left >= right) return;
        int pivot = division(a, left, right);
    
        int i = left; // 开始位置,警戒标志
        int j = right - 1;
    
        while (i < j) {
            /* 从left+1和right-2开始 */
            while (a[++i] < pivot){}
            while (pivot < a[--j]){}
            if (i < j)
                std::swap(a[i], a[j]);
            else
                break;
        }
        std::swap(a[right - 1], a[i]);
    
        quicksort(a, left, i - 1);
        quicksort(a, i + 1, right);
    }
    View Code
    int division(vector<int> &a, int left, int right)
    {
        int center = (left + right) / 2;
    
        if (a[center] < a[left]) std::swap(a[left], a[center]);
        if (a[left] > a[right]) std::swap(a[left], a[right]);
        if (a[center] > a[right]) std::swap(a[right], a[center]);
    
        std::swap(a[center], a[right - 1]);
        return a[right - 1];
    }
    View Code

      分析:选取a[left],a[center],a[right]的中值作为基准值。

      先对三者进行排序,a[left]< a[center]<a[right],a[center]为三者的中值,作为枢纽元pivot,而a[left], a[right]都处于分割阶段应该在的区间。

      可以把pivot放到a[right-1],并在分割阶段将i和j初始化为left和right-1。a[left]<pivot,所以a[left]作为j的警戒标记,不必担心j跑过端点;由于i会停在pivot元素处,所以将pivot存储在a[right-1]处为i提供了一个警戒标记,不必担心i跑过端点。

     时间复杂度:

      N=0或1,时间为常数,记为1。

           N>1,用时为:T(N)=T(i)+T(N-i-1)+cN(分割花费的时间)

     1)最坏情形:

      枢纽元始终是最小元素,此时i=0,忽略无关紧要的T(0)=1

            T(N)=T(N-1)+cN

            T(N-1)=T(N-2)+c(N-1)

             ......

            T(2)=T(1)+c(2)

            相加可得 T(N) =T(1)+c(2+3+4+...+N) =Θ (N2)

    1)最好情形:

      枢纽元正好位于中间,为简化,假设两个子数组恰好是原数组的一半大小

            T(N)=2T(N/2)+cN ——> T(N)/N=T(N/2)/(N/2)+c

             T(N/2)/(N/2)=T(N/4)/(N/4)+c

             ......

            T(2)/2=T(1)/1+c

            相加可得 T(N) =cNlogN+N =Θ (NlogN)

    3) 平均情形:

      T(N)=O(NlogN)

    适用情形:   

      对于很小的数组(N<=20),快速排序不如插入排序好。通常的解决办法是,对于小的数组不递归地使用快速排序,而是使用插入排序这样的对小数组有效的排序算法。截止范围在N = 5~20之间都可以,相对只用快排的方式,实际上可以节省15%的运行时间。  

    if(left+10<=right)
    { 
        // 使用快速排序    
        quicksort(a,left,right);
    }
    else 
    {  
        // 对小数组使用插入排序
        insertionSort(a,left,right);    
    }
    View Code

       STL的sort算法,数据量大时采用快排,分段递归排序,一旦分段后的数据量小于某个门槛,为避免快排的递归调用带来过大的额外负载,就改用插入排序。如果递归层次过深,还会改用堆排序。

  • 相关阅读:
    JS 百度地图路书---动态路线
    jQuery---创建和添加节点
    CSS基础
    第一篇:前端知识之HTML内容
    JS高级---为内置对象添加原型方法
    JS DOM属性+JS事件
    Vue-router
    vue使用kkfileview文件预览功能
    JS高级---案例:验证密码的强度
    promise是怎么来的?
  • 原文地址:https://www.cnblogs.com/warrior1988/p/8024309.html
Copyright © 2011-2022 走看看