【问题标题】:Algorithm:Sort using Median and Just 1 comparison in $ O(n \log n) $算法:在 $ O(n \log n) $ 中使用 Median 和 Just 1 比较进行排序
【发布时间】:2013-09-29 15:56:52
【问题描述】:

我正在尝试编写一个排序算法,它接受一个输入数组并生成排序后的数组。以下是算法的约束条件。

  1. 时间复杂度 = $O(n \log n)$
  2. 整个算法只有一个比较操作。
  3. 我们有一个函数可以为一组三个元素提供中值。

我已尝试找到解决方案,以下是我的结果。

  1. 我们使用中值函数得到最小和最大的对。 例子:给一个数组 A[1..n],我们得到前三个元素的中位数,我们称它为一个集合,我们得到一个 $Median$。在下一次迭代中,我们从集合中删除接收到的中值并将下一个元素添加到集合中,然后再次调用 Median 函数。当在长度上重复此步骤时,会为整个数组生成一对最大和最小元素。

  2. 我们使用这对,使用比较操作并将它们放在位置 A[0] & A[n-1]。

  3. 我们对数组 A[1..n-2] 重复同样的操作,得到另一对最大和最小的。

  4. 我们取 A[0] 和新收到的对的中位数。

  5. 中值位于 A[1]。
  6. 我们取 A[n-1] 和新收到的对的中位数。
  7. 中值放置在 A[n-2] 处。

重复第3~7步,得到一个排序好的数组。

该算法满足条件 2 & 3 但不满足时间复杂度。我希望有人可以提供一些指导如何进一步进行。

【问题讨论】:

  • 整个算法只有一个比较操作(你能解释一下吗)?如果您有中位数操作(需要比较)以及实际的排序功能(也需要比较),这似乎是不可能的。整体听起来像是quicksort 或mergesort 的实现。另外:3.We repeat the same operation over the array A[1..n-2]。这似乎是一个奇怪的子集——如果你想要O(n ln n) 时间,为什么只截断外部值?你不想把数组减半吗?
  • 很抱歉给您带来了困惑。除了由中值函数完成的比较之外,还有一个比较。在当前问题的上下文中,我们将 Median 函数视为一个单元,不考虑它完成的时间或操作,即 MedianFunction 需要 \theta(1)。
  • 像quicksort 这样的sooouunnddss,虽然这通常是通过两次比较来完成的(我相信mergesort 也是如此)。由于它是n-lg(n),因此每次递归都必须将数组的一侧减半,但是为了知道何时停止,您至少需要一个比较(表明数组中只有一个元素)。然后,您需要在实际的排序循环本身中进行另一个比较。那里至少有两个……
  • 副手,我想我可以写 quicksort 大约 5 次左右的比较(没有一些疯狂的创造力)......你可以用更少的几个来做 mergesort,但是这样做one 似乎是不可能的。您可能想要求澄清。您可能可以通过一个比较来进行insertion/bubble/selection 样式排序,但是,这显然不是nlg(n) 时间
  • 我不知道快速排序,但在合并排序的情况下,我们在每次递归时都有不止 1 个比较。因此具有最少的 \log n 比较。这违反了约束,即在算法的整个执行过程中只有一个比较。

标签: arrays algorithm sorting


【解决方案1】:

快速排序(以相反的顺序显示)的工作原理是这样的。假设你有一个数组:

 [1, 4, 5, 2, 3]

摘要中的快速排序基本上是通过从左右两侧向数组中间滑动来工作的。当我们向内滑动时,我们想要交换项目,使得大的东西移到右边,小东西移到左边。最终我们应该有一个数组,其中所有的小东西都在左边,所有的大东西都在右边。

这个过程的结果也保证了一个元素将被放置在正确的位置(因为它左边的所有东西都会更小,而右边的所有东西都会更大,所以它必须在正确的位置)。该值称为pivot。快速排序的第一步是确保枢轴在正确的位置。

我们这样做的方法是选择一个随机元素作为我们的pivot - 我们想要放入正确位置的项目。对于这个简单的示例,我们将只使用最后一个数字(3)。 pivot 是我们的比较值。

一旦我们选择了 pivot/comparison 值,我们就会监控最左边的元素 (1) 和最右边的元素 (3)。我们将它们称为left-pointer 和right-pointer。 left-pointers 的工作是滑向数组的中间,当它发现大于pivot 的东西时停止。 right 指针做同样的事情,但它向内滑动寻找值 less 比pivot。在代码中:

while (true) {
    while (array[++left] < pivot);
    while (array[--right] > pivot) if (left == right) break;

    if (left >= right) break;           // If the pointers meet then exit the loop
    swap(array[left], array[right]);    // Swap the values otherwise.
}

所以在我们上面的例子中,当left-pointer 命中(4) 时,它会识别出高于我们的枢轴元素并停止移动。右枢轴从右侧执行相同的操作,但在到达(2) 时停止,因为它低于pivot。当双方都停止时,我们进行交换,所以我们最终得到:

