HashTable : HashMap 和 HashSet 说俺是隐形守护者

在这里插入图片描述


前话:

今天北大松哥给王道训练营的成员们讲了桶排序(计数/基数排序): 将要排序的序列的元素全部添加到 其计数的数组中:

  • 排序的序列元素值对应计数的数组下标, 比如序列为 2,2,3 , 元素2 对应 计数的数组角标为 2 的桶,遍历到即 arr[2] += 1, 表示将元素 2 放入桶 2 中,之后再将下个2也放入桶2中,再将下个 3 放入 桶 3 中: arr[3]+=1;
  • 从角标 0 开始遍历桶,如果该位置上的桶有元素,arr[i] > 0 则输出桶的元素(根据对应值来决定输出的个数), 1,3,2,3,1 输出为 1,1,2,3,3

经历上述步骤就完成了桶排序,桶排序的时间是 O(n), 但是致命的问题就是数组的额外负担是很大的,假定我们有 67777 这个值,那么是不是我们的计数的数组就至少有 67777 个int类型空间,这是非常恐怖的!!!限制性很强。

那么,稍后我们会提到上面的解决办法。



HashTable 概述

在上一节我们介绍了一种被广泛应用的平衡二叉搜索树: RB-tree(红黑树)。红黑树指针树形的平衡上表现不错, 在效率表现和实现复杂度上也保持相当的平衡,其因此称为 STL set 和 map 的标准底层机制。

二叉搜索树具有对数平均时间 O(logn) 去查找特定值, 这次我们介绍一个更厉害的在 插入,删除,搜索上具有 ”常数平均时间“ 的表现的 哈希表结构, 而且这种表现和桶排数组的统计类似, 不需依赖输入元素的随机性。

在这里插入图片描述

哈希表的特点介绍:

  • 本质上是数组,数组的元素是其 有名项 。
  • 提供对任何有名项的存取和删除操作, 其本身可视为一种字典结构。
  • 建表操作通过映射函数计算其插入的桶的位置,并将其插入, pos = get_bucket(key)
    查找操作将键值通过映射函数得到其桶位置,直接访问到其元素。

由于桶是固定的,可能会有不同的键值映射到相同的桶上,我们利用开放寻址法开链法 来避免哈希冲突。


避免哈希冲突的方法

开放寻址法:

  1. 若发生了哈希冲突,则循序的按该位置后的下一位置去寻找一个可用空间
  2. 或者 按 pos += 2^1 ,发生一次冲突跳跃 2 的一次方,两次 2的平方,依次类推

缺点很明显,若数目十分多,光寻找位置就要反复跳跃,违背了其 O(1)查找

开链法:

  • 哈希表中的每一个元素维护一个链表,当出现哈希冲突时,就插入到链表中,表示他们共用一个 桶
  • 查找时,利用哈希函数映射到该链表中,若链表为空,则视为查找不到,不为空,则遍历查找,若遍历到链表尾部也没找到,也是查找不到。

好处:虽然说针对 list 而进行的搜寻 只能是一种线性操作, 但如果 list 够短, 速度还是够快。

STL 的 hash_table 就是采用 开链法 。


hashtable 的 桶子 和 节点

从桶排序到 hashtable 中的桶,我们大概能推导出桶这个现实意思:

存放了很多东西的 物品 桶: BUCKET

STL hashtable 给出桶的概念即 hashtable 的元素(一整个链表,存放了很多映射到该桶的元素节点) , 其节点即存放了元素信息的节点

在这里插入图片描述

hashtable 的节点定义:

template <class value>
struct hashtable_node {
  hashtable_node *next;
  value val;
}



hashtable 的数据结构

我们知道 hashtable 使用开链法时很有可能导致一个桶放多个元素, 这样降低了其查找的效率,STL 的做法完美了解决了这个问题:

  • 使用vector<node*> 作为其 hashtable 的主要数据结构
  • 当现存元素大于等于当前vector 的容量,这样即描述为(hashtable 的平均查找时间复杂度大于 O(1), 有些桶内元素大于1),这时对 vector 进行扩充,重建哈希表。


1. 扩充? 扩充的多少,桶的变化

STL 以质数来设计表格大小, 并且先将 28 个质数(逐渐呈现大约两倍的关系)计算好,最小 53, 以备随时访问,同时提供一个函数 STL_NEXT_PRIME 来查询在 28 个质数中,最接近某个数并大于某数的质数。

