【问题标题】:How to optimize quicksort如何优化快速排序
【发布时间】:2012-09-09 09:50:24
【问题描述】:

我正在尝试制定一个高效的quicksort 算法。它工作得很好,但是当元素数量很大并且数组的某些部分是预先排序的时需要很长时间才能运行。我在 quicksort 上查找 Wikipedia 文章,发现这里写的是:

为了确保最多使用 O(log N) 空间,首先递归到数组的较小一半,然后使用尾调用递归到另一半。

使用插入排序,它具有较小的常数因子,因此在小型数组上更快,用于对此类小型数组的调用(即长度小于实验确定的阈值 t)。这可以通过使此类数组不排序并在最后运行单个插入排序过程来实现,因为插入排序可以有效地处理几乎已排序的数组。每个小段的单独插入排序会增加启动和停止许多小排序的开销,但避免浪费精力比较多个段边界的键,由于快速排序过程的工作,哪些键将是有序的。它还提高了缓存的使用率。

我目前正在对两个分区进行递归。知道如何实施第一个技巧吗? 先递归到数组的较小一半,然后使用尾调用递归到另一个是什么意思?其次,如何在快速排序中实现insertion-sort?它会始终提高效率,还是仅在对数组的某些部分进行预排序时才提高效率?如果是第二种情况,那我当然不知道什么时候会发生。那么我什么时候应该包含insertion-sort

【问题讨论】:

  • 插入排序的常数因子要低得多,并且存在一个交叉点,对于大小取决于您的实现、您的计算机和行星对齐方式的小数组,插入排序比快速排序快。
  • 您能详细说明一下吗?详细的解释将非常有用。
  • 当然:对于 n 项,快速排序完成的工作是 Anlog(n)(在预期的情况下),而插入排序完成的工作是 Bn^2,其中 A 和 B 是常数大致对应于“每次迭代执行的指令成本”的因素。现在 B

标签: algorithm recursion quicksort insertion-sort


【解决方案1】:

在快速排序中,您选择一个随机枢轴,将数组分隔为两半,大多数情况下一个可能更小,

例如数组大小为 100,pivot 将数组分隔为 40 / 60,40 是较小的大小。

假设您决定使用插入排序的阈值大小为 10, 您需要继续通过 pivot 递归地拆分数组,每当一半的一个变得小于或等于 10 时,您可以使用在小尺寸数组上表现得像 O(n) 的插入排序。

考虑到如果您的数组被反向排序(最坏情况),插入排序将表现不佳。

关于递归的东西,你只需要修改快速排序递归的停止情况 -> 数组大小

尾递归的意思是对前半部分做所有你需要的事情,然后对小半部分调用插入排序作为最后一个方法,它是用来节省空间的。

  Quick-sort()
      choose a pivot
      move the smaller elements from left
      move the bigger elements from right
      quick-sort on the bigger half of the array

      if half is less then X
         only then do an insertion sort on the other half <- this is a tail recursion insertion sort 
      else
         quick sort on this half also

据我所知,第二个优化建议不要对每个递归步骤都使用插入排序,而是记住对其进行约束的索引,然后在一批中调用插入排序,将所有切片中的项目连接起来,这将确保提高缓存的使用率,但实现起来稍微困难,

【讨论】:

  • 较小阵列的quicksort 效率与insertion-sortO(log N) 相比是多少?
  • 快速排序效率平均为o(nlog(n)),而不是o(log(n)),小数组的插入排序效率为o(n)
  • 但是,如果分区是反向排序的,即使它也有它的警告。尾调用位的递归又如何呢?
  • 当然它有它的警告,为了决定你是否会使用这个优化,你需要看看你的数据是什么样子的,如果它是部分排序的还是随机排序的“例如A ,B,E,G,C" 那么你很有机会提高你的性能,如果你有像 "E,D,C,B,A" 这样的初始数据,那么你可以在交换数组元素后应用你的算法,你您的决定必须基于数据的分布。此外,我认为快速排序递归没有任何问题。
  • 谢谢。您能否解释一下第一点:首先递归到数组的较小一半,然后使用尾调用递归到另一半。
【解决方案2】:

有多种方法可以提高标准快速排序的效率。要实现您帖子中的第一个提示,您应该编写如下内容:

void quicksort(int * tab, int l, int r)
{
   int q;
   while(l < r)
   {
      q = partition(tab, l, r);
      if(q - l < r - q) //recurse into the smaller half
      {
         quicksort(tab, l, q - 1);
         l = q + 1;
      } else
      {
         quicksort(tab, q + 1, r);
         r = q - 1;
      }
   }
}

