哈希集unordered_set - 用法

哈希集是集合的实现之一,它是一种存储不重复值的数据结构

我们提供了在 Java,C++ 和 Python 中使用哈希集的示例。 如果你不熟悉哈希集的用法,那么通过这一示例将会很有帮助。

#include<unordered_set>
using namespace std;
int main()
{
    unordered_set<int>hashset;
    hashset.insert(3);
    hashset.insert(2);
    hashset.insert(1);
    //delete a key
    hashset.erase(2);
    //check if the key is in the hash set
    if(hashset.count(2)<=0)
    {
        cout<<"2 is not in the hashset"<<endl;
    }
    //get the size of the hash set
    cout<<"the size of the hashset is "<<hashset.size()<<endl;
    //iterate the hashset
    for(auto it=hashset.begin();it!=hashset.end();it++)
    {
        cout<<(*it)<<endl;
    }
    //clear the hashset
    hashset.clear();
    //check if the hash set is empty
    if(hashset.empty())
        cout<<"hashset is empty now"<<endl;
}

 

unordered_set可以把它想象成一个集合,它提供了几个函数让我们可以增删查:
unordered_set::insert
unordered_set::find
unordered_set::erase

这个unorder暗示着,这两个头文件中类的底层实现----Hash。 也是因为如此,你才可以在声明这些unordered模版类的时候,传入一个自定义的哈希函数,准确的说是哈希函数子(hash function object)。

单向迭代器

哈希表的实现复杂了该容器上的双向遍历,似乎没有一种合适的方法能够做到高效快速。 因此,unorder版本的map和set只提供前向迭代器(非unorder版本提供双向迭代器)。

首先要include这个unordered_set头文件。
然后就是第六行我们定义了一个整型int的集合,叫myset。
后面几行,我们演示了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)的,也不用太担心

 

           C++ 11中出现了两种新的关联容器:unordered_set和unordered_map,其内部实现与set和map大有不同,set和map内部实现是基于RB-Tree,而unordered_set和unordered_map内部实现是基于哈希表(hashtable),由于unordered_set和unordered_map内部实现的公共接口大致相同,所以本文以unordered_set为例。
        unordered_set是基于哈希表,因此要了解unordered_set,就必须了解哈希表的机制。哈希表是根据关键码值而进行直接访问的数据结构,通过相应的哈希函数(也称散列函数)处理关键字得到相应的关键码值,关键码值对应着一个特定位置,用该位置来存取相应的信息,这样就能以较快的速度获取关键字的信息。比如:现有公司员工的个人信息(包括年龄),需要查询某个年龄的员工个数。由于人的年龄范围大约在[0,200],所以可以开一个200大小的数组,然后通过哈希函数得到key对应的key-value,这样就能完成统计某个年龄的员工个数。而在这个例子中,也存在这样一个问题,两个员工的年龄相同,但其他信息(如:名字、身份证)不同,通过前面说的哈希函数,会发现其都位于数组的相同位置,这里,就涉及到“冲突”。准确来说,冲突是不可避免的,而解决冲突的方法常见的有:开发地址法、再散列法、链地址法(也称拉链法)。而unordered_set内部解决冲突采用的是----链地址法,当用冲突发生时把具有同一关键码的数据组成一个链表。下图展示了链地址法的使用:

unordered_set

      模板原型:
[cpp]  view plain  copy
 
  1. template < class Key,  
  2.     class Hash = hash<Key>,  
  3.     class Pred = equal_to<Key>,  
  4.     class Alloc = allocator<Key>  
  5. class unordered_set;  

       C++ 11中对unordered_set描述大体如下:无序集合容器(unordered_set)是一个存储唯一(unique,即无重复)的关联容器(Associative container),容器中的元素无特别的秩序关系,该容器允许基于值的快速元素检索,同时也支持正向迭代。
       在一个unordered_set内部,元素不会按任何顺序排序,而是通过元素值的hash值将元素分组放置到各个槽(Bucker,也可以译为“桶”),这样就能通过元素值快速访问各个对应的元素(均摊耗时为O(1))。
       原型中的Key代表要存储的类型,而hash<Key>也就是你的hash函数,equal_to<Key>用来判断两个元素是否相等,allocator<Key>是内存的分配策略。一般情况下,我们只关心hash<Key>和equal_to<Key>参数,下面将介绍这两部分。
 

