为什么std::set比std::map慢?
How is std::set slower than std::map?
我试图从acm.timus.ru解决这个问题,它基本上希望我输出给定字符串(最大长度5000)的不同子字符串的数量。
我即将提出的解决方案是非常低效的,并且在限制条件下注定要超过时间限制。然而,这两个解决方案的唯一不同之处在于(至少在我看来/理解的范围内),一个使用std::map<long long, bool>
,而另一个使用std::set <long long>
(参见最后一个for循环的开头)。其余部分是相同的(您可以使用任何不同的工具进行检查)。地图解决方案的结果是"测试3超过时间限制",而集合解决方案的结果是"测试2超过时间限制",这意味着在测试2上,地图解决方案比集合解决方案运行得更快。如果我选择Microsoft Visual Studio 2010编译器,情况就是如此。如果我选择GCC,那么两种解决方案都会在测试3上导致TLE。
std::map
明显比使用std::set
更有效。我只是没有看到这种现象的机制,希望有人能提出一些见解。
Code1 (uses map, TLE 3):
#include <iostream>
#include <map>
#include <string>
#include <vector>
using namespace std;
int main ()
{
string s;
cin >> s;
vector <long long> p;
p.push_back(1);
for (int i = 1; i < s.size(); i++)
p.push_back(31 * p[i - 1]);
vector <long long> hash_temp;
hash_temp.push_back((s[0] - 'a' + 1) * p[0]);
for (int i = 1; i < s.size(); i++)
hash_temp.push_back((s[i] - 'a' + 1) * p[i] + hash_temp[i - 1]);
int n = s.size();
int answer = 0;
for (int i = 1; i <= n; i++)
{
map <long long, bool> hash_ans;
for (int j = 0; j < n - i + 1; j++)
{
if (j == 0)
hash_ans[hash_temp[j + i - 1] * p[n - j - 1]] = true;
else
hash_ans[(hash_temp[j + i - 1] - hash_temp[j - 1]) * p[n - j - 1]] = true;
}
answer += hash_ans.size();
}
cout << answer;
}
Code2 (using set, TLE 2):
#include <iostream>
#include <string>
#include <vector>
#include <set>
using namespace std;
int main ()
{
string s;
cin >> s;
vector <long long> p;
p.push_back(1);
for (int i = 1; i < s.size(); i++)
p.push_back(31 * p[i - 1]);
vector <long long> hash_temp;
hash_temp.push_back((s[0] - 'a' + 1) * p[0]);
for (int i = 1; i < s.size(); i++)
hash_temp.push_back((s[i] - 'a' + 1) * p[i] + hash_temp[i - 1]);
int n = s.size();
int answer = 0;
for (int i = 1; i <= n; i++)
{
set <long long> hash_ans;
for (int j = 0; j < n - i + 1; j++)
{
if (j == 0)
hash_ans.insert(hash_temp[j + i - 1] * p[n - j - 1]);
else
hash_ans.insert((hash_temp[j + i - 1] - hash_temp[j - 1]) * p[n - j - 1]);
}
answer += hash_ans.size();
}
cout << answer;
}
我看到的实际差异(如果我错过了什么请告诉我)是,在地图的情况下,你做
hash_ans[key] = true;
而在set的情况下,你做
hash_ans.insert(key);
在这两种情况下,都插入一个元素,除非该元素已经存在,且在其中不做任何操作。在这两种情况下,查找都需要找到相应的元素,并在失败时插入它。实际上,在每种实现中,容器都将使用树,这使得查找同样昂贵。更重要的是,c++标准实际上要求set::insert()
和map::operator[]()
的复杂度为O(log n),因此两种实现的复杂度应该相同。
那么,一个人表现更好的原因是什么呢?一个区别是,在一种情况下,底层树的节点包含string
,而在另一种情况下,它包含pair<string const, bool>
。由于这对包含一个字符串,它一定更大,对机器的RAM接口施加更大的压力,所以这并不能解释加速。它所能做的就是扩大节点大小,这样其他节点就会被挤出缓存线,这对多核系统的性能不利。
总而言之,我想尝试以下几点:
在集合
中使用相同的数据我会这样做与struct data: string {bool b};
即捆绑字符串在一个结构体,应该有一个类似的二进制布局作为地图的元素。作为比较器,使用less<string>
,这样只有字符串实际参与比较。在地图上使用insert()
我不认为这应该是一个问题,但是插入可能会导致参数的副本,即使最后没有插入发生。我希望它不会,所以我不太相信这会改变任何事情。关闭调试
大多数实现都有诊断模式,在该模式下对迭代器进行验证。您可以使用它来捕获c++只说"未定义行为"的错误,耸耸肩并崩溃。读取代码
如果set和map的实现具有不同的质量和优化水平,这可以解释差异。在引擎盖下,我希望地图和设置都建立在同一类型的树上,所以这里也不太希望。
在这种情况下,set只比map快一点点。我还是不认为你们应该把ttle 2或ttle 3说得那么严重。如果你很接近时间限制,就可能发生同样的解决方案的时间限制在一个给定的提交上,测试2的时间限制,下一次测试3的时间限制。我有一些解决方案在时间限制内通过了测试,我敢打赌,如果我重新提交它们,它们会失败的。
这个问题我用Ukonen后缀树解决了
这取决于使用的实现算法。通常集合是使用映射实现的,只使用key字段。在这种情况下,与使用map相比,使用set会有很小的开销。
- 如何导出包含具有"std::unique_ptr"值的"std::map"属性的
- 使用一个考虑到std::map中键值的滚动或换行的键
- 有没有办法对std::unordered_set、std::unrdered_map、std::set、std::map
- 将重物插入std::map
- 使用通用值初始化 std::map,不重复
- 仅包含可移动 std::map 的类的移动构造函数不起作用
- C++:当所有条目都保证是唯一时,替代 std::map
- 使用模板化的键类型定义 std::map,该键类型基于作为参数接收的函数
- 如果 KEY 是 std::list 或 std::vector 而不是值,那么 std::map 的默认行为是什么?
- C++如何创建 std::map
- 从其他容器中移动构造"std::map"
- 将 std::map::emplace 与返回 shared_ptr 的函数一起使用是否正确?
- C++中 std::map 的运行时复杂度是多少?
- 为什么在 std::map 上移动无法将元素从一个映射移动到另一个映射
- 使用重载 [] 运算符返回 std::map() 的可赋值
- std::map, std::unordered_map - 缩小初始值设定项列表中的转换范围
- C++ 使用枚举类对象分配 std::map 值
- 静态 std::map instatiation 在类的方法中调用构造函数吗?
- std::map:当元素不可默认构造时创建/替换元素
- Arduino编译器和STL:使用std::vector和std::map