希望这已经足够清楚了。下一步将是实现您自己的堆栈(或使用您使用的任何语言的一些内置),而不是使用递归调用。示例(伪)代码:

void quicksort2(int * tab, int l, int r)
{
    int le, ri, q;
    init stack;
    push(l, r, stack);
    while(!empty(stack))
    {
        //take the top pair of values from the stack and set them to le and ri
        pop(le, ri, stack);
        if(le >= ri)
            continue;
        q = partition(tab, le, ri);
        if(q - le < ri - q) //smaller half goes first
        {
            push(le, q - 1, stack);
            push(q + 1, ri, stack);
        } else
        {
            push(q + 1, ri, stack);
            push(le, q - 1, stack);
        }
    }
    delete stack;
}

然后您可以继续实施您帖子中的其他提示。为此,您应该设置一些任意常数,我们将其称为 CUT_OFF,大约为 20。这将告诉您的算法何时应该切换到插入排序。更改前面的示例应该很容易(添加一个 if 语句),以便在到达 CUT_OFF 点后切换到插入排序,所以我将留给您。

至于分区方法,我建议使用 Lomuto 分区而不是 Hoare。

但是,如果您的数据已经预先排序,那么您可以考虑完全使用不同的算法。根据我的经验,如果您的数据是预先排序的,那么在链表上实现的自然序列合并排序是一个非常好的选择。

【讨论】:

  • 谢谢。我不明白你的第二个算法。什么被压入堆栈,它如何提高性能?
  • 您将范围,即成对的值(左、右)压入堆栈。基本上,您尝试使用自己的堆栈来模仿调用堆栈。这提高了性能,因为它消除了递归的需要。
  • 非递归版本几乎总是比递归版本快。这是因为,一般来说,递归不利于性能。递归涉及大量增加大量开销的幕后工作。有关此主题的更多信息,请参阅:stackoverflow.com/questions/3021/…
  • Lomuto 更简单,因此虽然它们具有相同的复杂性,但 Lomuto 应该快一点(操作更少)。在实施时也更难犯错误。 ;)
  • 实际上,Hoare 倾向于执行较少的交换,并且在实践中通常更快。
【解决方案3】:

我前段时间写了一个基于快速排序的算法,你可以在那里找到(实际上它是一种选择算法,但也可以用作排序算法):

我从这次经历中学到的教训如下:

  1. 仔细调整算法的分区循环。这通常被低估了,但是如果您注意编写编译器/CPU 将能够进行软件流水线的循环,您确实会获得显着的性能提升。仅此一项就导致 CPU 周期提高了约 50%。
  2. 手工编码的小分类让您在性能上大获全胜。当分区中要排序的元素数量低于 8 个元素时,不要费心尝试递归,而是仅使用 ifs 和 swaps 实现硬编码排序(查看此代码中的 fast_small_sort 函数) .这可以在 CPU 周期中获得大约 50% 的优势,从而为快速排序提供与编写良好的“合并排序”相同的实际性能。
  3. 当检测到“糟糕”的枢轴选择时,花时间选择更好的枢轴值。每当枢轴选择导致一侧低于要排序的剩余元素的 16% 时,我的实现就开始使用“中位数的中位数”算法进行枢轴选择。 这是一种缓解快速排序最坏情况性能的策略,有助于确保在实践中上限也是 O(n*log(n)) 而不是 O(n^2) .
  4. 优化具有大量相等值的数组(在需要时)。如果要排序的数组有很多相等的值,则值得优化,因为它会导致糟糕的枢轴选择。在我的代码中,我通过计算所有等于枢轴值的数组条目来做到这一点。这使我能够以更快的方式处理数组中的枢轴和所有相等的值,并且在不适用时不会降低性能。 这是针对最坏情况性能的另一种缓解策略,它通过大幅降低最大递归级别来帮助减少最坏情况下的堆栈使用量

我希望这会有所帮助,劳伦特。

【讨论】:

  • 最后一点很有趣。假设我选择了一个枢轴,那么在数组中搜索相等的键本身就需要大量时间,对吧?其次,即使没有,我如何以更快的方式处理数组中的枢轴和所有相等的值
  • 一旦你选择了一个主元,你就可以通过与数组中的每个元素进行比较来将数组分成两部分,这样你就可以以相同的成本比较是否相等。在我的代码中查找 equalCount 变量,它说明了一种方法。
  • 是的,但是将分区中的每个元素与枢轴进行比较本身就会产生惩罚,它不会抵消通过记录所有相等数字可能实现的时间增益吗?
  • 快速排序已经对每个被排序的元素执行枢轴比较,因此添加相等比较不会增加任何成本。一旦你将元素分成两组,并且如果你知道你有“许多”值等于枢轴,那么只有第二次通过来处理等于枢轴的值。对“相等元素”进行特殊处理的决定是一种权衡:如果 equalCount 较低且剩余要排序的元素数量很大,则不要这样做,但如果 equalCount 与要排序的元素数量之比排序是大的,然后去做。
  • 好的,我试试看。谢谢。
