C++ hashmap——unordered_map

主要尝试回答下面几个问题:

  1. 一般情况下,使用 hash 结构,需要有桶的概念,那么 unordered_map 是如何自动管理桶的,这个问题其实再细分的话是这样的:
    1. 初始的桶是如何设置的
    2. 当需要扩容的时候,是如何重新分布的
  2. 对于 string,unordered_map 的默认哈希函数是怎样的

需要注意的是,unorder_map 和 unorder_set,其实都是一个封装而已,底下用的是 hashtable,所以分析也着重分析 hashtable

最根本的区别为底层的实现机制不同,map底层实现为红黑时,hash_map为hash表,所以就有一些其他方面的不同:
1)map存储的时候为排好序的,所以输出时候也是排序的。而hash_map不是的。
2)map具有稳定性,底层存储为树,这种算法差不多相当与list线性容器的折半查找的效率一样,都是O (log2N)。而hash_map使用hash表来排列配对,hash表是使用关键字来计算表位置。当这个表的大小合适,并且计算算法合适的情况下,hash表的算法复杂度为O(1)的,但是这是理想的情况下的,如果hash表的关键字计算与表位置存在冲突,那么最坏的复杂度为O(n)。 map在一次查找中,你可以断定它最坏的情况下其复杂度不会超过O(log2N)。而hash表就不一样,是O(1),还是O(N),或者在其之间,你并不能把握。

我觉得如果数量级很小,不到w,那么使用map和hash_map的区别不大,速度,稳定性都相差不大。但是如果数量级很大,就要考虑是要平均效率高,还是稳定性好了,如果用hash_map那么可以自己来根据经验来设定hash函数优化速度。而如果算法对稳定性要求高的话,首选map。


不过gnu hash_map和c++ stl的api不兼容,c++ tr1(C++ Technical Report1)作为标准的扩展,实现了hash map,提供了和stl兼容一致的api,称为unorder_map.在头文件 <tr1/unordered_map>中。另外c++ tr1还提供了正则表达式、智能指针、hash table、 随机数生成器的功能。 
Linux 下的hash_map

[cpp]  view plain  copy
  1. #include <iostream>  
  2. #include <string>  
  3. #include <tr1/unordered_map>  
  4. using namespace std;  
  5.   
  6. int main(){  
  7.  typedef std::tr1::unordered_map<int,string> hash_map;  
  8.  hash_map hm;  
  9.  hm.insert(std::pair<int,std::string>(0,"Hello"));  
  10.  hm[1] = "World";  
  11.  for(hash_map::const_iterator it = hm.begin(); it != hm.end(); ++it){  
  12.   cout << it->first << "-> " << it->second << endl;  
  13.  }  
  14.  return 0;  
  15. }  

先来看一个典型的操作,[ ] 运算符,在 679 行附近,有这样的代码

template<typename K, typename Pair, typename Hashtable>
    typename map_base<K, Pair, extract1st<Pair>, true, Hashtable>::mapped_type&
    map_base<K, Pair, extract1st<Pair>, true, Hashtable>::
    operator[](const K& k)
    {
  Hashtable* h = static_cast<Hashtable*>(this);
  typename Hashtable::hash_code_t code = h->m_hash_code(k);
  std::size_t n = h->bucket_index(k, code, h->bucket_count());

  typename Hashtable::node* p = h->m_find_node(h->m_buckets[n], k, code);
  if (!p)
  return h->m_insert_bucket(std::make_pair(k, mapped_type()),
          n, code)->second;
  return (p->m_v).second;
    }

可以看到,这是典型的 hash 操作的写法

  1. 先对 key 算出 hash code
  2. 找到这个 hash code 对应的桶
  3. 在这个桶里面,遍历去找这个 key 对应的节点
  4. 把节点返回

需要注意的是,如果找不到节点,不是返回空,而是会创建一个新的空白节点,然后返回这个空白节点,这里估计是受到返回值的约束,因为返回值声明了必须为一个引用,所以总得搞一个东西出来才能有的引用

接下来看初始化过程,gdb 跟踪代码可以发现,在 /usr/include/c++/4.1.2/tr1/unordered_map:86,有下面这样的代码,可以看到,初始化的桶大小,被写死为 10。

explicit
  unordered_map(size_type n = 10,
        const hasher& hf = hasher(),
        const key_equal& eql = key_equal(),
        const allocator_type& a = allocator_type())
  : Base(n, hf, Internal::mod_range_hashing(),
       Internal::default_ranged_hash(),
       eql, Internal::extract1st<std::pair<const Key, T> >(), a)
  { }

