如何在 C 或 C++ 中以 O(n) 删除数组中的重复元素?

Posted

技术标签:

【中文标题】如何在 C 或 C++ 中以 O(n) 删除数组中的重复元素?【英文标题】:How does one remove duplicate elements in place in an array in O(n) in C or C++? 【发布时间】:2010-08-08 01:41:10 【问题描述】:

是否有任何方法可以在 O(n) 中的 C/C++ 中删除数组中的重复元素? 假设元素是a[5]=1,2,2,3,4 那么结果数组应该包含1,2,3,4 该解决方案可以使用两个 for 循环来实现,但我相信这将是 O(n^2)。

【问题讨论】:

如果数组必须是排序的,你可以很容易地逃脱。 您正在使用 C,或者您正在使用 C++。选择一个。 @Greg Hewgill:如果 OP 刚毕业,很可能两者看起来都一样。 @William Pursell:除非是这种情况,否则他不知道如何选择两者之一。 @pranay:我的意思是使用我发布的文章中引用的循环,删除模板内容,并将“ForwardIterator”更改为某个迭代器或指针类型,然后就完成了。 【参考方案1】:

当且仅当源数组已排序,这可以在线性时间内完成:

std::unique(a, a + 5); //Returns a pointer to the new logical end of a.

否则您必须先排序,即(99.999% 的时间)n lg n

【讨论】:

不是真的。看我的回答。 排序不一定是 n lg n。根据数据,可能有 O(n) 种可用排序(例如计数排序、桶排序)。 @Borealid:请参阅我对您的回答的评论。 @Jamesdin:是的,但是 A. 大多数此类排序在实践中表现不佳,B. 我假设使用 std::sort,这是一个比较排序。 就地意味着唯一有效的排序是堆排序。其他一切都是O(n^2) 或非常量空间。大多数人都有这两个问题。 @R. std::sort 已就位且 n lg n。它通常是 Introsort 的一种形式。【参考方案2】:

最好的情况是O(n log n)。对原始数组执行堆排序:O(n log n) in time,O(1)/in-place in space。然后使用 2 个索引(源和目标)依次遍历数组以折叠重复。这具有不保留原始顺序的副作用,但是由于“删除重复项”没有指定要删除哪些重复项(第一个?第二个?最后一个?),我希望您不在乎订单丢失.

如果您确实想保留原始订单,则无法就地执行操作。但是,如果您创建一个指向原始数组中元素的指针数组,对指针进行所有工作,并在最后使用它们折叠原始数组,这将是微不足道的。

任何声称它可以在O(n) 时间和就地完成的人都是错误的,以一些关于O(n) 和就地意味着什么的争论为模。如果您的元素是 32 位整数,一个明显的伪解决方案是使用初始化为全零的 4 千兆位数组(大小为 512 兆字节),当您看到该数字时翻转一点并跳过它如果该位已经开始了。当然,您正在利用n 受常数限制的事实,所以从技术上讲,一切都是O(1),但具有可怕的常数因子。但是,我确实提到了这种方法,因为如果 n 以一个小常数为界 - 例如,如果您有 16 位整数 - 这是一个非常实用的解决方案。

【讨论】:

Unicode 代码点(20.1 位)是位数组解决方案非常实用的另一种情况 - 例如,如果您想获取文本中使用了哪些字符的列表。在这种情况下,位数组甚至可能是最终输出的理想形式。 _ 任何声称可以在 O(n) 时间内就地完成的人都是错误的,取模一些关于 O(n) 和就地意味着什么的论点_如果你有证据,CS期刊正在等待您的精彩见解。 我没有证据,就像没有人有证据证明无法有效地分解大量数字一样。我想扭转您的挑战:如果这里有人有O(n) 时间和就地解决方案,CS 期刊正在等待他们的精彩见解。同时,我会假设他们错了。 澄清:证明不可能,尽管其难度令人印象深刻,但不会彻底改变任何事情;它只会确认每个人已经凭直觉相信的东西。另一方面,一个有效的解决方案(我提到的任何一个问题)将彻底改变计算 考虑因素,是的,在 O(n) 时间内用 O(1) 额外空间删除单位成本 RAM 上的重复项,不。【参考方案3】:

是的。因为对哈希表的访问(插入或查找)是 O(1),您可以在 O(N) 中删除重复项。

伪代码:

hashtable h = 
numdups = 0
for (i = 0; i < input.length; i++) 
    if (!h.contains(input[i])) 
        input[i-numdups] = input[i]
        h.add(input[i])
     else 
        numdups = numdups + 1
    

这是 O(N)。

一些评论者指出,哈希表是否为 O(1) 取决于许多因素。但在现实世界中,通过良好的散列,您可以期待恒定时间的性能。并且可以设计一个 O(1) 的哈希来满足理论家的要求。

【讨论】:

“假设非冲突哈希”是一个非常大胆的声明。 哈希表查找/插入的 O(1) 复杂度不取决于非冲突哈希函数。 (根据生日悖论,碰撞会很快发生:对于大小为 m 的表,当插入大约 log(m) 个元素时——很有可能。)O(1) 复杂度基于组合事物:一个相当好的(接近统一,没有冲突)散列函数和以小(最终摊销)成本动态调整表大小的能力。 @Borealid:您的算法是最坏情况的二次算法。我的是最坏的情况 n lg n。声称您的算法始终是线性时间是完全不正确的,尽管在 average 情况下,如果具有良好的哈希值,它将是线性的。但在算法设计中,我们通常讨论的是最坏情况,而不是最佳情况下的性能。 -1 因为问题明确指定了in place。这个算法不是。 -1 表示错误答案。哈希表不是 O(1) 并且与就地不相似。【参考方案4】:

