根据问题的参数,有很多方法可以解决这个问题。
如果不允许使用 O(n) 外部存储器,那么一种选择是使用标准排序算法在 O(n log n) 时间内对数组进行就地排序,然后在它上面运行第二遍以将重复项移动到最后(如您所建议的那样)。您在上面发布的代码需要 O(n2) 时间,但我认为这一步可以使用稍微复杂的算法在 O(n log n) 时间内完成。这个想法分两步起作用。在第一步中,在 O(n log n) 时间内,您将所有非重复元素按排序顺序放在前面,并将所有重复元素按非排序顺序放在后面。完成后,您可以使用第一步中的排序算法在 O(n log n) 时间内对数组的后半部分进行排序。
我不打算进入对数组进行排序的代码。我真的很喜欢排序,但是关于如何对数组进行就地排序还有很多其他好的资源,所以我在这里没有很好地利用我的时间/空间来进入它们。如果有帮助,这里是 heapsort、quicksort 和 smoothsort 的 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) 内存,一种选择是从所有存储键/值对的元素中构建一个平衡的二叉搜索树,其中每个键是数组的一个元素,值是它出现的次数。然后,您可以按以下格式对数组进行排序:
- 对于数组中的每个元素:
- 如果该元素已存在于 BST 中,则增加其计数。
- 否则,向 BST 添加一个新节点,该节点的计数为 1。
- 执行 BST 的有序遍历。遇到一个节点,输出它的key。
- 对 BST 进行第二次有序遍历。当遇到一个节点时,如果它的计数大于1,则输出该节点的n-1个副本,其中n是它出现的次数。
这个算法的运行时间是 O(n log n),但是从头开始编写 BST 会非常棘手。它还需要外部空间,我不确定你是否可以这样做。
但是,如果您被允许使用外部空间并且您正在排序的数组很小并且包含小整数,您可以使用修改后的counting sort 来修改上述方法。只需将 BST 替换为一个足够大的数组,以使原始数组中的每个整数都成为键。这将运行时间减少到 O(n + k),内存使用 O(k),其中 k 是数组中的最大元素。
希望这会有所帮助!