【问题标题】:Sorting an array while moving duplicates to the end?在将重复项移动到末尾时对数组进行排序?
【发布时间】:2011-08-30 15:12:57
【问题描述】:

这是我朋友的一个编程课上的一个问题。

问。如何对ints 的数组进行排序,然后将它们排列成所有重复的元素都出现在数组的末尾?

例如,给定输入

{5, 2, 7, 6, 1, 1, 5, 6, 2}

输出将是

{1, 2, 5, 6, 7, 1, 2, 5, 6}

注意,数字是排序的,重复的数字在7之后,这是数组中的最大值。

这必须在不使用任何 Java 库包/工具的情况下实现

我建议先使用插入或冒泡排序对数组进行排序,然后遍历数组,执行如下操作:

for (int i = 0; i < nums.length - 2; i++) {
    for (int j = i + 1; j < nums.length; j++) {
        //current and next are same, move elements up
        //and place the next number at the end.
        if (nums[i] == nums[j]) {
            int temp = nums[j];
            for (int k = j; k < nums.length - 1; k++) {
                nums[k] = nums[k + 1];
            }
            nums[nums.length - 1] = temp;
            break;
        }
    }
}

我后来自己尝试了这个(上面的代码就是这样) - 当我尝试这个时,我认为这可以通过使用更少的代码来实现,效率更高。可能是我给出了错误的建议。

有什么想法吗?

【问题讨论】:

  • @Nick Johnson:记住,他正在学习编程。
  • 那么我的问题在于教他的人 - 将冒泡排序作为“实际上不能很好地工作的幼稚算法”的示例之外的任何内容都是不负责任的。 :)

标签: java algorithm sorting duplicates


【解决方案1】:

根据问题的参数,有很多方法可以解决这个问题。

如果不允许使用 O(n) 外部存储器,那么一种选择是使用标准排序算法在 O(n log n) 时间内对数组进行就地排序,然后在它上面运行第二遍以将重复项移动到最后(如您所建议的那样)。您在上面发布的代码需要 O(n2) 时间,但我认为这一步可以使用稍微复杂的算法在 O(n log n) 时间内完成。这个想法分两步起作用。在第一步中,在 O(n log n) 时间内,您将所有非重复元素按排序顺序放在前面,并将所有重复元素按非排序顺序放在后面。完成后,您可以使用第一步中的排序算法在 O(n log n) 时间内对数组的后半部分进行排序。

我不打算进入对数组进行排序的代码。我真的很喜欢排序,但是关于如何对数组进行就地排序还有很多其他好的资源,所以我在这里没有很好地利用我的时间/空间来进入它们。如果有帮助,这里是 heapsortquicksortsmoothsort 的 Java 实现的链接,所有这些都在 O(n log n) 时间内运行。堆排序和平滑排序仅使用 O(1) 外部内存,而快速排序在最坏的情况下可以使用 O(n)(尽管好的实现可以使用可爱的技巧将其限制为 O(log n))。

有趣的代码是将所有非重复元素带到范围前面的逻辑。直观地说,代码通过存储两个指针来工作——一个读指针和一个写指针。读指针指向下一个要读取的元素,而写指针指向应该放置下一个唯一元素的位置。例如,给定这个数组:

1 1 1 1 2 2 3 4 5 5

我们从最初指向 1 的读写指针开始:

write  v
       1 1 1 1 2 2 3 4 5 5
read   ^

接下来,我们将读取指针向前跳过到下一个不是 1 的元素。这会找到 2:

write  v
       1 1 1 1 2 2 3 4 5 5
read           ^

然后,我们将写指针撞到下一个位置:

write    v
       1 1 1 1 2 2 3 4 5 5
read           ^

现在,我们将 2 交换到写指针所持有的位置:

write    v
       1 2 1 1 1 2 3 4 5 5
read           ^

将读取指针前进到下一个不是 2 的值:

write    v
       1 2 1 1 1 2 3 4 5 5
read               ^

然后推进写指针:

write      v
       1 2 1 1 1 2 3 4 5 5
read               ^

再次,我们交换'read'和'write'指向的值,并将写指针向前移动,然后将读指针移动到下一个唯一值:

write        v
       1 2 3 1 1 2 1 4 5 5
read                 ^

再次收获

write          v
       1 2 3 4 1 2 1 1 5 5
read                   ^

最后的迭代给出了

write            v
       1 2 3 4 5 2 1 1 1 5
read                      ^

如果我们现在从写指针到读指针排序,我们得到

write            v
       1 2 3 4 5 1 1 1 2 5
read                      ^

还有宾果游戏!我们已经得到了我们正在寻找的答案。

在(未经测试,抱歉...)Java 代码中,此修复步骤可能如下所示:

int read = 0;
int write = 0;

while (read < array.length) {
     /* Swap the values pointed at by read and write. */
     int temp = array[write];
     array[write] = array[read];
     array[read] = temp;

     /* Advance the read pointer forward to the next unique value.  Since we
      * moved the unique value to the write location, we compare values
      * against array[write] instead of array[read].
      */
     while (read < array.length && array[write] == array[read])
         ++ read;

     /* Advance the write pointer. */
     ++ write;
}

该算法在 O(n) 时间内运行,这导致该问题的整体算法为 O(n log n)。由于重新排序步骤使用 O(1) 内存,因此总体内存使用量将是 O(1)(对于平滑排序或堆排序等)或 O(log n)(对于快速排序等)。