hash<Key>

      hash<Key>通过相应的hash函数,将传入的参数转换为一个size_t类型值,然后用该值对当前hashtable的bucket取模算得其对应的hash值。而C++标准库,为我们提供了基本数据类型的hash函数:
  1. 整型值:bool、char、unsigned char、wchar_t、char16_t、char32_t、short、int、long、long long、unsigned short、unsigned int、unsigned long、unsigned long long。上述的基本数据类型,其标准库提供的hash函数只是简单将其值转换为一个size_t类型值,具体可以参考标准库functional_hash.h头文件,如下所示:
    [cpp]  view plain  copy
     
    1.  /// Primary class template hash.  
    2.   template<typename _Tp>  
    3.     struct hash;  
    4.   
    5.   /// Partial specializations for pointer types.  
    6.   template<typename _Tp>  
    7.     struct hash<_Tp*> : public __hash_base<size_t, _Tp*>  
    8.     {  
    9.       size_t  
    10.       operator()(_Tp* __p) const noexcept  
    11.       { return reinterpret_cast<size_t>(__p); }  
    12.     };  
    13.   
    14.   // Explicit specializations for integer types.  
    15. #define _Cxx_hashtable_define_trivial_hash(_Tp)     \  
    16.   template<>                      \  
    17.     struct hash<_Tp> : public __hash_base<size_t, _Tp>  \  
    18.     {                                                   \  
    19.       size_t                                            \  
    20.       operator()(_Tp __val) const noexcept              \  
    21.       { return static_cast<size_t>(__val); }            \  
    22.     };  
    23.   
    24.   /// Explicit specialization for bool.  
    25.   _Cxx_hashtable_define_trivial_hash(bool)  
    26.   
    27.   /// Explicit specialization for char.  
    28.   _Cxx_hashtable_define_trivial_hash(char)  
    29.   
    30.   /// Explicit specialization for signed char.  
    31.   _Cxx_hashtable_define_trivial_hash(signed char)  
    32.   
    33.   /// Explicit specialization for unsigned char.  
    34.   _Cxx_hashtable_define_trivial_hash(unsigned char)  
    35.   
    36.   /// Explicit specialization for wchar_t.  
    37.   _Cxx_hashtable_define_trivial_hash(wchar_t)  
    38.   
    39.   /// Explicit specialization for char16_t.  
    40.   _Cxx_hashtable_define_trivial_hash(char16_t)  
    41.   
    42.   /// Explicit specialization for char32_t.  
    43.   _Cxx_hashtable_define_trivial_hash(char32_t)  
    44.   
    45.   /// Explicit specialization for short.  
    46.   _Cxx_hashtable_define_trivial_hash(short)  
    47.   
    48.   /// Explicit specialization for int.  
    49.   _Cxx_hashtable_define_trivial_hash(int)  
    50.   
    51.   /// Explicit specialization for long.  
    52.   _Cxx_hashtable_define_trivial_hash(long)  
    53.   
    54.   /// Explicit specialization for long long.  
    55.   _Cxx_hashtable_define_trivial_hash(long long)  
    56.   
    57.   /// Explicit specialization for unsigned short.  
    58.   _Cxx_hashtable_define_trivial_hash(unsigned short)  
    59.   
    60.   /// Explicit specialization for unsigned int.  
    61.   _Cxx_hashtable_define_trivial_hash(unsigned int)  
    62.   
    63.   /// Explicit specialization for unsigned long.  
    64.   _Cxx_hashtable_define_trivial_hash(unsigned long)  
    65.   
    66.   /// Explicit specialization for unsigned long long.  
    67.   _Cxx_hashtable_define_trivial_hash(unsigned long long)  
    对于指针类型,标准库只是单一将地址转换为一个size_t值作为hash值,这里特别需要注意的是char *类型的指针,其标准库提供的hash函数只是将指针所指地址转换为一个sieze_t值,如果,你需要用char *所指的内容做hash,那么,你需要自己写hash函数或者调用系统提供的hash<string>。
  2. 标准库为string类型对象提供了一个hash函数,即:Murmur hash,。对于float、double、long double标准库也有相应的hash函数,这里,不做过多的解释,相应的可以参看functional_hash.h头文件。
      上述只是介绍了基本数据类型,而在实际应用中,有时,我们需要使用自己写的hash函数,那怎么自定义hash函数?参考标准库基本数据类型的hash函数,我们会发现这些hash函数有个共同的特点:通过定义函数对象,实现相应的hash函数,这也就意味我们可以通过自定义相应的函数对象,来实现自定义hash函数。比如:已知平面上有N,每个点的x轴、y轴范围为[0,100],现在需要统计有多少个不同点?hash函数设计为:将每个点的x、y值看成是101进制,如下所示:
[cpp]  view plain  copy
 
  1. #include<bits\stdc++.h>  
  2. using namespace std;  
  3. struct myHash   
  4. {  
  5.     size_t operator()(pair<int, int> __val) const  
  6.     {  
  7.         return static_cast<size_t>(__val.first * 101 + __val.second);  
  8.     }  
  9. };  
  10. int main()  
  11. {  
  12.     unordered_set<pair<int, int>, myHash> S;  
  13.     int x, y;  
  14.     while (cin >> x >> y)  
  15.         S.insert(make_pair(x, y));  
  16.     for (auto it = S.begin(); it != S.end(); ++it)  
  17.         cout << it->first << " " << it->second << endl;  
  18.     return 0;  
  19. }  

equal_to<key>

             该参数用于实现比较两个关键字是否相等,至于为什么需要这个参数?这里做点解释,前面我们说过,当不同关键字,通过hash函数,可能会得到相同的关键字值,每当我们在unordered_set里面做数据插入、删除时,由于unordered_set关键字唯一性,所以我们得确保唯一性。标准库定义了基本类型的比较函数,而对于自定义的数据类型,我们需要自定义比较函数。这里有两种方法:重载==操作符和使用函数对象,下面是STL中实现equal_to<Key>的源代码:
[cpp]  view plain  copy
 
  1. template<typename _Arg, typename _Result>  
  2.     struct unary_function  
  3.     {  
  4.       /// @c argument_type is the type of the argument  
  5.       typedef _Arg  argument_type;     
  6.   
  7.       /// @c result_type is the return type  
  8.       typedef _Result   result_type;    
  9.     };  
  10. template<typename _Tp>  
  11.     struct equal_to : public binary_function<_Tp, _Tp, bool>  
  12.     {  
  13.       bool  
  14.       operator()(const _Tp& __x, const _Tp& __y) const  
  15.       { return __x == __y; }  
  16.     };  

扩容与缩容

             在vector中,每当我们插入一个新元素时,如果当前的容量(capacity)已不足,需要向系统申请一个更大的空间,然后将原始数据拷贝到新空间中。这种现象在unordered_set中也存在,比如当前的表长为100,而真实存在表中的数据已经大于1000个元素,此时,每个bucker均摊有10个元素,这样就会影响到unordered_set的存取效率,而标准库通过采用某种策略来对当前空间进行扩容,以此来提高存取效率。当然,这里也存在缩容,原理和扩容类似,不过,需要注意的是,每当unordered_set内部进行一次扩容或者缩容,都需要对表中的数据重新计算,也就是说,扩容或者缩容的时间复杂度至少为
code:
// unordered_set::find
#include <iostream>
#include <string>
#include <unordered_set>

int main ()
{
  std::unordered_set<std::string> myset = { "red","green","blue" };

  std::string input;
  std::cout << "color? ";
  getline (std::cin,input);

  std::unordered_set<std::string>::const_iterator got = myset.find (input);

  if ( got == myset.end() )
    std::cout << "not found in myset";
  else
    std::cout << *got << " is in myset";

  std::cout << std::endl;

  return 0;
}




unordered_set与与unordered_map相似,这次主要介绍unordered_set

unordered_set它的实现基于hashtable,它的结构图仍然可以用下图表示,这时的空白格不在是单个value,而是set中的key与value的数据包

有unordered_set就一定有unordered_multiset.跟set和multiset一样,一个key可以重复一个不可以

unordered_set是一种无序集合,既然跟底层实现基于hashtable那么它一定拥有快速的查找和删除,添加的优点.基于hashtable当然就失去了基于rb_tree的自动排序功能

unordered_set无序,所以在迭代器的使用上,set的效率会高于unordered_set

注:

