在我们的课堂上,我们被介绍了快速排序(带有数组)。我一直在撞墙,试图理解他们希望我们的快速排序作业如何与“三的中位数”枢轴选择方法一起工作。我只需要一个关于它是如何工作的高级解释。我们的文本没有帮助,我很难在谷歌上找到一个清晰的解释。
这是我到目前为止想明白的:
三的中位数函数取index 0
(第一个)、array_end_index
(最后一个)和(index 0array_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){}
首先了解快速排序,然后是3的中位数。
要执行快速排序,您:
第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成比例的数字。
那么哪个是最好的选择?最好的数字是在你完成分区操作后最终会在中间的项目。但是我们不知道那是什么项目,因为要找到中间项目,我们必须对所有项目进行排序,这就是我们首先要做的。
因此,我们可以采取一些策略:
>
就选第一个,因为,嗯,为什么不呢?
选择中间的那个,因为也许数组已经排序或由于某种原因几乎排序,如果没有,它也不比任何其他选择更糟糕。
随便挑一个。
选择第一个、中间一个和最后一个,然后选择这三个选项的中位数,因为它至少是这三个选项中最好的。
选择数组前三分之一的中位数为3,第二三分之一的中位数为3,最后三分之一的中位数为3,然后最后选择这三个中位数的中位数。
这些有不同的利弊,但总的来说,这些选项中的每一个都比前一个选项在选择最佳支点方面给出了更好的结果,但代价是在选择支点上花费更多的时间和精力。(随机还有一个进一步的优势,那就是击败有人故意试图创建数据的情况,而你会在这些数据上有更糟糕的行为,作为某种DoS攻击的一部分)。
我的作业指出,为了继续对数组进行分区,支点必须位于左右边界之间。
是的。当我们将3
排序到正确的位置并排序为左侧时,请再次考虑上述内容:
1 2 3 4 8 9 5 7 6.
现在,这里我们需要对范围4 8 9 5 7 6
进行排序。边界是3
和4
之间的线以及6
和数组末尾之间的线(或者另一种看待它的方式,边界是4和6,但它是包含这些项目的包容性边界)。因此,我们选择的是4
(第一个)6
(最后一个)以及9
或5
,这取决于我们在将计数除以2时是向上还是向下舍入(我们可能会像整数除法中常见的那样向下舍入,因此9
)。所有这些都在我们当前排序的分区的边界内。我们的三个中位数是6
(或者如果我们确实四舍五入,我们会选择5
)。
(顺便说一句,一个神奇完美的支点选择器,总是选择最好的支点,只会选择6
或7
,所以选择6
在这里是相当好的,尽管仍然有一些时候,三个中位数会不吉利,最终选择第三个更差的选项,或者甚至是从三个相同的元素中任意选择,所有这些元素都更差。这比其他方法发生的可能性要小得多)。
medianOf三
的留档说:
* 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.
所以你的描述与留档不匹配。你所做的是对数据中的第一个、中间和最后一个元素进行就地排序,并始终返回中间索引。
因此,可以保证枢轴索引在边界之间(除非当中间结束时在边界中…)。
即便如此,改变界限也没什么错…
计算“三的中位数”是一种在数组中获取伪中位数元素并使该索引等于您的分区的方法。这是一种粗略估计数组中位数的简单方法,从而获得更好的性能。
为什么这很有用?因为理论上,你希望这个分区值是你数组的真正中值,所以当你对这个数组进行快速排序时,支点会将这个数组平均划分,并启用快速排序给你的很好的O(NlogN)排序时间。
示例:您的数组是:
[5,3,1,7,9]
三的中位数将查看5、1和9。中位数显然是5,所以这是我们要考虑的快速排序的分区函数的枢轴值。接下来你可以做的是将此索引与最后一个交换并获得
[9,3,1,7,5]
现在我们尝试将所有小于5的值放在中间的左边,所有大于5的值放在中间的右边。我们现在得到
[1,3,7,9,5]
将最后一个元素(我们存储分区值的地方)与中间交换
[1,3,5,9,7]
这就是使用3中间的想法。想象一下,如果我们的分区是1或9。你可以想象我们得到的这个数组不是快速排序的好例子。