快速排序:选择枢轴

Posted

技术标签:

【中文标题】快速排序:选择枢轴【英文标题】:Quicksort: Choosing the pivot 【发布时间】:2010-09-14 22:00:39 【问题描述】:

在实现快速排序时,您必须做的一件事就是选择一个轴。但是当我看下面这样的伪代码时,我不清楚我应该如何选择枢轴。列表的第一个元素?还有什么?

 function quicksort(array)
     var list less, greater
     if length(array) ≤ 1  
         return array  
     select and remove a pivot value pivot from array
     for each x in array
         if x ≤ pivot then append x to less
         else append x to greater
     return concatenate(quicksort(less), pivot, quicksort(greater))

谁能帮我理解选择支点的概念,以及不同的场景是否需要不同的策略。

【问题讨论】:

***.com/questions/1688264/improving-the-quick-sort 【参考方案1】:

选择随机枢轴可以最大限度地减少遇到最坏情况 O(n2) 性能的机会(总是选择第一个或最后一个会导致接近排序或接近反向的最坏情况性能-排序的数据)。在大多数情况下,选择中间元素也是可以接受的。

此外,如果您自己实现此功能,则有一些算法版本可以就地工作(即无需创建两个新列表然后将它们连接起来)。

【讨论】:

我认为自己实施搜索可能不值得付出努力。另外,请注意如何选择随机数,因为随机数生成器有时会有点慢。 @Jonathan Leffler 的回答更好【参考方案2】:

这取决于您的要求。随机选择一个支点会使创建产生 O(N^2) 性能的数据集变得更加困难。 “三个中位数”(第一个、最后一个、中间)也是一种避免问题的方法。不过,请注意比较的相对表现;如果您的比较代价高昂,那么 Mo3 会进行比随机选择(单个枢轴值)更多的比较。比较数据库记录的成本可能很高。


更新:将 cmets 拉入答案。

mdkess 断言:

“3 的中位数”不是第一个最后一个中间值。选择三个随机索引,并取其中间值。关键是要确保您对枢轴的选择不是确定性的——如果是,则可以很容易地生成最坏情况的数据。

我的回复:

Analysis Of Hoare's Find Algorithm With Median-Of-Three Partition (1997) 作者:P Kirschenhofer、H Prodinger、C Martínez 支持您的论点(“三中的中位数”是三个随机项目)。

portal.acm.org 上描述了一篇关于 Hannu Erkiö 的“The Worst Case Permutation for Median-of-Three Quicksort”的文章,发表在 The Computer Journal,第 27 卷,第 3 期,1984 年。[更新2012-02-26:收到article 的文本。第 2 节“算法”开始:'通过使用 A[L:R] 的第一个、中间和最后一个元素的中值,可以在大多数实际情况下实现有效的划分为大小相当的部分。 em>' 因此,它正在讨论前中后 Mo3 方法。]

另一篇有趣的短文是 M. D. McIlroy,"A Killer Adversary for Quicksort",发表在 Software-Practice and Experience, Vol. 上。 29(0),1-4(0 1999)。它解释了如何使几乎所有快速排序的行为都成为二次方。

AT&T 贝尔实验室技术杂志,1984 年 10 月“构建工作排序例程的理论与实践”指出“Hoare 建议围绕几条随机选择的行的中位数进行分区。Sedgewick [...] 建议选择第一个 [...] 最后一个 [...] 和中间的中位数"。这表明文献中已知两种用于“三中位数”的技术。 (2014 年 11 月 23 日更新:这篇文章似乎可以通过IEEE Xplore 或Wiley 获得——如果您有会员资格或准备付费。)

'Engineering a Sort Function' 由 JL Bentley 和 MD McIlroy 于 1993 年 11 月在 Software Practice and Experience 卷 23(11) 上发表,对这些问题进行了广泛的讨论,他们选择了一种基于部分数据集的大小。有很多关于各种方法的权衡的讨论。

Google 搜索“三中位数”非常适合进一步跟踪。

感谢您的信息;我之前只遇到过确定性的“三中位数”。

【讨论】:

3 的中位数不是第一个最后一个中间值。选择三个随机索引,并取其中间值。关键是要确保您选择的枢轴不是确定性的——如果是,则可以很容易地生成最坏情况的数据。 我正在阅读 abt introsort,它结合了快速排序和堆排序的良好特性。使用 3 的中位数选择枢轴的方法可能并不总是有利的。 选择随机索引的问题是随机数生成器非常昂贵。虽然它不会增加排序的大 O 成本,但它可能会使事情变得比你刚刚选择第一个、最后一个和中间元素时更慢。 (在现实世界中,我敢打赌,没有人会制造人为的情况来减慢您的快速排序。)【参考方案3】:

如果您要对可随机访问的集合(如数组)进行排序,通常最好选择物理中间项。这样一来,如果数组都已排序(或接近排序),则两个分区将接近均匀,您将获得最佳速度。

如果您正在对仅具有线性访问权限的内容进行排序(如链表),那么最好选择第一项,因为它是访问速度最快的项。然而,在这里,如果列表已经排序,那么你就完蛋了——一个分区将始终为空,而另一个分区拥有一切,这会产生最糟糕的情况。

但是,对于链表,选择除第一个之外的任何内容只会让事情变得更糟。它选择列出列表中的中间项目,您必须在每个分区步骤中逐步完成它 - 添加一个 O(N/2) 操作,该操作执行 logN 次,总时间为 O(1.5 N *log N)那就是如果我们在开始之前就知道列表有多长——通常我们不知道,所以我们必须一路走过去数一数,然后走一半找到中间,然后一步一步走第三次做实际分区:O(2.5N * log N)

【讨论】:

【参考方案4】:

这完全取决于您的数据的排序方式。如果您认为它是伪随机的,那么您最好的选择是随机选择或选择中间。

【讨论】:

【参考方案5】:

呵呵,我刚教过这门课。

有多种选择。 简单:选择范围的第一个或最后一个元素。 (对部分排序的输入不利) 更好:选择范围中间的项目。 (对部分排序的输入更好)

但是,选择任意元素存在将大小为 n 的数组划分为大小为 1 和 n-1 的两个数组的风险。如果你经常这样做,你的快速排序就有变成 O(n^2) 的风险。

我看到的一个改进是选择中位数(first, last, mid); 在最坏的情况下,它仍然可以达到 O(n^2),但从概率上讲,这是一种罕见的情况。

对于大多数数据,选择第一个或最后一个就足够了。但是,如果你发现你经常遇到最坏的情况(部分排序的输入),第一个选择是选择中心值(对于部分排序的数据来说,这是一个统计上很好的支点)。

如果你仍然遇到问题,那就走中间路线。

【讨论】:

我们在课堂上做了一个实验,按排序顺序从数组中获取 k 个最小的元素。我们生成随机数组,然后使用最小堆或随机选择和固定枢轴快速排序并计算比较次数。在这个“随机”数据上,第二个解决方案的平均表现比第一个更差。切换到随机枢轴可以解决性能问题。因此,即使对于所谓的随机数据,固定枢轴的性能也明显比随机枢轴差。 为什么将大小为 n 的数组划分为大小为 1 和 n-1 的两个数组会冒变成 O(n^2) 的风险? 假设一个大小为 N 的数组。分区为大小 [1,N-1]。下一步是将右半部分划分为 [1, N-2]。依此类推,直到我们有 N 个大小为 1 的分区。但是,如果我们要分成两半,我们将在每一步中进行 2 个 N/2 分区,从而导致复杂性的 Log(n) 项; 【参考方案6】:

永远不要选择固定的枢轴 - 这可能会被攻击以利用算法的最坏情况 O(n2) 运行时,这只是自找麻烦。快速排序的最坏情况运行时发生在分区结果为一个包含 1 个元素的数组和一个包含 n-1 个元素的数组时。假设您选择第一个元素作为分区。如果有人向您的算法提供一个按降序排列的数组,那么您的第一个枢轴将是最大的,因此数组中的其他所有内容都将移动到它的左侧。然后当您递归时,第一个元素将再次成为最大元素,因此您再次将所有内容放在它的左侧,依此类推。

更好的技术是 median-of-3 方法,您可以随机选择三个元素,然后选择中间。你知道你选择的元素不会是第一个或最后一个,而且,根据中心极限定理,中间元素的分布将是正常的,这意味着你会倾向于中间(因此, nlog(n) 时间)。

如果您绝对想保证算法的运行时间为 O(nlog(n)),则用于查找数组中位数的 columns-of-5 方法在 O(n) 时间内运行,这意味着在最坏情况下快速排序的递归方程将是:

T(n) = O(n) (find the median) + O(n) (partition) + 2T(n/2) (recurse left and right)

根据主定理,这是 O(nlog(n))。然而,常数因子会很大,如果最坏情况下的性能是您最关心的问题,请改用合并排序,它平均只比快速排序慢一点,并保证 O(nlog(n)) 时间(并且将比这个蹩脚的中位数快速排序要快得多)。

