快速排序:选择枢纽


109

实施Quicksort时,要做的一件事情是选择一个枢轴。但是当我看下面的伪代码时,不清楚如何选择支点。列表的第一个元素?还有吗

 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))

有人可以帮助我掌握选择支点的概念,以及不同的情况是否需要不同的策略。


Answers:


87

选择随机数据透视可最大程度地降低遇到最坏情况O(n 2)性能的机会(始终选择第一个或最后一个将对几乎排序或几乎反向排序的数据造成最坏情况的性能)。在大多数情况下,选择中间元素也是可以接受的。

另外,如果您自己实现此功能,则有一些算法可以就地运行(即,无需创建两个新列表然后将它们串联在一起)。


10
我的观点是,自己实施搜索可能不值得付出努力。另外,要小心选择随机数,因为随机数生成器有时会很慢。
PeterAllenWebb

@Jonathan Leffler的答案更好
Nathan

60

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


更新:将评论纳入答案。

mdkess断言:

'3的中位数'不是第一倒数第二。选择三个随机索引,并取其中间值。关键是要确保对枢轴的选择不是确定性的-如果是这样,最坏的情况下可以很容易地生成数据。

我对此回应:

  • P. Kirschenhofer,H Prodinger和CMartínez对Hoare的具有三位数中位数的查找算法进行分析(1997年),CMartínez支持您的争论(“三位数中位数”是三个随机项)。

  • portal.acm.org上有一篇文章,描述的是HannuErkiö撰写的“三位数中位数的最差情况置换”,发表于1984年第27卷第3期,《计算机杂志》。[更新2012-02- 26:得到了文章的文字。第2节“算法”开始:' 通过使用A [L:R]的第一个,中间和最后一个元素的中位数,可以在大多数实际情况下将大小有效地划分为大小相等的部分。'因此,它正在讨论Mo3的倒数第二个方法。]

  • 另一篇有趣的短文是MD McIlroy 撰写的“ Quicksort的杀手Ad”,发表在《软件实践与经验》第一卷。29(0),1-4(0 1999)。它说明了如何使几乎所有Quicksort都具有二次行为。

  • AT&T贝尔实验室技术杂志,1984年10月,“构建工作排序例程的理论和实践”指出:“ Hoare建议在几条随机选择的行的中值附近进行划分。Sedgewick建议选择第一个行的中值。 ..]末尾和中间”。这表明在文献中已知两种用于“三中位数”的技术。(2014年11月23日更新:如果您已经成为会员或准备付费,则这篇文章似乎可以在IEEE Xplore或从Wiley上获得。)

  • JL Bentley和MD McIlroy于1993年11月在《软件实践与经验》第23(11)卷上发表的“设计排序函数”进行了广泛的讨论,他们选择了一种自适应分区算法,该算法部分基于数据集的大小。关于各种方法的折衷方法有很多讨论。

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

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


4
3的中位数不是第一倒数第二。选择三个随机索引,并取其中间值。关键是要确保对枢轴的选择不是确定性的-如果是这样,最坏的情况下可以很容易地生成数据。
mindvirus

我正在阅读abt introsort,它结合了quicksort和heapsort的良好功能。使用中位数3来选择枢轴的方法可能并不总是很理想。
Sumit Kumar Saha 2013年

4
选择随机索引的问题是随机数生成器非常昂贵。尽管它不会增加big-O排序的成本,但它可能会使事情比只选择了第一个,最后一个和中间元素的情况要慢。(在现实世界中,我敢打赌,没人会在人为的情况下拖延您的快速排序。)
Kevin Chen

20

嘿,我刚教过这堂课。

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

但是,选择任意元素会冒将n大小的数组错误地分为大小为1和n-1的两个数组的风险。如果您经常这样做,那么您的快速排序就有可能变成O(n ^ 2)。

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

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

如果您仍然遇到问题,请选择中间路线。


1
我们在课堂上做了一个实验,以排序的顺序从数组中获取了k个最小的元素。我们生成了随机数组,然后使用最小堆或随机选择和固定枢轴快速排序,并计算了比较次数。在此“随机”数据上,第二种解决方案的平均效果要比第一种解决方案差。切换到随机数据透视可以解决性能问题。因此,即使对于所谓的随机数据,固定数据透视表的性能也远比随机数据透视表差。
罗伯特·S·巴恩斯

