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();
### 回答1: C++ STL是C++标准库的一部分,包含了许多常用的数据结构和算法,如vector、list、map、set等。STL的设计目的是提供高效、可靠、通用的数据结构和算法,使得程序员可以更加方便地编写高质量的代码。 map和unordered_map是STL中的两种关联容器,它们都可以用于存储键值对。map是一种有序的关联容器,它使用红黑树实现,可以快速地查找、插入和删除元素,但是它的空间复杂度较高。unordered_map是一种无序的关联容器,它使用哈希表实现,可以在常数时间内查找、插入和删除元素,但是它的空间复杂度较低。 在使用map和unordered_map时,需要注意它们的特点和适用场景。如果需要有序地存储键值对,并且需要快速地查找、插入和删除元素,那么该选择map;如果不需要有序地存储键值对,并且需要在常数时间内查找、插入和删除元素,那么该选择unordered_map。 ### 回答2: C++ STL(标准模板库)是C++标准库的一部分,提供了一组标准的库函数和容器类,能够帮助C++开发者提高代码复用性,减少代码编写时间。其中,map和unordered_map是STL库中常用的关联容器,用于存储键值对。 map是有序的关联容器,它存储键值对,并且根据键排序,使用红黑树实现,插入/查找/删除操作的时间复杂度均为log(N)。map可以被用于实现数据结构,例如有序映射和堆积。map对于需要手动排序的问题十分实用,因为它会自动维护键值的顺序。 unordered_map是哈希表实现的关联容器,它同样存储键值对,但是不会对键进行排序,插入/查找/删除的平均时间复杂度为常数级别,实际上操作速度更快。一般来说,unordered_map速度更快,但是由于哈希表的不确定性,其效率可能会受到键分布的影响。 两个容器都可以通过类似于数组的方式访问元素,但是map访问方式是map[key],而unordered_map的访问方式为unordered_map[key]。对于元素的添加,map需要使用insert函数,而unordered_map使用emplace或insert函数均可。 在使用STL中的容器时,选择合适的数据结构非常重要。如果需要存储有序的数据并且希望通过键来快速访问数据,可以选择map。但是,如果只关心快速访问和查找数据,可以选择unordered_map。需要注意的是,在许多情况下,具体的使用场景会影响选择的容器类型。 ### 回答3: C++ STL(Standard Template Library)是C++标准库的一部分,包含了许多模板类、函数和算法,大大提高了C++程序的开发效率。其中,map和unordered_map是两个重要的容器类。 map是一个关联容器,它将键值对映射到一个有序的序列中,其中键是唯一的。map的底层实现是红黑树,因此它具有快速的查找和插入操作。map是按照键的自然顺序进行排序的,默认按照小于号(<)进行比较。如果需要按照其他方式进行排序,可以自定义比较函数。 unordered_map也是一个关联容器,它将键值对映射到一个无序的序列中,其中键是唯一的。unordered_map的底层实现是哈希表,因此它具有快速的查找和插入操作。unordered_map的元素是无序的,因此不能像map那样直接遍历。如果需要按照键的自然顺序进行遍历,可以将unordered_map中的元素复制到一个vector中,然后对vector进行排序。 map和unordered_map都支持以下操作: 1. 插入元素:使用插入函数(insert)或者下标运算符[]来插入元素。 2. 删除元素:使用删除函数(erase)来删除指定位置或者指定键的元素。 3. 查找元素:使用find函数来查找指定键的元素,如果找到了就返回对元素的迭代器,否则返回尾迭代器。 4. 遍历元素:可以使用迭代器(iterator)来遍历容器中的所有元素。 在使用map和unordered_map时,需要注意以下问题: 1. 如果键是自定义类型,需要重载==运算符和小于号(<)运算符,以便能够正确比较元素。 2. 在使用unordered_map时,需要定义一个哈希函数,以便将元素散列到桶中。 3. 在插入元素时,如果键已经存在,会直接覆盖原有元素。 4. 在删除元素时,需要注意迭代器过期的问题,删除元素后,迭代器可能会失效,不能再使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值