Explanation of the Median of Medians Algorithm

【讨论】:

【参考方案7】:

不要试图变得太聪明并结合枢轴策略。如果您通过选择第一个、最后一个和中间随机索引的中值来将 3 的中值与随机枢轴相结合,那么您仍然容易受到许多发送 3 二次方中值的分布的影响(所以它实际上比普通随机枢轴)

例如,管风琴分布 (1,2,3...N/2..3,2,1) 第一个和最后一个都为 1,随机索引将是大于 1 的某个数字,取中位数为 1 (第一个或最后一个),你会得到一个极端不平衡的分区。

【讨论】:

【参考方案8】:

这样做更容易将快速排序分为三个部分

    交换或交换数据元素函数 配分函数 处理分区

它只比一个长函数效率低一点,但更容易理解。

代码如下:

/* This selects what the data type in the array to be sorted is */

#define DATATYPE long

/* This is the swap function .. your job is to swap data in x & y .. how depends on
data type .. the example works for normal numerical data types .. like long I chose
above */

void swap (DATATYPE *x, DATATYPE *y)  
  DATATYPE Temp;

  Temp = *x;        // Hold current x value
  *x = *y;          // Transfer y to x
  *y = Temp;        // Set y to the held old x value
;


/* This is the partition code */

int partition (DATATYPE list[], int l, int h)

  int i;
  int p;          // pivot element index
  int firsthigh;  // divider position for pivot element

  // Random pivot example shown for median   p = (l+h)/2 would be used
  p = l + (short)(rand() % (int)(h - l + 1)); // Random partition point

  swap(&list[p], &list[h]);                   // Swap the values
  firsthigh = l;                                  // Hold first high value
  for (i = l; i < h; i++)
    if(list[i] < list[h])                  // Value at i is less than h
      swap(&list[i], &list[firsthigh]);   // So swap the value
      firsthigh++;                        // Incement first high
    
  swap(&list[h], &list[firsthigh]);           // Swap h and first high values
  return(firsthigh);                          // Return first high
;



/* Finally the body sort */

void quicksort(DATATYPE list[], int l, int h)

  int p;                                      // index of partition 
  if ((h - l) > 0) 
    p = partition(list, l, h);              // Partition list 
    quicksort(list, l, p - 1);        // Sort lower partion
    quicksort(list, p + 1, h);              // Sort upper partition
  ;
;

【讨论】:

【参考方案9】:

理想情况下,枢轴应该是整个数组的中间值。 这将减少获得最坏情况性能的机会。

【讨论】:

这里的马车在前面。【参考方案10】:

在真正优化的实现中,选择枢轴的方法应取决于数组大小 - 对于大型数组,花更多时间选择一个好的枢轴是值得的。如果不进行全面分析,我猜“O(log(n)) 个元素的中间”是一个好的开始,这还有一个额外的好处,就是不需要任何额外的内存:在较大的分区上使用尾调用并在 -位置分区,我们几乎在算法的每个阶段都使用相同的 O(log(n)) 额外内存。

【讨论】:

找到3个元素的中间可以在常数时间内完成。再多的,我们基本上必须对子数组进行排序。随着 n 变大,我们又回到了排序问题。【参考方案11】:

快速排序的复杂性随着枢轴值的选择而变化很大。例如,如果您总是选择第一个元素作为枢轴,算法的复杂性会变得像 O(n^2) 一样糟糕。这是选择枢轴元素的明智方法- 1.选择数组的第一个、中间、最后一个元素。 2. 比较这三个数,找出大于一且小于另一数的数,即中位数。 3. 将此元素作为枢轴元素。

通过这种方法选择枢轴将数组分成近两半,因此复杂性 减少到 O(nlog(n))。

【讨论】:

【参考方案12】:

平均而言,中位数为 3 对较小的 n 有利。对于较大的 n,中位数为 5 会更好一些。第九个,即“三个中值的三个中值”对于非常大的 n 甚至更好。

随着 n 的增加,采样次数越高,效果越好,但随着采样次数的增加,改进会显着减慢。而且您会产生采样和分类样本的开销。

【讨论】:

【参考方案13】:

我建议使用中间索引,因为它可以很容易地计算出来。

你可以通过取整(array.length / 2)来计算。

【讨论】:

以上是关于快速排序:选择枢轴的主要内容,如果未能解决你的问题,请参考以下文章

带有“三的中位数”枢轴选择的快速排序:了解过程

快速排序

使用第一个元素作为枢轴进行快速排序[关闭]

快速排序从入门到精通

快速排序

C++ 快速排序枢轴优化