一是这个容器是个集合,所以重复插入相同的值是没有效果的。大家可以看到我们这里第7行和第9行插入了2次3,实际上这个集合里也只有1个3,第10行输出的结果是2。
二是find的返回值是一个迭代器(iterator),如果找到了会返回指向目标元素的迭代器,没找到会返回end()。

对于unordered_set,insert/find/erase的平均复杂度是O(1),但是最坏复杂度是O(N)的,这里N是指容器中元素数量。
 

复制代码

  1. template<class _Value,
  2. class _Hash = hash<_Value>,
  3. class _Pred = std::equal_to<_Value>,
  4. class _Alloc = std::allocator<_Value> >
  5. class unordered_set
  6. : public __unordered_set<_Value, _Hash, _Pred, _Alloc>
  7. {
  8.    typedef __unordered_set<_Value, _Hash, _Pred, _Alloc> _Base;
  9. }  

参数1 _Value key和value的数据包

参数2 _Hash hashfunc获取hashcode的函数

参数3 _Pred 判断key是否相等

参数4 分配器

下面介绍一下unordered_set的基本使用,最后我会分享一下我的测试代码


 

一 定义

  1. //定义
  2. unordered_set<int> c1;
  3. //operator=
  4. unordered_set<int> c2;
  5. c2 = c1;

 

二 容量操作

  1. //判断是否为空
  2. c1.empty();
  3. //获取元素个数 size()
  4. c1.size();
  5. //获取最大存储量 max_size()
  6. c1.max_size();

 

三 迭代器操作

  1. //返回头迭代器 begin()
  2. unordered_set<int>::iterator ite_begin = c1.begin();
  3. //返回尾迭代器 end()
  4. unordered_set<int>::iterator ite_end = c1.end();
  5. //返回const头迭代器 cbegin()
  6. unordered_set<int>::const_iterator const_ite_begin = c1.cbegin();
  7. //返回const尾迭代器 cend()
  8. unordered_set<int>::const_iterator const_ite_end = c1.cend();
  9. //槽迭代器
  10. unordered_set<int>::local_iterator local_iter_begin = c1.begin(1);
  11. unordered_set<int>::local_iterator local_iter_end = c1.end(1);

四 基本操作

  1. //查找函数 find() 通过给定主键查找元素
  2. unordered_set<int>::iterator find_iter = c1.find(1);
  3.  //value出现的次数 count() 返回匹配给定主键的元素的个数
  4. c1.count(1);
  5. //返回元素在哪个区域equal_range() 返回值匹配给定搜索值的元素组成的范围
  6. pair<unordered_set<int>::iterator, unordered_set<int>::iterator> pair_equal_range = c1.equal_range(1);
  7. //插入函数 emplace()
  8. c1.emplace(1);
  9. //插入函数 emplace_hint() 使用迭代器
  10. c1.emplace_hint(ite_begin, 1);
  11. //插入函数 insert()
  12. c1.insert(1);
  13. //删除 erase()
  14. c1.erase(1);//1.迭代器 value 区域
  15. //清空 clear()
  16. c1.clear();
  17. //交换 swap()
  18. c1.swap(c2);

 五 篮子操作

复制代码

  1. //篮子操作 篮子个数 bucket_count() 返回槽(Bucket)数
  2. c1.bucket_count();
  3. //篮子最大数量 max_bucket_count() 返回最大槽数
  4. c1.max_bucket_count();
  5. //篮子个数 bucket_size() 返回槽大小
  6. c1.bucket_size(3);
  7. //返回篮子 bucket() 返回元素所在槽的序号
  8. c1.bucket(1);
  9. // load_factor 返回载入因子,即一个元素槽(Bucket)的最大元素数
  10. c1.load_factor();
  11. // max_load_factor 返回或设置最大载入因子
  12. c1.max_load_factor();

 

六 内存操作

  1. // rehash 设置槽数
  2. c1.rehash(1);
  3. // reserve 请求改变容器容量
  4. c1.reserve(1000);

 

七 hash func

    1. //hash_function() 返回与hash_func相同功能的函数指针
    2. auto hash_func_test = c1.hash_function();
    3. //key_eq() 返回比较key值得函数指针
posted @ 2019-11-27 19:25  任仁人  阅读(345)  评论(0编辑  收藏  举报