问题描述
我正在尝试解决编程问题 firstDuplicate on codesignal。问题是“给定一个数组 a
,它只包含 1
到 a.length
范围内的数字,找到第一个重复的数字,第二次出现的索引最少”。
示例:对于 a = [2,1,3,5,2]
,输出应为 firstDuplicate(a) = 3
有 2 个重复:数字 2 和 3。第二次出现的 3 比第二次出现的 2 的索引小,所以答案是 3。
使用此代码,我通过了 21/23 次测试,但随后它告诉我该程序超出了测试 22 的执行时间限制。我将如何使其更快以通过剩余的两个测试?
#include <algorithm>
int firstDuplicate(vector<int> a) {
vector<int> seen;
for (size_t i = 0; i < a.size(); ++i){
if (std::find(seen.begin(),seen.end(),a[i]) != seen.end()){
return a[i];
}else{
seen.push_back(a[i]);
}
}
if (seen == a){
return -1;
}
}
解决方法
每当您被问到有关“查找重复项”、“查找丢失的元素”或“查找应该在那里的内容”的问题时,您的第一直觉应该是使用哈希表 .在 C++ 中,有 unordered_map
和 unordered_set
类用于此类编码练习。 unordered_set
实际上是 bool 键的映射。
另外,通过引用传递向量,而不是值。按值传递会产生复制整个向量的开销。
此外,这种比较最终似乎代价高昂且没有必要。
这可能更接近您想要的:
#include <unordered_set>
int firstDuplicate(const vector<int>& a) {
std::unordered_set<int> seen;
for (int i : a) {
auto result_pair = seen.insert(i);
bool duplicate = (result_pair.second == false);
if (duplicate) {
return (i);
}
}
return -1;
}
,
std::find
是容器中第一个和最后一个元素(或直到找到数字)之间距离的线性时间复杂度,因此最坏情况复杂度为 O(N),因此您的算法将是 O(N^2)。
不是将您的数字存储在向量中并每次都搜索它,Yyu 应该使用 std::map
进行散列以存储遇到的数字并返回一个数字,如果在迭代时它已经存在于地图中.
std::map<int,int> hash;
for(const auto &i: a) {
if(hash[i])
return i;
else
hash[i] = 1;
}
编辑:如果键的顺序无关紧要,std::unordered_map
的效率甚至更高,因为与 std::map
的对数插入复杂度相比,插入时间复杂度在平均情况下是恒定的。
这可能是一种不必要的优化,但我想我会尝试更好地利用规范。哈希表主要用于从可能的键到实际键的转换相当稀疏的情况——也就是说,只使用了一小部分可能的键。例如,如果您的键是长度不超过 20 个字符的字符串,则理论上的最大键数为 25620。有这么多可能的键,很明显没有实际的程序会存储超过一个极小的百分比,因此哈希表是有意义的。
然而,在这种情况下,我们被告知输入是:“一个只包含 1 到 a.length 范围内的数字的数组 a”。因此,即使有一半数字是重复的,我们也会使用 50% 的可能键。
在这种情况下,我会使用 std::vector<bool>
代替哈希表,尽管它经常受到诽谤,并且希望在绝大多数情况下获得更好的性能。
int firstDuplicate(std::vector<int> const &input) {
std::vector<bool> seen(input.size()+1);
for (auto i : input) {
if (seen[i])
return i;
seen[i] = true;
}
return -1;
}
这里的优势相当简单:至少在典型情况下,std::vector<bool>
使用特化将 bool
存储在每个位中。通过这种方式,我们只为每个输入数存储一位,这增加了存储密度,因此我们可以期待出色地使用缓存。特别是,只要缓存中的字节数至少比输入数组中元素数的 1/8th 多一点,我们就可以期待所有的 seen
大部分时间都在缓存中。
现在不要搞错了:如果您环顾四周,您会发现很多文章指出 vector<bool>
存在问题——在某些情况下,这是完全正确的。有些地方和时间应该避免 vector<bool>
。但是它的任何限制都适用于我们在这里使用它的方式——而且它确实在存储密度方面提供了一个非常有用的优势,尤其是对于这种情况。
我们还可以编写一些自定义代码来实现位图,从而提供比 vector<bool>
更快的代码。但是使用 vector<bool>
很容易,而且编写我们自己的更高效的替代品需要相当多的额外工作...