【解决方案4】:

你可以看一下 TimSort,对于非完全随机数据,它比快速排序表现更好(它们具有相同的渐近复杂度,但 TimSort 具有较低的常数)

【讨论】:

    【解决方案5】:

    我最近找到了this optimization。 它比 std::sort 运行得更快。它对小数组使用选择排序,并使用 3 的中位数作为分区元素。

    这是我的 C++ 实现:

    const int CUTOFF = 8;
    
    template<typename T>
    bool less (T &v, T &w)
    {
        return (v < w);
    }
    
    template<typename T>
    bool eq (T &v, T &w)
    {
        return w == v;
    }
    
    template <typename T>
    void swap (T *a, T *b)
    {
        T t = *a;
        *a = *b;
        *b = t;
    }
    
    template<typename T>
    void insertionSort (vector<T>& input, int lo, int hi) 
    {
        for (int i = lo; i <= hi; ++i)
        {
            for (int j = i; j > lo && less(input[j], input[j-1]); --j)
            {
                swap(&input[j], &input[j-1]);
            }
        }
    }
    
    
    template<typename T>
    int median3 (vector<T>& input, int indI, int indJ, int indK)
    {
        return (less(input[indI], input[indJ]) ?
                (less(input[indJ], input[indK]) ? indJ : less(input[indI], input[indK]) ? indK : indI) :
                (less(input[indK], input[indJ]) ? indJ : less(input[indK], input[indI]) ? indK : indI));
    }
    
    
    template <typename T>
    void sort(vector<T>& input, int lo, int hi) 
    { 
        int lenN = hi - lo + 1;
    
        // cutoff to insertion sort
        if (lenN <= CUTOFF) 
        {
            insertionSort(input, lo, hi);
            return;
        }
    
        // use median-of-3 as partitioning element
        else if (lenN <= 40) 
        {
            int median = median3(input, lo, lo + lenN / 2, hi);
            swap(&input[median], &input[lo]);
        }
    
        // use Tukey ninther as partitioning element
        else  
        {
            int eps = lenN / 8;
            int mid = lo + lenN / 2;
            int mFirst = median3(input, lo, lo + eps, lo + eps + eps);
            int mMid = median3(input, mid - eps, mid, mid + eps);
            int mLast = median3(input, hi - eps - eps, hi - eps, hi); 
            int ninther = median3(input, mFirst, mMid, mLast);
            swap(&input[ninther], &input[lo]);
        }
    
        // Bentley-McIlroy 3-way partitioning
        int iterI = lo, iterJ = hi + 1;
        int iterP = lo, iterQ = hi + 1;
    
        for (;; ) 
        {
            T v = input[lo];
            while (less(input[++iterI], v))
            {
                if (iterI == hi) 
                    break;
            }
            while (less(v, input[--iterJ]))
            {
                if (iterJ == lo)    
                    break;
            }
            if (iterI >= iterJ) 
                break;
            swap(&input[iterI], &input[iterJ]);
            if (eq(input[iterI], v)) 
                swap(&input[++iterP], &input[iterI]);
            if (eq(input[iterJ], v)) 
                swap(&input[--iterQ], &input[iterJ]);
        }
        swap(&input[lo], &input[iterJ]);
    
        iterI = iterJ + 1;
        iterJ = iterJ - 1;
        for (int k = lo + 1; k <= iterP; ++k) 
        {
            swap(&input[k], &input[iterJ--]);
        }
        for (int k = hi  ; k >= iterQ; --k)
        {
            swap(&input[k], &input[iterI++]);
        }
    
        sort(input, lo, iterJ);
        sort(input, iterI, hi);
    }
    

    【讨论】:

    • “它比std::sort 运行得更快”:这是一个大胆的主张,应该有一个具体的基准(具体的输入和时间)来支持!标准库的实现者通常知道他们在做什么。 std::sort 的典型实现还委托对小子序列进行选择或插入排序,并且还使用分区元素的智能选择。
    • 不错的优化!我用您的版本替换了“std::sort()”,并且在启用完全优化的情况下运行速度提高了 21%(编译器是 Visual Studio C++ 2013)。谢谢! =)
    【解决方案6】:

    尾递归是将递归调用变为循环。 对于快速排序,它类似于:

    QuickSort(SortVar)                                                                     
       Granularity = 10                                                            
       SortMax = Max(SortVar)
       /* Put an element after the last with a higher key than all other elements 
          to avoid that the inner loop goes on forever */
       SetMaxKey(SortVar, SortMax+1)
    
       /* Push the whole interval to sort on stack */               
       Push 1 SortMax                                                              
       while StackSize() > 0                                                       
          /* Pop an interval to sort from stack */
          Pop SortFrom SortTo                                                     
    
          /* Tail recursion loop */                           
          while SortTo - SortFrom >= Granularity                                
    
             /* Find the pivot element using median of 3 */                            
             Pivot = Median(SortVar, SortFrom, (SortFrom + SortTo) / 2, SortTo)             
             /* Put the pivot element in front */                                     
             if Pivot > SortFrom then Swap(SortVar, SortFrom, Pivot)
    
             /* Place elements <=Key to the left and elements >Key to the right */           
             Key = GetKey(SortVar, SortFrom)                                                
             i = SortFrom + 1                                                      
             j = SortTo                                                            
             while i < j                                                        
                while GetKey(SortVar, i) <= Key; i = i + 1; end                          
                while GetKey(SortVar, j) > Key; j = j - 1; end                           
                if i < j then Swap(SortVar, i, j)                                       
             end                                                                   
    
             /* Put the pivot element back */                            
             if GetKey(SortVar, j) < Key then Swap(SortVar, SortFrom, j)                                         
    
             if j - SortFrom < SortTo - j then                                  
                /* The left part is smallest - put it on stack */                     
                if j - SortFrom > Granularity then Push SortFrom j-1               
                /* and do tail recursion on the right part */                           
                SortFrom = j + 1                                                   
             end                                                                   
             else
                /* The right part is smallest - put it on stack */                       
                if SortTo - j > Granularity then Push j+1 SortTo                   
                /* and do tail recursion on the left part */                         
                SortTo = j - 1                                                     
             end                                                                   
          end                                                                      
       end                                                                         
    
       /* Run insertionsort on the whole array to sort the small intervals */    
       InsertionSort(SortVar)                                                          
    return                                                                         
    

    此外,没有理由在小间隔上调用 InsertionSort,因为当 QuickSort 完成时,数组已大致排序,因此只剩下小间隔需要排序。而这正是 InsertionSort 的完美案例。

    如果你没有堆栈,你可以使用递归——但保留尾递归:

    QuickSort(SortVar, SortFrom, SortTo)                                                                     
       Granularity = 10                                                            
    
       /* Tail recursion loop */                           
       while SortTo - SortFrom >= Granularity                                
    
          /* Find the pivot element using median of 3 */                            
          Pivot = Median(SortVar, SortFrom, (SortFrom + SortTo) / 2, SortTo)             
          /* Put the pivot element in front */                                     
          if Pivot > SortFrom then Swap(SortVar, SortFrom, Pivot)
    
          /* Place elements <=Key to the left and elements >Key to the right */           
          Key = GetKey(SortVar, SortFrom)                                                
          i = SortFrom + 1                                                      
          j = SortTo                                                            
          while i < j                                                        
             while GetKey(SortVar, i) <= Key; i = i + 1; end                          
             while GetKey(SortVar, j) > Key; j = j - 1; end                           
             if i < j then Swap(SortVar, i, j)                                       
          end                                                                   
    
          /* Put the pivot element back */                            
          if GetKey(j) < Key then Swap(SortVar, SortFrom, j)                                         
    
          if j - SortFrom < SortTo - j then                                  
             /* The left part is smallest - recursive call */                     
             if j - SortFrom > Granularity then QuickSort(SortVar, SortFrom, j-1)           
             /* and do tail recursion on the right part */                           
             SortFrom = j + 1                                                   
          end                                                                   
          else
             /* The right part is smallest - recursive call */                       
             if SortTo - j > Granularity then QuickSort(SortVar, j+1, SortTo)                   
             /* and do tail recursion on the left part */                         
             SortTo = j - 1                                                     
          end                                                                   
       end                                                                         
    
       /* Run insertionsort on the whole array to sort the small intervals */    
       InsertionSort(SortVar)                                                          
    return                                                                         
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-07-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-26
      • 1970-01-01
      相关资源
      最近更新 更多