但是,我们看一下下面这个代码的输出

#include <tr1/unordered_map>
#include <string>
#include <stdio.h>

int main() {
    std::tr1::unordered_map<std::string, int> m;
    printf("%d\n", m.bucket_count());
    return 0;
}

输出是 11。为什么呢,这个涉及到 rehash。他是初始化为 10,然后 rehash 为 11 了。

rehash 有两个问题,一个是判断什么时候需要 rehash,一个是什么时候需要 rehash,一个是怎么 rehash。

need_rehash 在 hasttable 的 614 附近:

inline std::pair<bool, std::size_t>
  prime_rehash_policy::
  need_rehash(std::size_t n_bkt, std::size_t n_elt, std::size_t n_ins) const
  {
    if (n_elt + n_ins > m_next_resize)
      {
  float min_bkts = (float(n_ins) + float(n_elt)) / m_max_load_factor;
  if (min_bkts > n_bkt)
    {
      min_bkts = std::max(min_bkts, m_growth_factor * n_bkt);
      const unsigned long* const last = X<>::primes + X<>::n_primes;
      const unsigned long* p = std::lower_bound(X<>::primes, last,
                  min_bkts, lt());
      m_next_resize = 
        static_cast<std::size_t>(std::ceil(*p * m_max_load_factor));
      return std::make_pair(true, *p);
    }
  else 
    {
      m_next_resize = 
        static_cast<std::size_t>(std::ceil(n_bkt * m_max_load_factor));
      return std::make_pair(false, 0);
    }
      }
    else
      return std::make_pair(false, 0);
  }

来看他是怎么做的,首先是用一个 m_max_load_factor 的因子来判断目前的容量需要多少个哈希桶,如果需要 rehash,那么使用素数表来算出新的桶需要多大。

素数表在 491 行附近:

template<int ulongsize>
    const unsigned long X<ulongsize>::primes[256 + 48 + 1] =
    {
      2ul, 3ul, 5ul, 7ul, 11ul, 13ul, 17ul, 19ul, 23ul, 29ul, 31ul,

初始的时候,m_max_load_factor(1), m_growth_factor(2), m_next_resize(0),根据 std::lower_bound 来找到比 10 大的最小素数是 11,于是就分配为 11 个桶。

rehash 就很平淡无奇了,一个一个重算,然后重新填进去,没有什么特别的。

template<typename K, typename V, 
     typename A, typename Ex, typename Eq,
     typename H1, typename H2, typename H, typename RP,
     bool c, bool ci, bool u>
    void
    hashtable<K, V, A, Ex, Eq, H1, H2, H, RP, c, ci, u>::
    m_rehash(size_type n)
    {
      node** new_array = m_allocate_buckets(n);
      try
  {
    for (size_type i = 0; i < m_bucket_count; ++i)
      while (node* p = m_buckets[i])
        {
    size_type new_index = this->bucket_index(p, n);
    m_buckets[i] = p->m_next;
    p->m_next = new_array[new_index];
    new_array[new_index] = p;
        }
    m_deallocate_buckets(m_buckets, m_bucket_count);
    m_bucket_count = n;
    m_buckets = new_array;
  }
      catch(...)
  {
    // A failure here means that a hash function threw an exception.
    // We can't restore the previous state without calling the hash
    // function again, so the only sensible recovery is to delete
    // everything.
    m_deallocate_nodes(new_array, n);
    m_deallocate_buckets(new_array, n);
    m_deallocate_nodes(m_buckets, m_bucket_count);
    m_element_count = 0;
    __throw_exception_again;
  }
    }

然后就是 hash 函数了。hash 函数位于 /usr/include/c++/4.1.2/tr1/functional:1194,对于 std::string,用的是下面这种 hash 函数

template<>
    struct Fnv_hash<8>
    {
      static std::size_t
      hash(const char* first, std::size_t length)
      {
  std::size_t result = static_cast<std::size_t>(14695981039346656037ULL);
  for (; length > 0; --length)
    {
      result ^= (std::size_t)*first++;
      result *= 1099511628211ULL;
    }
  return result;
      }
    };

这个叫 FNV hash,http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function ,FNV 有分版本,例如 FNV-1 和 FNV-1a,区别其实就是先异或再乘,或者先乘在异或,这里用的是 FNV-1a,为什么呢,维基里面说,The small change in order leads to much better avalanche characteristics,什么叫 avalanche characteristics 呢,这个是个密码学术语,叫雪崩效应,意思是说输入的一个非常微小的改动,也会使最终的 hash 结果发生非常巨大的变化,这样的哈希效果被认为是更好的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值