我将建议对 Borealids 答案的一种变体,但我会提前指出这是作弊。基本上,它只适用于假设对数组中的值有一些严格的限制 - 例如所有键都是 32 位整数。

我们的想法是使用位向量,而不是哈希表。这是一个 O(1) 内存要求,理论上应该让 Rahul 高兴(但不会)。对于 32 位整数,位向量将需要 512MB(即 2**32 位) - 假设是 8 位字节,正如一些学究可能指出的那样。

正如 Borealid 应该指出的,这 一个哈希表 - 只是使用了一个微不足道的哈希函数。这确实保证不会发生任何冲突。可能发生冲突的唯一方法是在输入数组中使用相同的值两次 - 但由于重点是忽略第二次和以后的出现,这无关紧要。

为了完整性的伪代码...

src = dest = input.begin ();
while (src != input.end ())

  if (!bitvector [*src])
  
    bitvector [*src] = true;
    *dest = *src; dest++;
  
  src++;

//  at this point, dest gives the new end of the array

真的很傻(但理论上是正确的),我还要指出,即使数组包含 64 位整数,空间要求仍然是 O(1)。我同意,常数项有点大,您可能会遇到 64 位 CPU 的问题,它们实际上不能使用完整的 64 位地址,但是...

【讨论】:

R.. 7 小时前描述。 ***.com/questions/3432760/… 是的——我确实读过那篇文章,虽然不是(我记得的)全部。我为人类的错误辩护。【参考方案5】:

以你为例。如果数组元素是有界整数,则可以创建查找位数组。

如果您找到一个整数,例如 3,请打开第 3 位。 如果你找到一个整数,比如 5,把第 5 位打开。

如果数组包含元素而不是整数,或者元素没有界限,那么使用哈希表将是一个不错的选择,因为哈希表查找成本是一个常数。

【讨论】:

哈希表查找成本是O(n) 而不是O(1),除非您对数据有限制以确保碰撞次数的限制。预期的性能是恒定时间的,但 big-O 意味着最坏的情况定义 @R:Big-O 表示法并不指最坏的情况。 ...最坏情况或平均情况算法的运行时间或内存使用量通常使用大 O 表示法表示为其输入长度的函数...参见 wiki en.wikipedia.org/wiki/Big_O_notation。 @R:在实践中,某些数据结构在最坏情况下的 O(N),但它的摊销平均成本为 O(1),仍然被认为是好的。一个例子是向量:当它增长时,最坏的成本是 O(N),但平均来说是 O(1)。我的意思是表明 Big-O 表示法是对最坏情况或平均情况的测量,而不仅仅是最坏情况。 您混淆了术语。如果您正在考虑摊销运行时,您仍然有最坏情况、平均情况和最佳情况。 Big-Oh 根据定义处理最坏情况下的行为,无论是否摊销。 Big-O 给出了函数值的上限。该函数是否代表最坏情况或平均情况下的运行时间(或其他)不是 Big-O 定义的一部分。【参考方案6】:

unique() 算法的规范实现类似于以下内容:

template<typename Fwd>
Fwd unique(Fwd first, Fwd last)

    if( first == last ) return first;
    Fwd result = first;
    while( ++first != last ) 
        if( !(*result == *first) )
            *(++result) = *first;
    
    return ++result;

此算法采用一系列已排序的元素。如果范围未排序,请在调用算法之前对其进行排序。该算法将在原地运行,并返回一个迭代器,该迭代器指向唯一序列的最后一个元素。

如果你不能对元素进行排序,那么你已经走投无路了,你别无选择,只能使用运行时性能低于 O(n) 的算法来完成任务。

此算法在 O(n) 运行时间中运行。这是所有情况下最坏的情况,而不是摊销时间。它使用 O(1) 空间。

【讨论】:

【参考方案7】:

您给出的示例是一个排序数组。只有在这种情况下才有可能(鉴于您的恒定空间限制)

【讨论】:

什么常量空间约束? “就地”是否意味着空间限制? 我认为就地暗示 O(1) 空间。否则,您可以将副本作为临时存储,在副本中完成工作,将其存储在原件之上,并将其称为“就地”。那是假的。 不一定。首先,O(log n) 空间怎么样。其次,即使您仍然需要 O(n) 或更多额外空间,就地更新仍然是一种优化。例如,您可以创建然后对数据的引用数组进行排序,然后使用它在 O(n) 时间内对数据进行就地排序。初始排序仍然是 O(n log n),但如果引用的交换比数据项的交换快得多,这可能是有益的。空间限制并不是就地进行更新的唯一原因。

以上是关于如何在 C 或 C++ 中以 O(n) 删除数组中的重复元素?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C++ 中使用数组?

如何在 C++ 中使用数组?

如何在 C++ 中使用数组?

C++ 数组转数字

如何有效地将元素插入数组的任意位置?

如何删除重复的矩阵(以数组表示)?