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

Posted

技术标签:

【中文标题】如何在快速排序中选择枢轴值?【英文标题】:How to choose pivot value in QuickSort? 【发布时间】:2016-12-16 10:26:36 【问题描述】:

我已经研究了几个小时的快速排序,并且对选择枢轴值感到困惑。枢轴值是否需要存在于数组中?

例如,如果数组是 1,2,5,6 ,我们可以使用值 3 或 4 作为枢轴吗?

我们使用枢轴的位置将数组划分为子数组,但我有点困惑,当我们将值 5 移动到右侧之后,枢轴位置会是什么?

7,1,5,3,3,5,8,9,2,1

我用枢轴 5 对算法进行了空运行,结果如下:

1,1,5,3,3,5,8,9,2,7
1,1,5,3,3,5,8,9,2,7
1,1,5,3,3,5,8,2,9,7

我们可以看到值 2 仍然不在正确的位置。我究竟做错了什么?对不起,如果这是一个愚蠢的问题。

我想出了以下代码,但它仅在枢轴 = 左时才有效,我不能使用随机枢轴。

template <class T>
void quickSort(vector <T> & arr, int p, int r, bool piv_flag) 
    if (p < r) 
        int q, piv(p); counter++;

        //piv = ((p + r) / 2); doesn't work

        q = partition(arr, p, r, piv);
        quickSort(arr, p, q - 1, piv_flag); //Sort left half
        quickSort(arr, q + 1, r, piv_flag); //Sort right half
    

    return;


int partition(vector <T> & arr, int left, int right, int piv) 
    int i left - 0 , j right + 0 , pivot arr[piv] ;

    while (i < j) 
        while (arr[i] <= pivot)  i++; 
        while (arr[j] > pivot)  j--; 
        if (i < j) (swap(arr[i], arr[j]));
        else 
            swap(arr[j], arr[piv]);
            return j;
        
     

谢谢。

【问题讨论】:

【参考方案1】:

在许多应用程序中,枢轴被选为数组中的某个元素,但它也可以是您可以用来将数组中的数字一分为二的任何值。如果您选择的枢轴值是数组中的特定元素,则需要在将数组分成两部分后将其放置在这两个组之间。如果没有,您可以通过正确调用索引来继续递归排序过程。 (即记住,数组中没有枢轴元素,只有两组值)

请参阅this response 对类似问题的简要说明,了解一些广泛使用的选择枢轴的替代方法。

pivot 的最重要功能是作为我们在快速排序的分区阶段尝试创建的组之间的边界。这里的目标/挑战是以这样一种方式创建这些组,使它们的大小相等或几乎相等,以便快速排序可以有效地工作。这一挑战是构思如此多的枢轴选择方法的原因。 (即,至少在大多数情况下,数字将被分成大小相似的组)

关于分区完成后枢轴位置将如何变化的问题的第二部分,请参阅下面的示例分区阶段。

假设我们有一个包含元素 [4,1,5,3,3,5,8,9,2,1] 的数组 A,我们选择枢轴作为第一个元素,即 4。下面使用的字母 E表示小于枢轴的元素的结尾。 (即最后一个小于枢轴的元素)

   E
[4,1,5,3,3,5,8,9,2,1]

     E
[4,1,3,5,3,5,8,9,2,1]

       E
[4,1,3,3,5,5,8,9,2,1]

         E
[4,1,3,3,2,5,8,9,5,1]

           E
[4,1,3,3,2,1,8,9,5,5]

[1,1,3,3,2,4,8,9,5,5] // swap pivot with the rightmost element that is smaller than its value

显然,在这个分区之后,元素仍然没有排序。但是 4 左侧的所有元素都小于 4,而其右侧的所有元素都大于 4。为了对它们进行排序,我们对这些组递归地使用 Quicksort。

根据您的代码,下面是基于我上面描述的过程的示例分区代码。你也可以观察它的执行here。

template <class T>
int partition(vector<T>& arr, int left, int right, int piv) 
    int leftmostSmallerThanPivot = left;

    if(piv != left)
        swap(arr[piv], arr[left]);
    for(int i=left+1; i <= right; ++i) 
        if(arr[i] < arr[left])
            swap(arr[++leftmostSmallerThanPivot], arr[i]);
    
    swap(arr[left], arr[leftmostSmallerThanPivot]);
    return leftmostSmallerThanPivot;


template <class T>
void quickSort(vector<T>& arr, int p, int r) 
    if (p < r) 
        int q, piv(p);

        piv = ((p + r) / 2); // works

        q = partition(arr, p, r, piv);
        quickSort(arr, p, q - 1); //Sort left half
        quickSort(arr, q + 1, r); //Sort right half
    

【讨论】:

感谢您的回复,我实际上无法理解如何处理重复。我正在使用以下算法进行分区,但它仅在枢轴位置=左时才有效。这基本上意味着我不能使用完全随机的枢轴位置。 int partition(vector &lt;T&gt; &amp; arr, int left, int right, int piv) int i left - 0 , j right + 0 , pivot arr[piv] ; while (i &lt; j) while (arr[i] &lt;= pivot) i++; while (arr[j] &gt; pivot) j--; if (i &lt; j) (swap(arr[i], arr[j])); else swap(arr[j], arr[piv]); return j; ; 感谢您的详细回答。您在这里所做的是将枢轴向左移动,然后运行快速排序算法,但是我们可以简单地选择最左边的元素作为枢轴位置,选择随机枢轴位置没有任何优势。 其实优势还在。您不是选择最左边的元素,而是选择您想要的任何元素,因此您可以利用选择任何元素的优势来实现类似大小的分组。与最左边元素的交换只是在单个平滑循环中遍历向量,从索引 left+1 到索引右。 我更正了添加到回复中的示例代码中的几个错误,并编辑了链接到回复的 Ideone 代码。代码的当前状态适用于您可能提供给 piv 的任何索引,没有任何错误。 重申一下,是的,我的实现将最左边的元素与您选择的枢轴进行初始交换。但这并不影响您对支点的选择。 (即您指定的索引处的值,而不是最左边索引处的值,仍被视为枢轴)事实上,您可能会观察到它与您提到的不工作的 piv=(p+r) / 2 一起使用.

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

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

您如何在中间位置实现枢轴快速排序,这种变化称为啥? [关闭]

快速排序

快速排序

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

快速排序 - 中间枢轴实现奇怪的行为