C++程序员应了解的那些事(83)c++ set / unordered_set

目录

【set 与 unordered_set区别: 导入】 

【unordered_set使用举例 + hash模板函数特化】

【unordered_set 只提供单向迭代器 + insert/find/erase用法(平均圈复杂度1)】

【STL里面的五种迭代器】

【unordered_set原型及基本操作】


【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();
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值