Quicksort - 具有重复值的 Hoare 分区

Quicksort - Hoare's partitioning with duplicate values

我已经为 Quicksort 实现了经典的 Hoare 分区算法。它适用于任何唯一数字列表 [3、5、231、43]。唯一的问题是当我有一个包含重复项 [1, 57, 1, 34] 的列表时。如果我得到重复值,我将进入无限循环。

private void quicksort(int[]a, int lo, int hi) {
    if (lo < hi) {
        int q = hoare_partition(a, lo, hi);
        quicksort(a, lo, q - 1);
        quicksort(a, q + 1, hi);
    }
}

private int hoare_partition(int[] a, int lo, int hi) {

    int pivot = a[hi];
    int i = lo;
    int j = hi;

    while (true) {

        while (a[i] < pivot) i++;
        while (a[j] > pivot) j--;

        if (i < j) swap(a, i, j);
        else return j;
    }
}

有没有可能是 Hoare 的实现不正确,无法处理重复项?

我知道这个问题已经被问过很多次了(我都试过了)但是我很难找到这个实现的解决方案。

algorithm partition(A, lo, hi) is
    pivot := A[lo]
    i := lo – 1
    j := hi + 1
    loop forever
        do
            i := i + 1
        while A[i] < pivot

        do
            j := j – 1
        while A[j] > pivot

        if i >= j then
            return j

        swap A[i] with A[j]

上面的伪代码取自Wikipedia。让我们将它与您的代码进行比较。

问题是您必须在交换后移动索引。伪代码使用 do-while 循环而不是 while 循环在交换后移动索引。还要注意 ij.

的初始值
algorithm quicksort(A, lo, hi) is
    if lo < hi then
        p := partition(A, lo, hi)
        quicksort(A, lo, p)
        quicksort(A, p + 1, hi)

对于递归步骤,您可能需要处理边缘情况(即索引)。如果您将 quicksort(a, lo, q-1) 更改为 quicksort(a, lo, q).

,它应该可以工作

我刚刚写的一个完整的工作版本:

import java.util.Arrays;

public class Test {

    public static void main(String[] args) throws Exception {
        int[] input = {1, 57, 1, 34};
        quicksort(input, 0, input.length - 1);
        System.out.println(Arrays.toString(input));
    }

    private static void quicksort(int[]a, int lo, int hi) {
        if (lo < hi) {
            int q = hoare_partition(a, lo, hi);
            quicksort(a, lo, q);
            quicksort(a, q + 1, hi);
        }
    }

    private static int hoare_partition(int[] a, int lo, int hi) {

        int pivot = a[lo];
        int i = lo - 1;
        int j = hi + 1;

        while (true) {
            do {
                i++;
            }
            while (a[i] < pivot);

            do {
                j--;
            }
            while (a[j] > pivot);

            if (i >= j) {
                return j;
            }
            swap(a, i, j);

        }
    }

    private static void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

}

如果您更喜欢 while 循环而不是 do-while:

private static int hoare_partition(int[] a, int lo, int hi) {

            int pivot = a[lo];
            int i = lo;
            int j = hi;

            while (true) {

                while (a[i] < pivot) i++;

                while (a[j] > pivot) j--;

                if (i >= j) {
                    return j;
                }
                swap(a, i, j);
                i++;
                j--;

            }
        }

这里是一个 C++ 示例,它实现了 Hoare 方案加上中位数 3 检查,一个 pivot 检查的副本以排除等于 pivot 的中间值(注意等于 pivot 的值可以在任何地方结束,而不仅仅是中间,所以这没有多大帮助),仅在较小的部分使用递归,对较大的部分进行循环(这可以防止堆栈溢出)。最坏情况下的时间复杂度仍然是 O(n^2),但它需要相当具体的数据模式来产生这个(3 的中位数必须始终保持接近最小值或接近最大值)。