[1, 2, 5, 4, 3]

请注意,我们越来越接近排序了。我们继续向内移动两个指针,直到它们都指向同一个元素,或者它们交叉——以先到者为准。发生这种情况时,我们要做最后一步,即用left/right-pointers 指向的任何点替换枢轴元素(3),在这种情况下为(5),因为它们都会停在中间。然后我们交换,这样我们得到:

[1, 2, 3, 4, 5] 
(Notice that we swap the original pivot (3) with the value pointed to by both sides (5))

整个过程称为分区。在代码中它看起来像这样:

int partition(int *array, int lBound, int rBound) {
    int pivot = array[rBound];          // Use the last element as a pivot
    int left = lBound - 1;              // Point to one before the first element
    int right = rBound;             // Point to the last element;

    // We'll break out of the loop explicity
    while (true) {

        while (array[++left] < pivot);
        while (array[--right] > pivot) if (left == right) break;

        if (left >= right) break;    // If the pointers meet then exit the loop
        swap(array[left], array[right]);    // Swap the pointers otherwise.
    }

    swap(array[left], array[rBound]);   // Move the pivot item into its correct place
    return left;    // The left element is now in the right place
}

请务必注意,尽管在本例中分区步骤对我们的数组进行了完全排序,这通常不是分区步骤的重点。分区步骤的重点是将 one 元素放入正确的位置,并确保该元素左侧的所有内容都是 less 而右侧的所有内容都是 更多。或者换句话说,将pivot 值移动到正确的位置,然后保证枢轴左侧的所有内容都小于它,而右侧的所有内容都更大。所以虽然在这个例子中数组是完全排序的,但一般我们只能保证 one 项和 one 项在正确的位置(以及左边和右边分别更大/更小)。这就是上面的partition 方法返回left 的原因,因为它告诉调用函数这个one 元素位于正确的位置(并且数组已被正确分区)。

如果我们以这样的数组开始:

[1, 7, 5, 4, 2, 9, 3]

那么分区步骤将返回如下内容:

[1, 3, 2, [4], 7, 5, 9]

其中 [4] 是唯一保证在正确位置的值,但左侧的所有值都小于 [4] 而右侧的所有值更大(虽然 不是 必须排序!)。

第二步是递归执行这一步。也就是说,如果我们可以将一个元素放入正确的位置,那么我们最终应该能够将所有项目放入正确的位置。那就是quicksort 函数。在代码中:

int *quicksort(int *array, int lBound, int rBound) {
    if (lBound >= rBound) return array;   // If the array is size 1 or less - return.

    int pivot = partition(array, lBound, rBound);   // Split the array in two. 
    quicksort(array, lBound, pivot - 1);    // Sort the left size. (Recursive)
    quicksort(array, pivot + 1, rBound);    // Sort the right side. (Recursive) 

    return array;
}

请注意,第一步是确保我们有一个至少为 2 的数组边。处理任何小于这个值的东西是没有意义的,所以如果不满足这个条件,我们就返回。下一步是调用我们的分区函数,它将根据上述过程拆分数组。一旦我们知道数组中有一个元素在正确的位置,我们只需再次调用快速排序,但这次是在枢轴的左侧,然后再次在枢轴的右侧。 请注意,我们不包括枢轴,因为分区保证将其放入正确的位置!

如果我们继续递归调用quicksort,最终我们会将数组减半并对其进行分区,直到我们得到大小为一的数组(根据定义,它已经排序)。所以我们分区,然后减半,分区,减半等,直到整个数组排序(到位)。这给了我们在O(n lg(n)) 时间的排序。酷!

这是一个简单的使用示例:

int main() {
    int array [] {1, 0, 2, 9, 3, 8, 4, 7, 5, 6};

    quicksort(array, 0, 9); // Sort from zero to 9.

    // Display the results
    for (int index = 0; index != 10; ++index) {
        cout << array[index] << endl;
    }

    return 0;
}

可以在这里找到一个很好的视觉演示:http://www.youtube.com/watch?v=Z5nSXTnD1I4

【讨论】:

  • 不,我明白了,我们之前讨论过这个问题。但是 OP 不知道快速排序是什么,所以我写了一个。见上面的对话。
【解决方案2】:

步骤 1 和 2 确实是正确解决方案的第一步。然而,一旦你知道了最小和最大的元素,中值预言就是一个比较预言;如果您想比较a[i] 和a[j],那么a[i] &lt; a[j] 恰好在a[i] = median(a[i], a[j], a[0]) 时a[0] 是最小元素。所以你可以直接运行快速排序或合并排序或你有什么。

【讨论】:

  • 在我知道最大和最小元素的值后,您能否详细说明一下可以使用快速排序或合并排序。
  • @Mattlee:它们是基于比较的排序。我只是告诉你如何进行比较。你可以从那里拿走。
猜你喜欢
  • 2011-06-25
  • 1970-01-01
  • 1970-01-01
  • 2021-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多