目录
【unordered_set使用举例 + hash模板函数特化】
【unordered_set 只提供单向迭代器 + insert/find/erase用法(平均圈复杂度1)】
【set 与 unordered_set区别: 导入】
c++ std中set与unordered_set区别和map与unordered_map区别类似:
<1>set基于红黑树实现,红黑树具有自动排序的功能,因此set内部所有的数据,在任何时候都是有序的。
<2>unordered_set基于哈希表,数据插入和查找的时间复杂度很低,几乎是常数时间,而代价是消耗比较多的内存,无自动排序功能。底层实现上,使用一个下标范围比较大的数组来存储元素,形成很多的桶,利用hash函数对key进行映射到不同区域进行保存。
更详细的区别,如下图:
示例:
set:
Input : 1, 8, 2, 5, 3, 9
Output : 1, 2, 3, 5, 8, 9
Unordered_set:
Input : 1, 8, 2, 5, 3, 9
Output : 9 3 1 8 2 5 (顺序依赖于 hash function)
【unordered_set使用举例 + hash模板函数特化】
下面在给出一个以vector<int>为key的示例,对比下set与unordered_set的使用。
set<vector<int>> 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
因为vector重载了 operator<,因此可以作为set的key。但是如果直接使用unordered_set<vector<int>> s;则报错,因为vector没有hash函数,需要自己定义一个,可以定义一个类似下面这样的hash函数:
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;
}
};
接下来这样使用:
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[0] << " " << vec[1] <<endl;
// 1 2
// 1 3
☆☆或者 模板特化struct hash<std::vector<int>> 如下:(个人更推荐这种用法!)
namespace std {
template<>
struct hash<std::vector<int>> {
size_t operator()(const 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;
}
};
}
// usage example
void test_unordered_set()
{
std::unordered_set<std::vector<int>> s;
s.insert({1, 2});
s.insert({1, 3});
s.insert({1, 2});
for(const auto& vec:s)
cout<<vec[0] << "," << vec[1] <<endl;
// 1 3
// 1 2
std::hash<int> hasher;
cout<<"hasher(99): "<<hasher(99)<<" ,hasher(77): "<<hasher(77)<<endl;
// hasher(99): 99 ,hasher(77): 77
}
int main()
{
test_unordered_set();
return 0;
}
输出:
1,3
1,2
hasher(99): 99 ,hasher(77): 77
【unordered_set 只提供单向迭代器 + insert/find/erase用法(平均圈复杂度1)】
unordered_set可以把它想象成一个集合,它提供了几个函数让我们可以增删查:
unordered_set::insert
unordered_set::find
unordered_set::erase
这个unorder暗示着其底层实现为----Hash,也正因为如此,你才可以在声明这些unordered模版类的时候,传入一个自定义的哈希函数,准确的说是哈希函数对象(hash function object)。
哈希表的实现复杂了该容器上的双向遍历,似乎没有一种合适的方法能够做到高效快速。 因此unorder版本的map和set只提供前向迭代器(非unorder版本提供双向迭代器)!!!
上面代码演示了insert/find/erase的用法。有两点需要注意:
①容器是个集合,所以重复插入相同的值是没有效果的。可以看到我们第7行和第9行插入了2次3,实际上这个集合里也只有1个3,第10行输出的结果是2。
②find的返回值是一个迭代器(iterator),如果找到了会返回指向目标元素的迭代器,没找到会返回end()。
对于unordered_set,insert/find/erase的平均复杂度是O(1),但是最坏复杂度是O(N)的,这里N是指容器中元素数量。有两种情况会出现O(N)复杂度:
<1>是你的哈希函数太烂了,导致很多不同元素的哈希值都相同,全是碰撞,这种情况复杂度会变成O(N)。但是这种情况一般不用担心,因为对于string以及int double之类的基本数据类型,都有默认的哈希函数,而且默认的哈希函数足够好,不会退化到O(N)。如果是你自定义的哈希函数,那你要小心一点,别写的太差了。
<2>是如果insert很多数据,会触发rehash,就是整个哈希表重建。这个过程有点类似向vector里不断添加元素,vector会resize。比如你新建一个vector时,它可能只申请了一块最多保存10个元素的内存,当你插入第11个元素的时候,它会自动重新申请一块更大空间,比如能存下20个元素。哈希表也是类似,不过rehash不会频繁发生,均摊复杂度还是O(1)的,也不用太担心。
unordered_set是一个集合,有的时候我们需要一个字典,就是保存一系列key/value对,并且可以按key来查询。比如我们要保存很多同学的成绩,每位同学有一个学号,也有一个分数,我们想按学号迅速查到成绩。这时候我们就可以用unordered_map。
unordered_map同样也提供了增删查函数:这三个函数的平均时间复杂度也是O(1)
unordered_map::insert
unordered_map::find
unordered_map::erase
【STL里面的五种迭代器】
根据STL中的分类,iterator包括:
输入迭代器(Input Iterator):通过对输入迭代器解除引用,它将引用对象,而对象可能位于集合中。最严格的输入迭代只能以只读方式访问对象。例如:istream。
输出迭代器(Output Iterator):该类迭代器和Input Iterator极其相似,也只能单步向前迭代元素,不同的是该类迭代器对元素只有写的权力。例如:ostream, inserter。
以上两种基本迭代器可进一步分为三类:
前向迭代器(Forward Iterator):该类迭代器可以在一个正确的区间中进行读写操作,它拥有Input Iterator的所有特性,和Output Iterator的部分特性,以及单步向前迭代元素的能力。
双向迭代器(Bidirectional Iterator):该类迭代器是在Forward Iterator的基础上提供了单步向后迭代元素的能力。例如:list, set, multiset, map, multimap。
随机迭代器(Random Access Iterator):该类迭代器能完成上面所有迭代器的工作,它自己独有的特性就是可以像指针那样进行算术计算,而不是仅仅只有单步向前或向后迭代。例如:vector, deque, string, array。
<1>Input Iterators
Input Iterator只能逐元素的向前遍历,而且对元素是只读的,只能读取元素一次。通常这种情况发生在从标准输入设备(通常是键盘)读取数据时:
下面是Input Iterator的可用操作列表:
*iter: 只读访问对应的元素
iter->member: 只读访问对应元素的成员
++iter: 向前遍历一步(返回最新的位置)
iter++: 向前遍历一步(返回原先的位置)
iter1 == iter2: 判断两个迭代器是否相等
iter1 != iter2:判断两个迭代器是否不等
<2> Output Iterators
Output iterator跟Input Iterator相对应,只能逐元素向前遍历,而且对元素是只写的(*iter操作不能作为右值,只能作为左值),只能写入元素一次。通常这种情况发生在向标准输出设备(屏幕或者打印机)写入数据时,或者利用inserter向容器中追加新元素时。
下面是Output Iterator的可用操作列表:
*iter = value: 向对应的元素写入新值
++iter: 向前遍历一步(返回最新的位置)
iter++: 向前遍历一步(返回原先的位置)
<3>Forward Iterators
Forward Iterator是Input Iterator和Output Iterator的结合,虽然也只能逐元素向前遍历,但可以对元素进行读写操作。下面看Forward Iterator的可用操作列表:
*iter:
iter->member:
++iter:
iter++:
iter1 == iter2:
iter1 != iter2:
iter1 = iter2:
跟Input Iterator和Output Iterator不同的是,Forward Iterator可以对同一元素访问多次:
※下面我们特别关注一下Forward Iterator和Output Iterator的区别:
(1)对于Output Iterator,写入数据时不检查目标容器是否到达结束位置是正确的做法,比如下面循环对于Output Iterator是成立的:
//ok for output iterator
//error for forward iterator
while(true)
{
*pos = foo();
++pos;
}
(2)对于Forward Iterator,则必须保证访问元素的有效性,那么上面形式对Forward Iterator来说是错误的,因为当碰到容器end()位置时,
导致不确定的后果。对于Forward Interator,上面形式必须修改为这样:
while(pos != col1.end())
{
*pos = foo();
++pos;
}
<4>Bidirectional Iterators
双向迭代器行为特征类似于Forward Iterator,只是额外增加了一个逐元素向后遍历的能力。所以对于双向迭代器可用的操作,除了包含Forward Iterator的所有操作外,多了一组向后遍历的操作:
--iter: 向后遍历一步(返回最新的位置)
iter--: 向后遍历一步(返回原有的位置)
<5>Random Access Iterators
随机访问迭代器除了有双向迭代器的能力特征外,还可以进行元素随机访问。所以对于随机访问迭代器,增加了关于“迭代器运算”的一些操作。下面是除了双向迭代器的所有操作外,额外的操作列表:
iter[n]: 直接访问索引为n的元素
iter+=n: 向前或向后(n为负数)遍历n个元素
iter-=n: 先后或向前(n为负数)遍历n个元素
iter+n: 返回当前位置后面第n个元素的iterator位置
n+iter: 同上
iter-n: 返回当前位置前面第n个元素的iterator位置
iter1-iter2: 返回iter1和iter2之间的距离(distance)
iter1<iter2: 判断iter1是否在iter2之前
iter1>iter2: 判断iter1是否在iter2之后
iter1<=iter2: 判断iter1是否不再iter2之后
iter1>=iter2: 判断iter1是否不再iter2之前
【unordered_set原型及基本操作】
unordered_set的实现基于hashtable,它的结构图可以用下图表示,这时的空白格不在是单个value,而是set中的key与value的数据包。
unordered_set是一种无序集合,既然跟底层实现基于hashtable那么它一定拥有快速的查找和删除,添加的优点。基于hashtable当然就失去了基于rb_tree的自动排序功能。unordered_set无序,所以在迭代器的使用上,set的效率会高于unordered_set。
//unordered_set原型:
template<class _Value,
class _Hash = hash<_Value>,
class _Pred = std::equal_to<_Value>,
class _Alloc = std::allocator<_Value> >
class unordered_set
: public __unordered_set<_Value, _Hash, _Pred, _Alloc>
{
typedef __unordered_set<_Value, _Hash, _Pred, _Alloc> _Base;
...
}
参数1 _Value key和value的数据包
参数2 _Hash hashfunc获取hashcode的函数
参数3 _Pred 判断key是否相等
参数4 分配器
unordered_set的一些基本操作如下:
<1>定义
unordered_set<int> c1;
//operator=
unordered_set<int> c2;
c2 = c1;
<2>容量操作
//判断是否为空
c1.empty();
//获取元素个数 size()
c1.size();
//获取最大存储量 max_size()
c1.max_size();
<3>迭代器操作
//返回头迭代器 begin()
unordered_set<int>::iterator ite_begin = c1.begin();
//返回尾迭代器 end()
unordered_set<int>::iterator ite_end = c1.end();
//返回const头迭代器 cbegin()
unordered_set<int>::const_iterator const_ite_begin = c1.cbegin();
//返回const尾迭代器 cend()
unordered_set<int>::const_iterator const_ite_end = c1.cend();
//槽迭代器
unordered_set<int>::local_iterator local_iter_begin = c1.begin(1);
unordered_set<int>::local_iterator local_iter_end = c1.end(1);
<4>基本操作
//查找函数 find() 通过给定主键查找元素
unordered_set<int>::iterator find_iter = c1.find(1);
//value出现的次数 count() 返回匹配给定主键的元素的个数
c1.count(1);
//返回元素在哪个区域equal_range() 返回值匹配给定搜索值的元素组成的范围
pair<unordered_set<int>::iterator, unordered_set<int>::iterator> pair_equal_range = c1.equal_range(1);
//插入函数 emplace()
c1.emplace(1);
//插入函数 emplace_hint() 使用迭代器
c1.emplace_hint(ite_begin, 1);
//插入函数 insert()
c1.insert(1);
//删除 erase()
c1.erase(1);//1.迭代器 value 区域
//清空 clear()
c1.clear();
//交换 swap()
c1.swap(c2);
<5>篮子操作
//篮子操作 篮子个数 bucket_count() 返回槽(Bucket)数
c1.bucket_count();
//篮子最大数量 max_bucket_count() 返回最大槽数
c1.max_bucket_count();
//篮子个数 bucket_size() 返回槽大小
c1.bucket_size(3);
//返回篮子 bucket() 返回元素所在槽的序号
c1.bucket(1);
//load_factor 返回载入因子,即一个元素槽(Bucket)的最大元素数
c1.load_factor();
//max_load_factor 返回或设置最大载入因子
c1.max_load_factor();