快速sorting:select枢轴

在实现Quicksort时,你需要做的一件事是select一个数据透视表。 但是当我看下面的伪代码时,我不清楚应该如何select数据透视表。 列表的第一个元素? 别的东西?

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

有人可以帮助我理解select枢纽的概念,以及不同的情景是否需要不同的策略。

select一个随机数据点可以最大限度地减less遇到最坏情况的O(n 2 )性能(总是select第一个或最后一个会导致近似sorting或接近反向sorting的数据的最差情况)。 在大多数情况下,select中间因素也是可以接受的。

另外,如果你正在自己实现这个function,那么这个algorithm的版本可以在原地工作(即不需要创build两个新的列表,然后连接它们)。

这取决于你的要求。 随机select一个枢轴使得创build一个产生O(N ^ 2)性能的数据集变得更加困难。 “三位中间值”(第一,最后,中间)也是避免问题的一种方法。 小心比较的相对performance,虽然; 如果您的比较是昂贵的,那么Mo3会比随机select(单个数据中心值)做更多的比较。 数据库logging可能比较昂贵。


更新:将评论拉入答案。

mdkess断言:

3的中位数不是第一个最后的中间值。 select三个随机索引,并取其中间的值。 重点是要确保你select的枢纽不是确定性的 – 如果是这样,最坏的情况下数据可以很容易地产生。

我对此回答:

  • Hare Prodinger,CMartínez支持你的论点(即“三位中间值”是三个随机项目)。

  • 在portal.acm.org上有一篇关于HannuErkiö在“计算机杂志”第27卷第3期1984年出版的“三种快速sorting的最差情况排列”的文章。[Update 2012-02- 26:得到了文章的文字。 第2节“algorithm”开始:“ 通过使用A [L:R]的第一个,中间和最后一个元素的中间值,可以在大多数实际情况下实现将相等大小的部分划分为多个部分。 因此,它正在讨论Mo3的第一个倒数第一个方法。]

  • 另一篇很有意思的文章是MD McIlroy, “Quicksort的杀手” ,发表在Software-Practice and Experience,Vol。 29(0),1-4(0 1999)。 它解释了如何让几乎所有的快速sorting均呈现二次曲线。

  • 美国电话电报公司贝尔实验室技术杂志,1984年10月“理论与实践工作分类程序的build设”指出:“霍尔build议在几个随机select的线的中位数周围进行分割,Sedgewickbuild议select第一个[ ..]最后和中间“。 这表明两种“三位中间值”的技术在文献中都是已知的。 (更新2014-11-23:文章似乎可以在IEEE Xplore或Wiley上find – 如果您有会员资格或准备付费。)

  • 由JL Bentley和MD McIlroy在1993年11月出版的软件实践和经验Vol 23(11)中发表的“devise分类函数”对这些问题进行了广泛的讨论,他们select了部分基于数据集的大小。 有很多关于各种方法的权衡的讨论。

  • 谷歌search“三位数中位数”对于进一步的跟踪工作非常有效。

感谢您的信息; 我以前只遇到了确定性的“三位中间值”。

呃,我刚刚教这门课。

有几个选项。
简单:select范围的第一个或最后一个元素。 (对部分sorting的input不好)更好:select范围中间的项目。 (更好的部分sortinginput)

但是,挑选任意元素会导致将大小为n的数组划分为两个大小为1和n-1的数组。 如果你经常这样做,你的quicksort运行的风险成为O(n ^ 2)。

我看到的一个改善是select中位数(第一,最后,中); 在最坏的情况下,它仍然可以去O(n ^ 2),但概率上来说,这是一个罕见的情况。

对于大多数数据来说,select第一个或最后一个就足够了。 但是,如果您发现自己经常遇到最糟糕的情况(部分sorting的input),那么首先select中心值(对于部分sorting的数据来说,这是一个统计上良好的支点)。

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

永远不要select一个固定的关键点 – 这可以被攻击来利用你的algorithm的最坏情况O(n ^ 2)运行时,这只是要求麻烦。 快速sorting的最坏情况运行时发生在分区结果为1个元素的数组和1个n-1元素的数组时。 假设你select第一个元素作为你的分区。 如果某人给你的algorithm递减一个数组,那么你的第一个数据将是最大的,所以数组中的其他数据将移动到它的左边。 那么当你recursion的时候,第一个元素将会是最大的,所以你再一次把所有东西放在它的左边,等等。

一个更好的技术是3中位数的方法,你随机挑三个元素,并select中间。 你知道你select的元素不是第一个也不是最后一个元素,而且通过中心极限定理,中间元素的分布是正常的,这意味着你将趋向中间(因此,n次n次)。

如果您绝对想要保证algorithm的O(nlgn)运行时间,则用于查找数组中值的5列方法以O(n)时间运行,这意味着在最坏情况下快速sorting的recursion方程将(n)= O(n)(find中位数)+ O(n)(分区)+ 2T(n / 2)(左右recursion)根据主定理,这是O(n lg n) 。 但是,常数因子将是巨大的,如果最坏的情况下性能是你最关心的,那就用一个合并sorting,而这个sorting只比平均速度慢一点,并且保证O(nlgn)的时间(并且会更快比这个跛脚中位数快速sorting)。

Mediansalgorithm中值的解释

不要试图变得太聪明,并结合旋转策略。 如果通过选取中间的第一个,最后一个和一个随机指数的中位数,将3的中位数与随机数据相结合,那么您将仍然很容易受到发送中位数为3二次方程的许多分布的影响(因此实际上比普通的随机数据)

例如,一个pipe风琴的分布(1,2,3 … N / 2..3,2,1)首先和最后都将是1,随机指标将是一些大于1的数字,取中间值1(无论是第一次还是最后一次),你会得到一个完全不平衡的分区。

这完全取决于您的数据如何sorting。 如果你认为这将是伪随机的,那么你最好的select是select一个随机select或select中间。

如果您正在对随机访问的集合(如数组)进行sorting,则最好select物理中间项目。 有了这个,如果数组已经准备好sorting(或接近sorting),那么这两个分区将会接近平均,您将获得最佳速度。

如果你只用线性访问(如链接列表)来sorting,那么最好select第一个项目,因为它是访问速度最快的项目。 然而,在这里,如果列表已经被sorting,那么你就被搞砸了 – 一个分区将永远是空的,另一个分区就是一切,产生最糟糕的时间。

然而,对于一个链表来说,select除第一个之外的任何东西,只会使情况变得更糟。 它select列表中的中间项目,您必须在每个分区步骤中逐步完成 – 添加一个O(N / 2)操作,完成logN次,使总时间O(1.5 N * log N)这就是说,如果我们知道开始之前列表有多长时间 – 通常我们不这样做,所以我们不得不一步一步来计算它们,然后中途find中间,然后逐步通过第三次做实际分区:O(2.5N * log N)

把这个快速分割成三个部分比较容易

  1. 交换或交换数据元素function
  2. 分区function
  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 }; }; 

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

快速sorting的复杂性随着枢轴值的select而变化很大。 例如,如果你总是select第一个元素作为一个支点,那么algorithm的复杂度就会变成和O(n ^ 2)一样差。 这里是一个聪明的方法来select枢轴元素 – 1.select数组的第一个,中间,最后一个元素。 2.比较这三个数字,找出大于一,小于其他中位数的数字。 3.把这个元素作为主元素。

通过这种方法select枢轴将把arrays分成两半,因此复杂度降低到O(nlog(n))。

平均而言,3的中值对于小n是有利的。 对于较大的n,5的中位数好一点。 这个“三个三个中间值”的中位数对于非常大的n更好。

随着采样越高,随着n的增加,得到的结果越好,但是当你增加样本时,改进会显着减慢。 你会招致采样和分类样品的开销。

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

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

在一个真正优化的实现中,用于select数据透视表的方法应该取决于数组的大小 – 对于一个大数组来说,它花费更多的时间来select一个好的数据透视表。 如果不进行全面的分析,我会猜测“O(log(n))元素的中间”是一个好的开始,而且这还有额外的好处,不需要额外的内存:在更大的分区上使用tail-call,我们在algorithm的几乎每个阶段都使用相同的O(log(n))额外内存。