也就是说:我们桶的变化是根据当前所需的量(元素的数量)找到最接近其并大于其的桶数量进行扩充:


static const int stl_num_primes = 28;
static const int unsigned long stl_prime_list[stl_num_primes] = {
    53,97,193,389,769//。。。。
};

//STL 中以质数设计表格大小, 并先将 28 个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问。
//提供如下函数,去查询在 28 个质数中,最接近某数并大于某数的质数。

inline unsigned long stl_next_prime(unsigned long n) {
  const unsigned long *first = stl_prime_list;
  const unsigned long *last = stl_prime_list + stl_num_primes;
  const unsigned long *pos = std::lower_bound(first, last, n);
  //low_bound 找到第一个不小于该值的数
  return pos == last ? *(last - 1) : *pos;
}



2 . hashtable 的数据结构内部

template<class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
class hashtable {
 public:
  typedef HashFcn hasher;
  typedef EqualKey key_equal;
  typedef size_t size_type;

 private:
  hasher hash;             //根据元素键值计算其桶的位置
  key_equal equals;        //根据元素键值的相等性判断,(用于判定是否
                           //与查找目标相等)
  ExtractKey get_key;

  typedef my_hashtable_node<Value> node;
  typedef std::allocator<node> node_allocater;

  std::vector<node*> buckets;    //桶
  size_type num_elements;

  node* new_node(const Value &obj) {

    node *n = node_allocater::allocate(1);
    n->next = 0;
    try {
      node_allocater::construct(&n->val, obj);
      return n;
    } catch (...) {
      node_allocater::deallocate(n);
    }
  }
  void delete_node(node *n) {
    node_allocater::destroy(&n->val);
    node_allocater::deallocate(n);
  }

 public:
  size_type bucket_count() const { return buckets.size(); }
  size_type max_bucket_count() const { return stl_prime_list[stl_num_primes - 1]; }


};




hash_set ( unorder_set ) / hash_map

C++11 中定义了 4 个无序关联容器,这些容器不是使用比较运算符来组织元素,而是以哈希函数和关键字 == 运算符, 当关键字类型的元素没有明显的序关系的情况下, 无序容器是非常有用的。

C++primer 上 p394 这样说道;

很明显, unordered_set 和 unordered_map 底层采用的 hash_table 机制,基本上 其无序容器的操作接口其哈希表结构全部已经提供,也可以说无序容器的操作都是转调 hashtable 的操作行为而已。

  • 由于hashtable 没有自动排序功能, 所以其 无序容器也都没有
  • hashtable 有一些无法处理的型别(自定义的 抽象数据结构),除非我们自己提供其 hash函数与 两个元素之间的相等比较操作,否则无法处理

在这里插入图片描述

这里我们谈一下 无序容器除了自身的增删改查元素之外,对 桶 的管理操作:

桶接口
c.bucket_count()    正在使用的桶数目
c.max_bucket_count()
c.bucket_size(n)
c.bucket(k)

哈希策略:

c.load_factor() 每个桶的平均元素数目,返回 float 值
c.max_load_factor()   c 试图维护的平均桶大小,返回 float值, 
c 会在需要时添加新的桶,使得 load_factor <= max_load_factor

c.rehash() : 重组哈希表,
       使得 bucket_count >= n 并且 bucket_count > size/max_load_factor
 
c.reserve(n)   : 扩容哈希表(vector<node*>



无序容器与 多关键字无序容器

从字面上来说:

unorder_set 与 unorder_multiset 的区别就在于关键字能否重复,我们来看看在hashtable中是怎么处理这一情况的。

在 STL 的 hashtable 的源码中终于找到了!!! 原来呢,在hashtable 插入节点时, 提供了不同的两个插入函数:

  • insert_unique( obj ) : 插入元素不允许重复,重复时直接不插入(忽略)
  • insert_equal( obj) : 插入元素允许重复,重复时插入重复的元素节点之后。

这样看来:

  • 当 unorder_set 插入元素时底层哈希表添加元素时使用的是 insert_unique
  • 当 unorder_multiset 插入元素时底层哈希表添加元素时使用的是 insert_equal

谢谢观看,能观看本博客真的是有缘, 如果有想看的内容(游戏客户端/服务器 方面都可以)可以直接评论!

猜你喜欢

转载自blog.csdn.net/chongzi_daima/article/details/107748779