void QuickSort(uint32_t a[], size_t lo, size_t hi) {
    while(lo < hi){
        size_t i = lo, j = (lo+hi)/2, k = hi;
        uint32_t p;
        if (a[k] < a[i])            // median of 3
            std::swap(a[k], a[i]);
        if (a[j] < a[i])
            std::swap(a[j], a[i]);
        if (a[k] < a[j])
            std::swap(a[k], a[j]);
        p = a[j];
        i--;                        // Hoare partition
        k++;
        while (1) {
            while (a[++i] < p);
            while (a[--k] > p);
            if (i >= k)
                break;
            std::swap(a[i], a[k]);
        }
        i = k++;
        while(i > lo && a[i] == p)  // exclude middle values == pivot
            i--;
        while(k < hi && a[k] == p)
            k++;
        // recurse on smaller part, loop on larger part
        if((i - lo) <= (hi - k)){
            QuickSort(a, lo, i);
            lo = k;
        } else {
            QuickSort(a, k, hi);
            hi = i;
        }
    }
}

这是 while 循环的另一个例子

private static int partition(int[] a, int start, int end){
   int i = start-1, j = end+1;

   while(true){
      while(a[++i] < a[start]);
      while(a[start] < a[--j]);

      if (i >= j) return j;
      swap(a, i, j);
   }
}

上面@Terry Li 的回答的地址,因为我的声誉不足以发表评论。首先非常感谢您的详细post。但是,我认为您提供的 while 循环的替代函数存在一些问题。它不是 return 原始枢轴的索引,它本身是不准确的。 (请用 hoare_partition([6,7,8,9,1,2,3,4,5], 0, 8) 试试)问题是由于 i 递增和 j 同时递减,导致你失去了对枢轴的追踪。因此,在下面提议的修复中,我插入了一个小条件以确保存储数据透视表的索引不会被更改。 如有错误请指正

private static int hoare_partition(int[] a, int lo, int hi) {

            int pivot = a[lo];
            int i = lo;
            int j = hi;

            while (true) {

                while (a[i] < pivot) i++;

                while (a[j] > pivot) j--;

                if (i >= j) {
                    return i;
                }
                swap(a, i, j);
                if (a[j] == pivot)
                    i++;
                elif (a[i] == pivot)
                    j--;

            }
        }

Is it possible that Hoare's implementation is incorrect and is unable to cope with duplicates?

没错,当枢轴在数组中出现多次时,Hoare 算法就会失效。

一种解决方法是使用 a three-way partitioning algorithm。它没有返回单个索引,而是 returns 两个索引:数据透视部分开始的位置和结束的位置。

我知道这个 post 已经过时了,但我相信我已经找到了解决方案。当 hoare_partition 立即发现 i = lo 处的 a[i] 需要换出它的位置时,问题就出现了,但它从来没有找到合适的 a[j] 来交换。我们知道如果 q == lo 就会发生这种情况。当枢轴是数组中的最小值并且是重复的时,就会出现此问题。为了解决这个问题,我们在快速排序中检查 q == lo,如果是,我们手动交换 a[lo] 和 a[pivot_index] 以确保 a[lo] 被移出它的位置并进入一个有效的位置。我们只需要进行一次递归调用,因为左侧分区的大小为 1。

我还对内部 while 循环中的条件进行了轻微修改。除了外循环之外,您还需要检查内循环中的 i < j 是否。第二个内部 while 现在检查 a[j] 是否 >= pivot,而不是严格 >。最后一次递归调用使用 q 而不是 q+1 作为新的 lo。我把 pivot_index 作为参数。最后,我返回了 j 而不是 i。

private void quicksort(int[] a, int lo, int hi) {
  if (lo < hi) {
    // int pivot_index = (rand() % (hi - lo + 1)) + lo; // randomized pivot_index is better. should be in range [lo, hi] inclusive 
    int pivot_index = hi; 
    int q = hoare_partition(a, lo, hi, pivot_index);

    if (q == lo) {
      swap(a, lo, pivot_index);
      quicksort(a, lo + 1, hi);

    } else {
      quicksort(a, lo, q - 1);
      quicksort(a, q, hi);

     }
  }
}

private int hoare_partition(int[] a, int lo, int hi, int pivot_index) {
  int pivot = a[pivot_index];
  int i = lo;
  int j = hi;

  while (true) {
    
    while (i < j && a[i] < pivot) i++;
    while (i < j && a[j] >= pivot) j--;
    
    if (i < j) swap(a, i, j);
    else return j;
  }
}