使用 O(m) 空间在 O(n) 时间内对向量<int>(n) 进行排序?

Posted

技术标签:

【中文标题】使用 O(m) 空间在 O(n) 时间内对向量<int>(n) 进行排序?【英文标题】:Sort vector<int>(n) in O(n) time using O(m) space? 【发布时间】:2013-10-30 03:04:40 【问题描述】:

我有一个大小为nvector&lt;unsigned int&gt; vecvec 中的每个元素都在[0, m] 范围内,没有重复,我要对vec 进行排序。如果允许您使用 O(m) 空间,是否有可能比 O(n log n) 时间做得更好?在平均情况下mn 大得多,在最坏情况下m == n

理想情况下,我想要 O(n) 的东西。

我觉得有一种桶排序方式可以做到这一点:

    unsigned int aux[m]; aux[vec[i]] = i; 以某种方式提取排列并置换vec

我不知道怎么做 3。

在我的应用程序中,m 大约为 16k。然而,这种类型在内部循环中,占我运行时的很大一部分。

【问题讨论】:

这个问题可能会更好:programmers.stackexchange.com m比n大多少? mn 大多少,比n log n 大多少?因为如果是这样的话,O(n log n) 仍然会比 linear O(m)@DavidRodríguez-dribeas O(m) 空间,而不是时间。 O(m) 时间是一个简单的桶排序。 @Adam:如果你真的是指O(n) 时间,那就做不到了。要查找O(m) 空间中存在哪些元素,您需要的不仅仅是O(n) 【参考方案1】:

基数排序,也不需要值小于 m 的限制。

下面的示例实现是在 int 类型上模板化的。如果您的 m 始终小于 2^15,则应尽可能使用 int16_t 向量(如果值始终为正,则应使用更好的 uint16_t 以避免处理有符号整数的偏移量)。对于 32 位整数,这将只需要两次排序,而不是 4 次。如果你不能改变你的输入类型,你可以特殊情况下代码只执行两次并避免签名偏移。

这个实现是 O(n) 并且使用了 O(n) 额外空间(排序不到位)。

template<typename I>
void radix_sort(I first, I last) 
    using namespace std;
    typedef remove_reference_t<decltype(*first)> int_t;
    typedef make_unsigned<int_t>::type uint_t;

    const uint_t signedOffset = is_signed<int_t>::value ? uint_t(numeric_limits<int_t>::max()) + uint_t(1) : 0;
    auto getDigit = [=](uint_t n, int power) -> size_t  return ((n + signedOffset) >> (power * 8)) & 0xff; ;

    array<size_t, 256> counts;
    vector<int_t> sorted(distance(first, last));
    for (int power = 0; power < sizeof(int_t); ++power) 
        counts.fill(0);
        for_each(first, last, [&](int_t i)  ++counts[getDigit(i, power)]; );
        partial_sum(begin(counts), end(counts), begin(counts));
        for_each(reverse_iterator<I>(last), reverse_iterator<I>(first), [&](int_t i) 
            sorted[--counts[getDigit(i, power)]] = i;
        );
        copy(begin(sorted), end(sorted), first);
    

【讨论】:

标准库中有基数排序吗? 不,但是实现起来相对简单,而且您最初的要求实际上是 m 是基数的特殊情况。 最大值为m的基数排序的运行时间为O(n log m)。如果 m 真的比 n 大得多,这实际上并不比 n log n 快,而且可能更慢。 对于 32 位整数和基数 256,log m 为 4。 确实如此,尽管这似乎很奇怪,因为它根本不依赖 m。【参考方案2】:

如果您知道 m = O(n2) 的事实,则可以执行 base-n 基数排序来对数组进行排序。这类似于普通的基数排序,但不是有 2 个桶或 10 个桶,而是有 n 个桶,每个可能的基数为 n 位的数字一个。

由于基数b中基数排序的运行时间是O(n logb U),其中U是最大值,在这种情况下我们知道运行时间是O(n logn n2) = O(n)。这比 O(n log n) 渐近地快。它也只需要 O(n) 内存,低于 O(m) 的限制。

希望这会有所帮助!

【讨论】:

n 是可变的。它通常很小,但它是可变的,在最坏的情况下可能高达 m。然而,在几乎所有情况下,它都小了 2-3 个数量级。所以基本上你是说平均有 2-3 次基数排序迭代?【参考方案3】:

不,你需要这样做:

unsigned int aux[m + 1];
bzero(aux, sizeof(aux));

foreach x in (vec)  aux[x]++; 

for(x = 0; x <= m; x++)
  for(qty = 0; qty < x; qty++)
    output(x);

【讨论】:

不过,这是 O(m) 时间。 我将第一行写为unsigned int aux[m + 1] = 0 并放弃bzero 调用。

以上是关于使用 O(m) 空间在 O(n) 时间内对向量<int>(n) 进行排序?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 O(n) 时间内对双向链表进行二进制搜索?

ac自动机时间复杂度是多少?

在 O(1) 空间和 O(n) 时间中查找 2 个字符串是不是是字谜

如何在时间复杂度为大O(N)的循环内对数组部分求和

[leetcode 88. 合并两个有序数组] 尾部归并O空间,最好O(n)最坏O(m+n),双100%

归并排序