c++中set和unordered_set的区别是什么?
what is the difference between set and unordered_set in C++?
我遇到了这个很好的问题,这是类似的,但不完全相同,因为它谈论的是Java,它有不同的哈希表实现,由于有同步的访问/mutator:Java中的HashMap和Hashtable有什么区别?
那么set
和unordered_set
的c++实现有什么不同呢?这个问题当然可以扩展到map
和unordered_map
,等等其他c++容器。
这是我的初步评估:
set
:虽然标准没有明确要求将其实现为树,但时间复杂度约束要求其查找/插入操作,这意味着它将始终实现为树。通常是RB树(如GCC 4.8中所见),它是高度平衡的。因为它们是高度平衡的,所以它们对find()
具有可预测的时间复杂度
优点:紧凑(与其他DS相比)
Con: Access time complexity is O(lgn)
unordered_set
:虽然标准没有明确要求将其实现为树,但时间复杂度约束要求其查找/插入操作,这意味着它将始终作为哈希表实现。
优点:
- 更快(承诺为搜索平摊O(1))
- 与tree-DS相比,容易将基本原语转换为线程安全的
缺点:
- 查找不保证是O(1)。理论上最坏的情况是0 (n)。
- 不像树那样紧凑(为了实际目的,负载因子从来不是1)。
注意:哈希表的0(1)来自于没有碰撞的假设。即使负载因子为0.5,每秒钟的变量插入都会导致碰撞。可以观察到,哈希表的负载因子与访问其中一个元素所需的操作次数成反比。我们越减少#操作,哈希表越稀疏。如果存储的元素的大小与指针相当,则开销相当大。
我是否错过了性能分析中map/set之间应该知道的任何区别?
我想你基本上已经回答了你自己的问题,然而,
不像树那样紧凑。(对于实际目的,负载系数从不为1)
不一定为真。类型T
的树的每个节点(我们假设它是一棵红黑树)使用的空间至少等于2 * pointer_size + sizeof(T) + sizeof(bool)
。这可能是3 * pointer size
,取决于树是否包含每个树节点的parent
指针。
将此与散列映射进行比较:由于您所说的load factor < 1
,每个散列映射都会浪费数组空间。但是,假设哈希映射使用单链表进行链接(实际上,没有理由不这样做),则插入的每个元素只占用sizeof(T) + pointer size
。
请注意,此分析忽略了任何可能来自对齐所使用的额外空间的开销。
对于任何具有较小大小的元素T
(因此,任何基本类型),指针的大小和其他开销占主导地位。例如,当负载因子为> 0.5
时,std::unordered_set
可能确实比等效的std::set
使用更少的内存。
另一个重要的缺失点是,根据给定的比较函数,迭代std::set
保证产生从最小到最大的顺序,而迭代std::unordered_set
将以"随机"顺序返回值。
另一个区别(虽然与性能无关)是set
插入不会使迭代器无效,而unordered_set
插入如果触发重新散列则可以。在实践中,这是一个非常小的问题,因为对实际元素的引用仍然有效。
玉石已经很好地解决了空间效率和其他问题;我将评论这个问题的其他几个部分……
哈希表的0(1)来自于没有碰撞的假设。
那不是真的。O(1)的意思并不是说第一次查找尝试总是会成功,而是平均来说,需要的尝试次数是恒定的,而不是随着值的增加而增加。例如,使用unordered_set
或…_map
, max_load_factor
在构造时默认为1.0,如果负载因子接近这个值,并且有一个好的哈希函数,那么无论表中有多少值,哈希到任何一个bucket的元素的平均数量将在2左右。
即使负载因子为0.5,每秒钟的变量插入都会导致碰撞。
对,但它并不像你直观地想象的那么可怕:在1.0负载系数下,平均链长为2并不坏。
可以观察到哈希表的负载因子是相反的与访问a所需的操作次数成正比元素。我们越少#操作,越稀疏哈希表
这绝对是相关的(不是反向的)。
在某些情况下set
更方便。
例如使用vector
作为key:
set<vector<int>> s;
s.insert({1, 2});
s.insert({1, 3});
s.insert({1, 2});
for(const auto& vec:s)
cout<<vec<<endl; // I have override << for vector
// 1 2
// 1 3
vector<int>
可以在set
中的原因是因为vector
覆盖了operator<
。
但是如果你使用unordered_set<vector<int>>
,你必须为vector<int>
创建一个哈希函数,因为vector没有哈希函数,所以你必须定义一个这样的:
struct VectorHash {
size_t operator()(const std::vector<int>& v) const {
std::hash<int> hasher;
size_t seed = 0;
for (int i : v) {
seed ^= hasher(i) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}
return seed;
}
};
vector<vector<int>> two(){
//unordered_set<vector<int>> s; // error vector<int> doesn't have hash function
unordered_set<vector<int>, VectorHash> s;
s.insert({1, 2});
s.insert({1, 3});
s.insert({1, 2});
for(const auto& vec:s)
cout<<vec<<endl;
// 1 2
// 1 3
}
你可以看到,在某些情况下,unordered_set
更复杂。
主要引自:https://stackoverflow.com/a/29855973/6329006
unordered_set
和set
的更多区别见:https://stackoverflow.com/a/52203931/6329006
- C++中std::resize(n)和std::shrink_to_fit之间的区别
- int(c) 和 c-'0' 之间的区别。C++
- 向量 <int> a {N, 0} 和 int arr a[N] = {0} 的时间复杂度有什么区别
- C++ - "!pointer"和"pointer == nullptr"的区别?
- C++ 使用 assign 函数的字符串与直接使用 '=' 更改值的字符串之间的区别
- 为什么我无法更改"set<set>"循环中的值<int>
- std::atomic和std::condition_variable wait,notify_*方法之间的区别
- 在 .h 文件中的类中声明静态变量和在.cpp文件中声明"global"变量有什么区别
- 对于set上的循环-获取next元素迭代器
- 我是C++编程的新手,这些代码之间有什么区别,我应该使用哪一个
- 在 const 函数中通过引用和指针返回之间的区别
- 我想知道长双倍和双倍之间的区别
- 返回常量对象引用 (getter) 和仅返回字符串有什么区别?
- 返回递归调用和仅递归调用的区别
- 在声明中合并两个常量"std::set"(不是在运行时)
- std::set 和 std::map 有什么区别
- std::lower_bound与std::set::lower_bound的区别
- std::lower_bound与std::set::lower_bound的区别
- 对std::inserter对std::set使用.begin()和.end()有区别吗?
- vector::iterator与set::iterator的主要区别