编辑:在与朋友讨论后,我认为基于快速排序的修改有一个更优雅的问题解决方案。通常,当您运行快速排序时,您最终会将数组划分为三个区域:

 +----------------+----------------+----------------+
 | values < pivot | values = pivot | values > pivot |
 +----------------+----------------+----------------+

然后递归对第一个和最后一个区域进行排序,以将它们排序。但是,我们可以针对我们的问题版本进行修改。我们需要 rotation 算法作为原始算法,它在一个数组中获取两个相邻的值块,并在 O(n) 时间内交换它们。它不会改变这些块中元素的相对顺序。例如,我们可以使用旋转来转换数组

1 2 3 4 5 6 7 8

进入

3 4 5 6 7 8 1 2

并且可以在 O(n) 时间内完成。

快速排序的修改版本将通过使用 Bentley-McIlroy 三路分区算法(描述为here)使用 O(1) 额外空间将数组元素重新排列为上述配置。接下来,我们应用旋转来重新排序元素,使它们看起来像这样:

 +----------------+----------------+----------------+
 | values < pivot | values > pivot | values = pivot |
 +----------------+----------------+----------------+

接下来,我们执行交换,以便将枢轴元素的一个副本准确地移动到至少与枢轴一样大的元素集合中。这可能在后面有额外的枢轴副本。然后我们递归地将排序算法应用于 范围。当我们这样做时,结果数组将如下所示:

 +---------+-------------+---------+-------------+---------+
 | < pivot | dup < pivot | > pivot | dup > pivot | = pivot |
 +---------+-------------+---------+-------------+---------+

然后我们对范围应用两次旋转以将其放入最终顺序。首先,使用大于枢轴的值旋转小于枢轴的重复值。这给了

 +---------+---------+-------------+-------------+---------+
 | < pivot | > pivot | dup < pivot | dup > pivot | = pivot |
 +---------+---------+-------------+-------------+---------+

此时,第一个范围是升序排列的唯一元素:

 +---------------------+-------------+-------------+---------+
 | sorted unique elems | dup < pivot | dup > pivot | = pivot |
 +---------------------+-------------+-------------+---------+

最后,对大于枢轴的重复元素和等于枢轴的元素进行最后一次旋转,得到这个:

 +---------------------+-------------+---------+-------------+
 | sorted unique elems | dup < pivot | = pivot | dup > pivot |
 +---------------------+-------------+---------+-------------+

请注意,最后三个块只是排序后的重复值:

 +---------------------+-------------------------------------+
 | sorted unique elems |      sorted duplicate elements      |
 +---------------------+-------------------------------------+

瞧!我们已经按照我们想要的顺序得到了一切。使用与普通快速排序相同的分析,再加上我们在每个级别只做 O(n) 的工作(三个额外的旋转),这在最好的情况下可以达到 O(n log n) O(log n) 内存使用量。在 O(log n) 内存的最坏情况下仍然是 O(n2),但这种情况发生的概率极低。

如果允许您使用 O(n) 内存,一种选择是从所有存储键/值对的元素中构建一个平衡的二叉搜索树,其中每个键是数组的一个元素,值是它出现的次数。然后,您可以按以下格式对数组进行排序:

  1. 对于数组中的每个元素:
    • 如果该元素已存在于 BST 中,则增加其计数。
    • 否则,向 BST 添加一个新节点,该节点的计数为 1。
  2. 执行 BST 的有序遍历。遇到一个节点,输出它的key。
  3. 对 BST 进行第二次有序遍历。当遇到一个节点时,如果它的计数大于1,则输出该节点的n-1个副本,其中n是它出现的次数。

这个算法的运行时间是 O(n log n),但是从头开始编写 BST 会非常棘手。它还需要外部空间,我不确定你是否可以这样做。

但是,如果您被允许使用外部空间并且您正在排序的数组很小并且包含小整数,您可以使用修改后的counting sort 来修改上述方法。只需将 BST 替换为一个足够大的数组,以使原始数组中的每个整数都成为键。这将运行时间减少到 O(n + k),内存使用 O(k),其中 k 是数组中的最大元素。

希望这会有所帮助!

【讨论】:

    【解决方案2】:

    修改后的合并排序可以解决问题:在最后一次合并过程中,跟踪您在结果数组前面推送的最后一个数字,如果下一个数字中的最低值相等,则添加到末尾而不是前面

    【讨论】:

    • 我不确定我知道这会如何工作。这不会破坏归并排序中必要的递归假设,即两个子数组返回排序吗?
    • 我在 LAST 合并过程中说过,在递归调用中,当原始调用者获得 2 个已排序的子数组时,它会在末尾而不是在前面添加重复的子数组,这是一个正常的合并排序
    【解决方案3】:

    欢迎来到数据结构和算法的世界。您绝对正确,因为您可以更快地进行排序。你也可以用十几种不同的方式来做。 PHD 都花在这些东西上了 :)

    这是一个链接,您可以在其中看到优化的bubble sort

    您可能还想查看Big O Notation

    玩得开心,祝你好运!

    【讨论】:

      【解决方案4】:

      使用快速排序对数组进行排序。在实现排序时,您可以通过将所有重复项添加到单独的重复数组中来稍微修改它。完成后,只需将重复的数组附加到排序数组的末尾即可。

      【讨论】:

        猜你喜欢
        • 2021-10-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-06-09
        • 2015-04-21
        • 2016-10-10
        相关资源
        最近更新 更多