【问题标题】:QuickSort and stack overflow exception快速排序和堆栈溢出异常
【发布时间】:2010-10-30 23:11:22
【问题描述】:

我认为QuickSort在某些特定情况下可能会导致堆栈溢出异常。

在排序过程中有两种选择枢轴元素的基本方法 - 枢轴值可以是排序范围中间的元素或随机选择的元素(在排序范围内)。第二种方法(随机)是否比第一种方法更不容易发生堆栈溢出?能给我建议吗?

这是我的快速排序(Delphi)版本:

procedure QuickSort(lLowBound, lHighBound: integer; lCompare: TListSortCompare;
  lSwap: TListSortSwap);

  procedure Sort(lLowIndex, lHighIndex: integer);
  var
    lLeft: Integer;
    lRight: Integer;
    lPivot: Integer;
    lLeftCompare: Integer;
    lRightCompare: Integer;
  begin
    repeat
      lLeft := lLowIndex;
      lRight := lHighIndex;
      lPivot := (lLowIndex + lHighIndex) div 2; //the pivot as the element in the middle
      //lPivot := lLowIndex + Random(lHighIndex - lLowIndex + 1); //the pivot chosen randomly
      repeat
        lLeftCompare := lCompare(lLeft, lPivot);
        while lLeftCompare < 0 do
        begin
          Inc(lLeft);
          lLeftCompare := lCompare(lLeft, lPivot);
        end;
        lRightCompare := lCompare(lRight, lPivot);
        while lRightCompare > 0 do
        begin
          Dec(lRight);
          lRightCompare := lCompare(lRight, lPivot);
        end;

        if lLeft <= lRight then
        begin
          if not ((lLeftCompare = 0) and (lRightCompare = 0)) then
          begin
            lSwap(lRight, lLeft);

            if lPivot = lLeft then
              lPivot := lRight
            else if lPivot = lRight then
              lPivot := lLeft;
          end;
          Inc(lLeft);
          Dec(lRight);
        end;
      until lLeft > lRight;

      if (lLowIndex < lRight) then
        Sort(lLowIndex, lRight);

      lLowIndex := lLeft;
    until lLeft >= lHighIndex;
  end;

begin
  if lHighBound > lLowBound then
    Sort(lLowBound, lHighBound);
end;

提前感谢您的建议!

马吕斯。

【问题讨论】:

    标签: delphi stack-overflow quicksort


    【解决方案1】:

    使用特定索引处的任何元素(第一个、最后一个或中间)作为枢轴元素总是会导致特定数据集退化的风险。第一个和最后一个元素特别糟糕,因为它们会随着预排序(或几乎预排序)的数据而退化,这很常见。中间元素在实践中问题较少,但仍然容易受到恶意构建的数据集的攻击。

    使用随机元素意味着退化只能通过纯粹的运气来发生(假设 RNG 无法被假设的攻击者预测),所以这是一个很好的策略。进一步的改进可以显着降低被坏运气击中的可能性,即使用 3 个(或 5 个或更多)随机选择的元素的中值,但它必须权衡由此产生的额外复杂性和运行时间。

    【讨论】:

      【解决方案2】:

      提高效率的一种概率方法是选择 3 个随机元素并使用中间值(不是最大也不是最小的那个)。

      您还可以使用记录堆栈来推送和弹出边界并编写循环而不是进行递归调用(也将使用更少的内存,因为不需要为所有调用复制指向数组的指针)。

      编辑:我注意到内部过程不将指针作为参数,所以忘记那部分^_^无论如何,堆栈帧的信息不仅仅是函数的参数,所以它仍然是内存效率更高(重点是分配数据堆栈的堆通常比进程堆栈​​大)。

      【讨论】:

      • 感谢您的建议。我设法按照您建议的方式制作了快速排序的迭代版本。
      【解决方案3】:

      感谢您的回答。

      Fortran,感谢您提出有关制作非递归方法的建议。基于它们,我设法进行了迭代快速排序,它似乎工作正常:)。

      代码如下:

      procedure QuickSortI(lLowBound, lHighBound: integer; lCompare: TListSortCompare;
        lSwap: TListSortSwap);
      var
        lLeft: Integer;
        lRight: Integer;
        lPivot: Integer;
        lLeftCompare: Integer;
        lRightCompare: Integer;
        lStack: array of integer;
        lStackLen: integer;
      begin
        if lHighBound > lLowBound then
        begin
          lStackLen := 2;
          SetLength(lStack, lStackLen);
          lStack[lStackLen - 1] := lLowBound;
          lStack[lStackLen - 2] := lHighBound;
      
          repeat
            lLowBound := lStack[lStackLen - 1];
            lHighBound := lStack[lStackLen - 2];
            SetLength(lStack, lStackLen - 2);
            Dec(lStackLen, 2);
      
            lLeft := lLowBound;
            lRight := lHighBound;
            lPivot := (lLowBound + lHighBound) div 2;
            repeat
              lLeftCompare := lCompare(lLeft, lPivot);
              while lLeftCompare < 0 do
              begin
                Inc(lLeft);
                lLeftCompare := lCompare(lLeft, lPivot);
              end;
              lRightCompare := lCompare(lRight, lPivot);
              while lRightCompare > 0 do
              begin
                Dec(lRight);
                lRightCompare := lCompare(lRight, lPivot);
              end;
      
              if lLeft <= lRight then
              begin
                if not ((lLeftCompare = 0) and (lRightCompare = 0)) then
                begin
                  lSwap(lRight, lLeft);
      
                  if lPivot = lLeft then
                    lPivot := lRight
                  else if lPivot = lRight then
                    lPivot := lLeft;
                end;
                Inc(lLeft);
                Dec(lRight);
              end;
            until lLeft > lRight;
      
            if (lHighBound > lLeft) then
            begin
              Inc(lStackLen, 2);
              SetLength(lStack, lStackLen);
              lStack[lStackLen - 1] := lLeft;
              lStack[lStackLen - 2] := lHighBound;
            end;
      
            if (lLowBound < lRight) then
            begin
              Inc(lStackLen, 2);
              SetLength(lStack, lStackLen);
              lStack[lStackLen - 1] := lLowBound;
              lStack[lStackLen - 2] := lRight;
            end;
      
          until lStackLen = 0;
        end;
      end;
      

      我希望我以最佳方式实现它。我使用了一个动态数组来存储排序边界(每对项都是上下界)。

      这种迭代方式似乎比递归方式慢一些,但我觉得没那么重要。

      如果您发现错误或者您知道优化方法的方法,如果您告诉我,我将不胜感激。

      谢谢!

      马吕斯。

      【讨论】:

        【解决方案4】:

        一个不错的快速排序实现使用 O(log n) 堆栈空间。它通过首先对最小的子数组进行排序来实现这一点。如果您不这样做,最坏的情况是枢轴是最大元素并且您尝试对每次仅小一个的子数组进行排序。当您使用已排序的数据作为输入并将正确的元素作为枢轴时,就会发生这种情况。

        您的显式堆栈实现速度较慢并且遇到同样的问题(尽管它现在是堆而不是堆栈)。

        另一个缺少的是当子数组很小(5-25 个元素)时切换到插入排序。另请查看本网站上的双轴快速排序问题。

        【讨论】: