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

Posted

技术标签:

【中文标题】带有“三的中位数”枢轴选择的快速排序:了解过程【英文标题】:Quicksort w/ "median of three" pivot selection: Understanding the process 【发布时间】:2015-11-29 03:50:19 【问题描述】:

我们正在课堂上介绍快速排序(使用数组)。我一直在努力思考他们希望我们的快速排序分配如何与“三的中位数”枢轴选择方法一起工作。我只需要一个高层次的解释它是如何工作的。我们的文字没有帮助,我很难通过谷歌搜索找到明确的解释。

这是我目前认为理解的:

“三的中位数”函数采用index 0(第一)、array_end_index(最后)和(index 0 + array_end_index)/2(中间)中的元素。计算具有这 3 个中值的指数。返回对应的索引。

函数参数如下:

/* @param left
*       the left boundary for the subarray from which to find a pivot
* @param right
*       the right boundary for the subarray from which to find a pivot
* @return
*       the index of the pivot (middle index); -1 if provided with invalid input
*/
int QS::medianOfThree(int left, int right)

然后,在“分区”函数中,索引与“三的中位数”函数返回的数字匹配的数字作为枢轴。我的任务指出,为了继续对数组进行分区,枢轴必须位于左右边界之间中间。问题是,我们的“三的中位数”函数返回三个索引之一:第一个、中间或最后一个索引。这三个索引中只有一个(中间)可以“介于”任何东西之间。

函数参数如下:

/* @param left
*       the left boundary for the subarray to partition
* @param right
*       the right boundary for the subarray to partition
* @param pivotIndex
*       the index of the pivot in the subarray
* @return
*       the pivot's ending index after the partition completes; -1 if
*       provided with bad input
*/
int QS::partition(int left, int right, int pivotIndex)

我误会了什么?

以下是函数的完整说明

/*
* sortAll()
*
* Sorts elements of the array.  After this function is called, every
* element in the array is less than or equal its successor.
*
* Does nothing if the array is empty.
*/
void QS::sortAll()

/*
* medianOfThree()
*
* The median of three pivot selection has two parts:
*
* 1) Calculates the middle index by averaging the given left and right indices:
*
* middle = (left + right)/2
*
* 2) Then bubble-sorts the values at the left, middle, and right indices.
*
* After this method is called, data[left] <= data[middle] <= data[right].
* The middle index will be returned.
*
* Returns -1 if the array is empty, if either of the given integers
* is out of bounds, or if the left index is not less than the right
* index.
*
* @param left
*       the left boundary for the subarray from which to find a pivot
* @param right
*       the right boundary for the subarray from which to find a pivot
* @return
*       the index of the pivot (middle index); -1 if provided with invalid input
*/
int QS::medianOfThree(int left, int right)

/*
* Partitions a subarray around a pivot value selected according to
* median-of-three pivot selection.
*
* The values which are smaller than the pivot should be placed to the left
* of the pivot; the values which are larger than the pivot should be placed
* to the right of the pivot.
*
* Returns -1 if the array is null, if either of the given integers is out of
* bounds, or if the first integer is not less than the second integer, OR IF THE
* PIVOT IS NOT BETWEEN THE TWO BOUNDARIES.
*
* @param left
*       the left boundary for the subarray to partition
* @param right
*       the right boundary for the subarray to partition
* @param pivotIndex
*       the index of the pivot in the subarray
* @return
*       the pivot's ending index after the partition completes; -1 if
*       provided with bad input
*/
int QS::partition(int left, int right, int pivotIndex)

【问题讨论】:

"将返回中间索引。"它就在文档中。它将返回中间索引,而不是中位数的索引。 【参考方案1】:

首先了解快速排序,然后是三个中位数。

要执行快速排序:

    从您要排序的数组中选择一个项目(任何项目都可以,但我们会回来讨论哪个是最好的)。 对数组重新排序,使所有小于您选择的项在数组中位于它之前,而所有大于它的项在它之后。 对您选择的项目之前和之后的集合递归地执行上述操作。

第 2 步称为“分区操作”。考虑一下您是否有以下情况:

3 2 8 4 1 9 5 7 6

现在假设您选择了这些数字中的第一个作为您的枢轴元素(我们在步骤 1 中选择的那个)。在我们应用第 2 步后,我们最终会得到如下结果:

2 1 3 4 8 9 5 7 6

3 的值现在在正确的位置,并且每个元素都在正确的一侧。如果我们现在对左侧进行排序,我们最终得到:

1 2 3 4 8 9 5 7 6.

现在,让我们只考虑它右边的元素:

4 8 9 5 7 6.

如果我们选择4 作为下一步,我们最终什么都不会改变,因为它一开始就处于正确的位置。它左边的元素集是空的,所以这里什么也不做。我们现在需要对集合进行排序:

8 9 5 7 6.

如果我们使用 8 作为支点,我们最终会得到:

5 7 6 8 9.

现在右边的9 只有一个元素,所以显然已经排序了。 5 7 6 留待排序。如果我们以5 为中心,我们最终会不理会它,我们只需要将7 6 排序为6 7

现在,考虑到更广泛背景下的所有这些变化,我们最终得出的结论是:

1 2 3 4 5 6 7 8 9.

所以再次总结一下,快速排序选择一个项目,在它周围移动元素,以便它们都相对于该项目正确定位,然后对剩余的两个集合递归执行相同的操作,直到没有未排序的块留下,一切都已排序。

让我们回到我说“任何物品都可以”时在那边捏造的事情。虽然确实任何物品都可以,但您选择的物品会影响性能。如果幸运的话,您最终会在与 n log n 成比例的操作中执行此操作,其中 n 是元素的数量。如果您足够幸运,它将是一个稍大的数字,仍然与 n log n 成正比。如果你真的不走运,那将是一个正比于一个正比于 n2 的数。

那么什么是最好的选择呢?最好的数字是完成分区操作后将在中间结束的项目。但我们不知道那是什么项目,因为要找到中间项目,我们必须对所有项目进行排序,而这正是我们最初尝试做的事情。

所以,我们可以采取一些策略:

    去第一个,因为,嗯,为什么不呢?

    选择中间那个,因为可能由于某种原因,数组已经排序或几乎排序,如果没有排序,它也不会比其他选择更糟糕。

    随机选择一个。

    选择第一个、中间一个和最后一个,然后选择这三个中的中间值,因为它至少是这三个选项中最好的。

    为数组的前三分之一选择三个中值,第二个三分之一的三个中值,最后三分之一的三个中值,然后最后选择中值这三个中位数。

这些有不同的优缺点,但总的来说,这些选项中的每一个在选择最佳支点方面都比前一个提供了更好的结果,但代价是要花费更多的时间和精力来选择那个支点。 (作为某种 DoS 攻击的一部分,Random 的另一个优势是可以击败有人故意尝试创建您会遇到更糟糕情况的数据的情况)。

我的作业指出,为了继续对数组进行分区,枢轴必须位于左右边界之间。

是的。当我们将3 排序到正确的位置并排序到左边时,再次考虑上面的情况:

1 2 3 4 8 9 5 7 6.

现在,我们需要对范围4 8 9 5 7 6 进行排序。 边界34 之间的线以及6 和数组末尾之间的线(或者另一种看待它的方式,边界是4和 6,但它是一个 inclusive 边界,包括这些项目)。因此,我们选择的三个是4(第一个)6(最后一个)和95,这取决于我们在将计数除以2时是向上还是向下(我们可能向下取整因为这在整数除法中很常见,所以9)。所有这些都在我们当前正在排序的分区的边界内。因此,我们的三分之二是6(或者如果我们确实四舍五入,我们会选择5)。

(顺便说一句,一个总是选择最佳枢轴的神奇完美枢轴选择器只会选择67,因此在这里选择6 非常好,尽管仍然存在中位数-of-three 将是不幸的,最终会选择第三个更差的选项,或者甚至可能是从 3 个相同的元素中任意选择,所有这些都是更糟糕的。与其他方法相比,发生这种情况的可能性要小得多)。

【讨论】:

非常感谢您详尽的高级解释。那么,一个问题:你说,在最后一个例子中,边界是34 之间以及6 之后的“线”。我必须以ints 的身份将边界传递给我的partition() 函数。这将如何运作? "(或者换一种说法,边界是 4 和 6,但它是包含这些项目的包容性边界)" 明白了! 答案溢出 :-) +1 虽然我认为问题只是措辞和规格不佳【参考方案2】:

medianOfThree 的文档说:

* 2) Then bubble-sorts the values at the left, middle, and right indices.
*
* After this method is called, data[left] <= data[middle] <= data[right].
* The middle index will be returned.

所以您的描述与文档不符。您正在做的是对数据中的第一个、中间和最后一个元素就地进行排序,并始终返回中间索引。

因此,可以保证枢轴索引位于边界之间(除非中间最终位于边界中...)。

即便如此,旋转边界并没有错...

【讨论】:

所以,如果我有一个包含 9 个元素(索引 0-8)的数组,我的 medianOfThree() 函数将始终返回(索引)4?为什么要退货?为什么不让“medianOfThree”成为一个 void 函数,以 data[left] &lt;= data[middle] &lt;= data[right] 顺序排列第一个、中间和最后一个元素并让 partition() 计算中间索引?还是我仍然不明白? 是的。身份证。身份证。我同意你的观点,但这就是我从文档中得到的。有时家庭作业的规格很奇怪,就像现实世界的开发人员工作一样。 有机会成为一个挑剔的学生:middle = (left + right)/2 很懒,因为它会导致溢出。这样做更安全middle = left + (right-left)/2 哦,兄弟。我想我将不得不学习如何理解与我不同思维过程的人,对吧? :] 而且,关于您的第二条评论,这实际上也正是我们的文本所说的。我认为他们有正当理由将其降低。但我会坚持书上所说的——如果可以的话,最好使用好的做法。【参考方案3】:

计算“三的中位数”是一种在数组中获取伪中位数元素并使该索引等于您的分区的方法。这是一种粗略估计数组中位数的简单方法,从而提高性能。

为什么这会有用?因为理论上,你希望这个分区值成为你数组的真正中位数,所以当你对这个数组进行快速排序时,枢轴会平均划分这个数组,并启用快速排序的 O(NlogN) 排序时间给你。

例子:你的数组是:

[5,3,1,7,9]

3 的中位数分别是 5、1 和 9。中位数显然是 5,所以这是我们要为快速排序的分区函数考虑的枢轴值。接下来你可以做的是将这个索引与最后一个交换并得到 ​​p>

[9,3,1,7,5]

现在我们尝试将所有小于 5 的值放在中间的左侧,将所有大于 5 的值放在中间的右侧。我们现在得到

[1,3,7,9,5]

用中间交换最后一个元素(我们存储分区值的地方)

[1,3,5,9,7]

这就是使用 3 中间的想法。想象一下,如果我们的分区是 1 或 9。你可以想象我们得到的这个数组不是快速排序的好例子。

【讨论】:

以上是关于带有“三的中位数”枢轴选择的快速排序:了解过程的主要内容,如果未能解决你的问题,请参考以下文章

快速排序从入门到精通

快速排序:选择枢轴

如何在快速排序中选择枢轴值?

快速排序

混合快速/合并排序对随机数据的性能

数据结构与算法之三 深入学习排序