为什么将大小为n的数组分为大小为1和n-1的两个数组冒着成为O(n ^ 2)的风险?
亚伦·弗兰克

假定大小为N的数组。划分为大小[1,N-1]。下一步是将右半部分划分为[1,N-2]。依此类推,直到我们得到大小为1的N个分区。但是,如果要进行一半的划分,则每步将进行2个N / 2的分区,这导致了复杂度的Log(n)项。
克里斯·库德莫

11

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

更好的技术是3中位数方法,您可以随机选择三个元素,然后选择中间元素。您知道选择的元素不会是第一个或最后一个,但根据中心极限定理,中间元素的分布将是正态的,这意味着您将趋向于中间(因此,n lg n次)。

如果您绝对要保证算法的运行时间为O(nlgn),则用于查找数组中位数的5列方法将以O(n)时间运行,这意味着在最坏的情况下quicksort的递归方程将是T(n)= O(n)(找到中位数)+ O(n)(分区)+ 2T(n / 2)(左右递归。)根据主定理,这是O(n lg n) 。但是,常数会很大,如果最主要的情况是最坏的情况,请改用合并排序,合并排序平均比快速排序慢一点,并且可以保证O(nlgn)时间(并且会更快)比这个la脚的中位数快速排序)。

中值算法中值的解释


6

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

例如,管风琴分布(1,2,3 ... N / 2..3,2,1)的首尾都将为1,而随机指数将大于1的某个数字,取中位数为1( (无论是第一个还是最后一个),您都将获得非常不平衡的分区。


2

将快速排序分为三个部分更容易做到这一点

  1. 交换或交换数据元素功能
  2. 分区功能
  3. 处理分区

它仅比一个长函数无效一点,但更容易理解。

代码如下:

/* 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
  };
};

1

它完全取决于如何对数据进行排序。如果您认为这将是伪随机的,那么最好的选择是选择一个随机选择或选择中间选项。


1

如果要对随机可访问的集合(例如数组)进行排序,通常最好选择物理中间项。这样,如果所有阵列都准备好排序(或几乎排序),则两个分区将接近偶数,并且将获得最佳速度。

如果您仅对线性访问进行排序(例如链表),那么最好选择第一项,因为这是访问最快的项。但是,在这里,如果列表已经排序,就很麻烦–一个分区将始终为空,另一个分区将具有所有内容,从而产生最差的时间。

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


0

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


1
马车在这里。
ncmathsadist

0

快速排序的复杂性随选择枢轴值而有很大差异。例如,如果您始终选择第一个元素作为枢轴,则算法的复杂度将达到O(n ^ 2)。这是选择枢轴元素的聪明方法-1.选择数组的第一个,中间,最后一个元素。2.比较这三个数字,找出大于一个且小于另一个(即中位数)的数字。3.将此元素作为枢轴元素。

通过这种方法选择枢轴将数组拆分为近一半,因此复杂度降低为O(nlog(n))。


0

平均而言,中位数3对小n有益。对于较大的n,中位数5更好一些。ninter是“三个中位数的三个中位数”,对于非常大的n甚至更好。

采样越多,n越大,得到的效果越好,但是随着采样的增加,改善会大大减慢。这样就招致了采样和分类样品的开销。


0

我建议使用中间索引,因为它很容易计算。

您可以通过四舍五入来计算它(array.length / 2)。


-1

在真正优化的实现中,选择支点的方法应取决于阵列的大小-对于大型阵列,花更多的时间选择一个好的支点是值得的。如果不做全面的分析,我想“ O(log(n))元素的中间”是一个很好的开始,而且这样做的好处是不需要任何额外的内存:在较大的分区和内部使用尾调用位置分区,我们在算法的几乎每个阶段都使用相同的O(log(n))额外内存。


1
找到3个元素的中间位置可以在固定时间内完成。再说了,我们本质上必须对子数组进行排序。随着n变大,我们再次遇到排序问题。
克里斯·库德莫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.