1. 简介
Hash_table可提供对任何有名项(nameditem)的存取、删除和查询操作。由于操作对象是有名项,所以hash_table可被视为是一种字典结构(dictionary)。
Hash_table使用名为hashfaction的散列函数来定义有名项与存储地址之间的映射关系。使用hash faction会带来一个问题:不同的有名项可能被映射到相同的地址,这便是所谓的碰撞(collision)问题,解决碰撞问题的方法主要有三种:线性探测(linear probing)、二次探测(quadratic probing)、开链(separate chaining)。
1.1 线性探测
先介绍一个名词:负载系数(loadingfactor),意为元素个数除以表格大小。负载系数永远在0 ~ 1之间(除非使用开链策略)。
当hash faction计算出某个元素的插入位置,而该位置已经被占用时,我们循序往下查找,直到找到一个可用空间为止。然而这凸显了一个问题:平均插入成本的涨幅,远高于负载系数的涨幅。这样的现象在hashing过程中被称为主集团(primary clustering)。此时我们手上有一大团已经被占用的方格,插入操作极有可能在主集团所形成的泥泞中艰难爬行,不断碰撞,最后好不容易才找到一个落脚处,但这又助长了主集团的泥泞面积。
1.2 二次探测
二次探测主要用来解决主集团的问题。如果hash faction计算出新元素的位置为H,但该位置被占用,那我们尝试H + 1^2、H + 2^2、H +3^2…..,而不是H + 1、H + 2、H + 3…….。二次探测可以消除主集团(primary clustering),但是可能造成次集团(secondary clustering)。
1.3 开链
这是我们采用的方法。
在每个表格元素中维护一个list(链表),list由hash faction为我们,我们在list上执行元素的插入、查找、删除等操作,虽然对list进行的查询是线性操作,但是listlist足够短的话,我们也能获得较好的效率。注意,使用开链法,表格的负载系数可能大于1。
1.4 开链法的名称约定
下面介绍开链法中的基本术语,这些术语将贯穿博客和代码注释。
我们用vector作为hash_table的底层实现,并称hash_table内的每个元素为桶子bucket,每个桶子里装着一个list头指针,通过list头指针,可以串联出一串节点(node)。这就是开链法的精髓:hash_table(底层是vector)的每个元素装着list头指针,每个list头指针可以开出一条链表。
Hash_table的实现方式有点像deque,关于deque的实现,请看另一篇博客:STL简单deque的实现
2.设计与实现
我用VS2013写的程序(github),set和multiset版本的代码位于cghSTL/version/cghSTL-0.4.3.rar
在STL中,set和multiset的底层都需要以下几个文件:
1. globalConstruct.h,构造和析构函数文件,位于cghSTL/allocator/cghAllocator/
2. cghAlloc.h,空间配置器文件,位于cghSTL/allocator/cghAllocator/
3. hash_table_node.h,hash_table节点的实现,位于cghSTL/associative
containers/RB-tree/
4. hash_table_iterator.h,hash_table迭代器的实现,位于cghSTL/associativecontainers/cgh_hash_table/
5. cgh_hash_table.h,hash_table的实现,位于cghSTL/associativecontainers/cgh_hash_table/
6. test_cgh_hash_table,测试文件,位于cghSTL/test/
另外,hash_table的底层采用vector作为容器,关于vector的实现,请看博客:STL简单vector的实现
下面介绍hash_table具体实现步骤
2.1 构造与析构
先看第一个,globalConstruct.h构造函数文件
/******************************************************************* * Copyright(c) 2016 Chen Gonghao * All rights reserved. * * [email protected] * * 功能:全局构造和析构的实现代码 ******************************************************************/ #include "stdafx.h" #include <new.h> #include <type_traits> #ifndef _CGH_GLOBAL_CONSTRUCT_ #define _CGH_GLOBAL_CONSTRUCT_ namespace CGH { #pragma region 统一的构造析构函数 template<class T1, class T2> inline void construct(T1* p, const T2& value) { new (p)T1(value); } template<class T> inline void destroy(T* pointer) { pointer->~T(); } template<class ForwardIterator> inline void destroy(ForwardIterator first, ForwardIterator last) { // 本来在这里要使用特性萃取机(traits编程技巧)判断元素是否为non-trivial // non-trivial的元素可以直接释放内存 // trivial的元素要做调用析构函数,然后释放内存 for (; first < last; ++first) destroy(&*first); } #pragma endregion } #endif
按照STL的接口规范,正确的顺序是先分配内存然后构造元素。构造函数的实现采用placement new的方式;为了简化起见,我直接调用析构函数来销毁元素,而在考虑效率的情况下一般会先判断元素是否为non-trivial类型,non-trivial的元素可以直接释放内存,可以理解为non-trivial全部分配在栈上,不用我们操心,由程序自动回收,trivial的元素要分配在堆上,需要手工调用析构函数来销毁,然后释放内存。
2.2 空间配置器
cghAlloc.h是空间配置器文件,空间配置器负责内存的申请和回收。
/******************************************************************* * Copyright(c) 2016 Chen Gonghao * All rights reserved. * * [email protected] * * 功能:cghAllocator空间配置器的实现代码 ******************************************************************/ #ifndef _CGH_ALLOC_ #define _CGH_ALLOC_ #include <new> #include <cstddef> #include <cstdlib> #include <climits> #include <iostream> namespace CGH { #pragma region 内存分配和释放函数、元素的构造和析构函数 // 内存分配 template<class T> inline T* _allocate(ptrdiff_t size, T*) { set_new_handler(0); T* tmp = (T*)(::operator new((size_t)(size * sizeof(T)))); if (tmp == 0) { std::cerr << "out of memory" << std::endl; exit(1); } return tmp; } // 内存释放 template<class T> inline void _deallocate(T* buffer) { ::operator delete(buffer); } // 元素构造 template<class T1, class T2> inline void _construct(T1* p, const T2& value) { new(p)T1(value); } // 元素析构 template<class T> inline void _destroy(T* ptr) { ptr->~T(); } #pragma endregion #pragma region cghAllocator空间配置器的实现 template<class T> class cghAllocator { public: typedef T value_type; typedef T* pointer; typedef const T* const_pointer; typedef T& reference; typedef const T& const_reference; typedef size_t size_type; typedef ptrdiff_t difference_type; template<class U> struct rebind { typedef cghAllocator<U> other; }; static pointer allocate(size_type n, const void* hint = 0) { return _allocate((difference_type)n, (pointer)0); } static void deallocate(pointer p, size_type n) { _deallocate(p); } static void deallocate(void* p) { _deallocate(p); } void construct(pointer p, const T& value) { _construct(p, value); } void destroy(pointer p) { _destroy(p); } pointer address(reference x) { return (pointer)&x; } const_pointer const_address(const_reference x) { return (const_pointer)&x; } size_type max_size() const { return size_type(UINT_MAX / sizeof(T)); } }; #pragma endregion #pragma region 封装STL标准的空间配置器接口 template<class T, class Alloc = cghAllocator<T>> class simple_alloc { public: static T* allocate(size_t n) { return 0 == n ? 0 : (T*)Alloc::allocate(n*sizeof(T)); } static T* allocate(void) { return (T*)Alloc::allocate(sizeof(T)); } static void deallocate(T* p, size_t n) { if (0 != n)Alloc::deallocate(p, n*sizeof(T)); } static void deallocate(void* p) { Alloc::deallocate(p); } }; #pragma endregion } #endif
classcghAllocator是空间配置器类的定义,主要的四个函数的意义如下:allocate函数分配内存,deallocate函数释放内存,construct构造元素,destroy析构元素。这四个函数最终都是通过调用_allocate、_deallocate、_construct、_destroy这四个内联函数实现功能。
我们自己写的空间配置器必须封装一层STL的标准接口,
template<classT, class Alloc = cghAllocator<T>> classsimple_alloc
2.3 vector的实现
根据开链法的设计思路,hash_table的底层容器是vector,vector的设计方法在本博客的讨论范围内,感兴趣的童鞋请移步:STL简单vector的实现
2.4 hash_table节点的实现
hash_table的底层容器是vector,所谓hash_table的节点,指的就是vector的元素。根据开链法的设计思路,vector的每个元素是list(链表)的头节点,由list的头节点串出一条链表,所以说,实现hash_table的节点也就是实现list节点。
List节点的实现很简单,相信童鞋们见过多次了,直接给出代码。
hash_table_node.h
/******************************************************************* * Copyright(c) 2016 Chen Gonghao * All rights reserved. * * [email protected] * * 内容:cgh_hash_table的节点 ******************************************************************/ #ifndef _CGH_HASH_TABLE_NODE_ #define _CGH_HASH_TABLE_NODE_ namespace CGH{ template<class value> struct hash_table_node{ hash_table_node* next; // 指向下一个节点 value val; // 节点的值域 }; } #endif
2.5 hash_table迭代器的实现
迭代器的内部结构可以分为以下部分:
1. 一堆typedef,成员变量的定义;
2. 迭代器的构造与析构函数;
3. 迭代器的一般操作,注意,hash_table的迭代器没有后退操作。
迭代器的代码注释已经写得很详细了,童鞋们可以参考迭代器的内部结构来总体把握迭代器的框架,通过注释来理解迭代器的工作原理。
hash_table_iterator.h
/******************************************************************* * Copyright(c) 2016 Chen Gonghao * All rights reserved. * * [email protected] * * 内容:cgh_hash_table的迭代器 ******************************************************************/ #ifndef _CGH_HASH_TABLE_ITERATOR_ #define _CGH_HASH_TABLE_ITERATOR_ #include "globalConstruct.h" #include "cghAlloc.h" #include "hash_table_node.h" #include "cgh_hash_table.h" #include <memory> using namespace::std; namespace CGH{ /* 迭代器中用到了cgh_hash_table,除了#include "hash_table.h"以外,我们要在这里声明一下cgh_hash_table 不然的话,在hash_table_iterator中 typedef cgh_hash_table<value, key, hashFun, extractKey, equalEey, Alloc> cgh_hash_table; 报错:error C2143: 语法错误 : 缺少“;”(在“<”的前面) */ template<class value, class key, class hashFun, class extractKey, class equalKey, class Alloc = cghAllocator<key>> class cgh_hash_table; template<class value,class key,class hashFun, class extractKey,class equalEey,class Alloc=cghAllocator<key>> struct hash_table_iterator{ #pragma region typedef 和 成员变量 typedef hash_table_iterator<value, key, hashFun, extractKey, equalEey, Alloc> iterator; // 声明迭代器 typedef hash_table_node<value> node; // 声明节点 typedef cgh_hash_table<value, key, hashFun, extractKey, equalEey, Alloc> cgh_hash_table; // 声明hash_table /* hash_table的迭代器没有后退操作,只能前进 */ typedef std::forward_iterator_tag iterator_category; typedef value value_type; // 声明实值类型 typedef ptrdiff_t difference_type; // 表示两迭代器之前的距离 typedef size_t size_type; typedef value& reference; // 声明引用类型 typedef value* pointer; // 声明指针类型 node* cur; // 节点和迭代器联系的纽带 cgh_hash_table* ht; // 迭代器和cgh_hash_table联系的纽带 #pragma endregion #pragma region 迭代器的构造 hash_table_iterator(node* n, cgh_hash_table* tab) :cur(n), ht(tab){} hash_table_iterator(){} #pragma endregion #pragma region 迭代器的一般操作 /* 解除引用,返回节点实值 */ reference operator*()const { return cur->val; } /* 解除引用 */ pointer operator->()const{ return &(operator*()); } /* 步进(注意,cgh_hash_table的迭代器只能前进)*/ iterator& operator++() { const node* old = cur; // 保存迭代器当前位置 // 由于节点被安置在list内,所以利用next指针可以轻易进行前进操作 cur = cur->next; // 如果前进一个步长之后来到list尾部,我们就要跳到下一个bucket上 if (!cur) { size_type bucket = ht->bkt_num(old->val); // 获得当前bucket的位置 // ++bucket:来到下一个bucket // cur = ht->buckets[bucket]:下一个bucket保存的list的头节点,如果cur == NULL // 说明下一个bucket为空,继续while循环,直到找到一个不为空的bucket while (!cur && ++bucket < ht->buckets.size()) { cur = ht->buckets[bucket]; } return *this; } } /* 步进(注意,cgh_hash_table的迭代器只能前进)*/ iterator& operator++(int) { iterator tmp = *this; ++*this; return tmp; } #pragma endregion }; } #endif
2.6 hash_table的实现
有了hash_table的节点和迭代器,接下来我们就要开始设计hash_table啦~
hash的内部结构可以分为以下部分:
1. 一堆typedef,成员变量的定义;
2. hash_table的构造与析构函数;
3. 提供给用户的api,提供对hash_table的读写操作;
4. 给定键值,计算bucket。也就是给定key值,根据hash faction计算出存放地址;
hash_table的代码注释已经写得很详细了,童鞋们可以参考hash_table的内部结构来总体把握hash_table的框架,通过注释来理解hash_table的工作原理。
cgh_hash_table.h
/******************************************************************* * Copyright(c) 2016 Chen Gonghao * All rights reserved. * * [email protected] * * 内容:cgh_hash_table的实现 ******************************************************************/ #ifndef _CGH_HASH_TABLE_ #define _CGH_HASH_TABLE_ #include "globalConstruct.h" // 全局构造与析构函数 #include "cghAlloc.h" // 空间配置器 #include "hash_table_node.h" #include "hash_table_iterator.h" #include "cghVector.h" // vector #include "cghUtil.h" // 工具类 #include <algorithm> // 需要用到算法 namespace CGH{ /* value: 节点的实值类型 key: 节点的键值类型 hashFun: hash faction的函数类型 extractKey:从节点中取出键值的方法 equalKey: 判断键值是否相同的方法 Alloc: 空间配置器 */ template<class value, class key, class hashFun, class extractKey, class equalKey, class Alloc = cghAllocator<key>> class cgh_hash_table{ #pragma region typedef 和 成员变量 public: typedef hashFun hasher; typedef equalKey key_equal; typedef size_t size_type; typedef value value_type; // 实值类型 typedef key key_type; // 键值类型 typedef hash_table_iterator<value, key, hasher, extractKey, equalKey, Alloc> iterator; // 迭代器 private: hasher hash; // hash faction key_equal equals; // 判断键值是否相同的方法 extractKey get_key; // 从节点中取出键值的方法 typedef hash_table_node<value> node; // 节点 typedef simple_alloc<node, Alloc> node_allocator; // 空间配置器 public: size_type num_elements; // hash_table中有效的节点个数 cghVector<node*, Alloc> buckets; // 用cghVector作为bucket的底层实现 #pragma endregion #pragma region cgh_hash_table的构造与析构 private: /* 构造新节点 */ node* new_node(const value_type& obj) { node* n = node_allocator::allocate(); // 空间配置器分配节点空间 n->next = 0; // 初始化节点的next指针指向NULL construct(&n->val, obj); // 全局构造函数,初始化节点实值 return n; // 返回新节点的指针 } /* 销毁节点 */ void delete_node(node* n) { destroy(&n->val); // 全局析构函数,销毁节点实值 node_allocator::deallocate(n); // 释还节点内存 } /* 我们以质数来设计表格大小,将28个质数(逐渐呈现大约两倍的关系)计算好 __stl_next_prime函数用于查询这28个质数中,“最接近某数并大于某数”的质数 __stl_num_primes = 28; // 表示28个质数 __stl_prime_list:指数数组,定义在本文件的末尾 */ 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; // 指向数组尾部的指针 // 这里用到了标准库的lower_bound算法,对质数数组(__stl_prime_list)排序(从小到大) const unsigned long* pos = std::lower_bound(first, last, n); return pos == last ? *(last - 1) : *pos; } /* 根据n值,返回合适的buckets大小 */ size_type next_size(size_type n){ return __stl_next_prime(n); } /* 初始化buckets,buckets的每个节点对应一个list链表,链表的节点类型是hash_table_node */ void initialize_buckets(size_type n) { const size_type n_buckets = next_size(n); // 取得初始化的buckets的元素个数 buckets.insert(buckets.end(), n_buckets, 0); // 每个元素对应的list链表初始化为空 } public: /* cgh_hash_table的构造函数 */ cgh_hash_table(size_type n, const hashFun& hf, const equalKey& eql) :hash(hf), equals(eql), get_key(extractKey()), num_elements(0) { initialize_buckets(n); } #pragma endregion #pragma region 提供给用户的API #pragma region 读操作 public: /* 返回当前bucket的总数 */ size_type bucket_count(){ return buckets.size(); } /* 最大bucket数 */ size_type max_bucket_count()const{ return __stl_prime_list[__stl_num_primes - 1]; } /* 返回当前cgh_hash_table中有效节点的个数 */ size_type size()const{ return num_elements; } /* 返回迭代器,指向第一个节点 */ iterator begin() { ///找到第一个非空的桶,得到其第一个元素 for (size_type n = 0; n < buckets.size(); ++n) { if (buckets[n]) { return iterator(buckets[n], this); } } return end(); // 如果cgh_hash_table为空,返回尾部 } /* 返回迭代器,指向第一个节点 */ iterator end() { return iterator(0, this); } /* 返回指定bucket中装的节点个数,节点类型为hash_table_node */ size_type elems_in_bucket(size_type num) { size_type result = 0; node* cur = buckets[num]; for (node* cur = buckets[num]; cur; cur = cur->next) { result += 1; } return result; } /* 输入键值,返回一个迭代器,指向键值对应的节点 */ iterator find(const key_type& key) { size_type n = bkt_num(key); // 计算键值落在哪个bucket里面 node* first; // 指向bucket的第一个节点 // 逐一比较bucket对应的list的每个节点的键值 for (first = buckets[n]; first && !equals(get_key(first->val), key); first = first->next){} return iterator(first, this); } /* 返回键值为key的节点个数 */ size_type count(const key_type& key) { size_type n = bkt_num(key); // 确定key落在哪个bucket里 size_type result = 0; // 遍历bucket对应的list的每个节点 for (const node* cur = buckets[n]; cur; cur = cur->next) { if (equals(get_key(cur->val), key)) // 如果节点键值等于key { ++result; } } return result; } #pragma endregion #pragma region 写操作 #pragma region 用户接口 public: /* 插入元素,不允许键值重复 */ cghPair<iterator, bool> insert_unique(const value_type& obj) { resize(num_elements + 1); // 判断是否需要重建表格 return insert_unique_noresize(obj); // 调用辅助函数 } /* 插入元素,允许键值重复 */ iterator insert_equal(const value_type& obj) { resize(num_elements + 1); // 判断是否需要重建表格 return insert_equal_noresize(obj); // 调用辅助函数 } #pragma endregion #pragma region 辅助函数 private: /* 是否需要重建表格 */ void resize(size_type num_elements_hint) { /* 判断重建表格的原则: 比较元素个数(计入新增元素)和bucket vector的大小 元素个数 > bucket vector的大小 => 重建表格 由此可知,每个bucket(list)的最大容量和bucket vector的大小相同 是一个 n * n 的矩阵 */ const size_type old_n = buckets.size(); // 获得bucket vector的大小 if (num_elements_hint > old_n) // 如果 元素个数 > bucket vector的大小,我们就要重建表格 { // 找出下一个质数,也就是待重建的表格大小 const size_type n = next_size(num_elements_hint); if (n > old_n) { cghVector<node*, Alloc> tmp(n, (node*)0); // 设立新的buckets // 处理每一个旧的bucket for (size_type bucket = 0; bucket < old_n; ++bucket) { node* first = buckets[bucket];// 旧的buckets中,每个元素(bucket)指向的list起点 while (first) // 循环遍历list { size_type new_bucket = bkt_num(first->val, n); // 找出节点落在哪一个新bucket内 buckets[bucket] = first->next; // 1.令就bucket指向其所对应的list的下一个节点 first->next = tmp[new_bucket]; //2、3.将当前节点插入到新bucket内,成为其list的第一个节点 tmp[new_bucket] = first; first = buckets[bucket]; // 4.回到就bucket所指向的list,准备处理下一个节点 } } buckets.swap(tmp); // 交换新旧buckets } // 离开时,tmp被释放 } } /* 不允许键值重复的插入 */ cghPair<iterator, bool> insert_unique_noresize(const value_type& obj) { const size_type n = bkt_num(obj); // 根据键值决定落在哪个bucket中 node* first = buckets[n]; // 令first指向bucket对应的list for (node* cur = first; cur; cur = cur->next) // 遍历list { if (equals(get_key(cur->val), get_key(obj))) // 如果发现键值重复 { return cghPair<iterator, bool>(iterator(cur, this), false); // 返回插入失败 } } node* tmp = new_node(obj); // 创建新节点 tmp->next = first; // 头插法:插入新节点到list头部 buckets[n] = tmp; ++num_elements; // 节点数加1 return cghPair<iterator, bool>(iterator(tmp, this), true); // 返回结果 } /* 允许键值重复的插入 */ iterator insert_equal_noresize(const value_type& obj) { const size_type n = bkt_num(obj); // 根据键值决定落在哪个bucket中 node* first = buckets[n]; // 令first指向bucket对应的list for (node* cur = first; cur; cur = cur->next) // 遍历list { if (equals(get_key(cur->val), get_key(obj))) // 如果发现键值重复 { node* tmp = new_node(obj); // 创建新节点 tmp->next = cur->next; // 头插法:插入新节点到cur的前面 cur->next = tmp; ++num_elements; // 节点数加1 return iterator(tmp, this); // 返回结果 } } node* tmp = new_node(obj); // 创建新节点 tmp->next = first; // 头插法:插入新节点到first的前面 buckets[n] = tmp; ++num_elements; // 节点数加1 return iterator(tmp, this); // 返回结果 } #pragma endregion #pragma endregion #pragma endregion #pragma region 给定键值,计算bucket /* 对于hash_table而言,最重要的就是确定key对应的bucket,这是hash faction的责任 我们把hash faction封装一层,由下面四个函数调用hash faction */ public: /* 接受实值和buckets个数 */ size_type bkt_num(const value_type& obj, size_type n) { return bkt_num_key(get_key(obj), n); } /* 只接受实值 */ size_type bkt_num(const value_type& obj) { return bkt_num_key(get_key(obj)); } /* 只接受键值 */ size_type bkt_num_key(const key_type& key) { return bkt_num_key(key, buckets.size()); } /* 接受键值和buckets个数 */ size_type bkt_num_key(const key_type& key, size_type n) { return hash(key) % n; // 调用hash faction,返回bucket } #pragma endregion }; static const int __stl_num_primes = 28; static const unsigned long __stl_prime_list[__stl_num_primes] = { 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741, 3221225473ul, 4294967291ul }; } #endif
3. 测试
测试环节的主要内容已在注释中说明
test_cgh_hash_table.cpp
/******************************************************************* * Copyright(c) 2016 Chen Gonghao * All rights reserved. * * [email protected] * * 文件内容:cgh_hash_table的测试 ******************************************************************/ #include "stdafx.h" #include "cgh_hash_table.h" using namespace::std; int _tmain(int argc, _TCHAR* argv[]) { using namespace::CGH; cgh_hash_table<int, int, std::hash<int>, std::identity<int>, std::equal_to<int>> test(50, std::hash<int>(), std::equal_to<int>()); std::cout << "**************** unique(不允许键值重复)插入测试 ****************" << endl << endl; std::cout << "依次插入:59,63,108,2,53,55" << endl << endl; test.insert_unique(59); test.insert_unique(63); test.insert_unique(108); test.insert_unique(2); test.insert_unique(53); test.insert_unique(55); std::cout << "遍历cgh_hash_table,打印插入结果:"; cgh_hash_table<int, int, std::hash<int>, std::identity<int>, std::equal_to<int>>::iterator it = test.begin(); cgh_hash_table<int, int, std::hash<int>, std::identity<int>, std::equal_to<int>>::iterator it2 = test.end(); for (int i = 0; i < test.size(); ++i, ++it) { cout << *it << ","; } std::cout << endl << endl << "遍历cgh_hash_table的所有bucket,如果节点个数不为0,就打印节点个数:" << endl << endl; for (int i = 0; i < test.bucket_count(); ++i) { int n = test.elems_in_bucket(i); if (n != 0) { cout << "bucket[" << i << "] 有 " << n << " 个节点" << endl; } } cout << endl << "cgh_hash_table的节点个数:" << test.size() << endl; cout << endl << "cgh_hash_table的bucket个数:" << test.bucket_count() << endl; cout << "-----------------------------" << endl << endl << endl; std::cout << "**************** equal(允许键值重复)插入测试 ****************" << endl << endl; std::cout << "在unique插入测试的基础上,继续插入:1,2,3,...,46,47" << endl << endl; for (int i = 0; i <= 47; ++i) { test.insert_equal(i); } std::cout << "遍历cgh_hash_table,打印插入结果:" << endl << endl; it = test.begin(); for (int i = 0; i < test.size(); ++i, ++it) { cout << *it << " "; if (i % 15 == 0 && i != 0) { cout << endl; } } cout << endl << endl << "cgh_hash_table的节点个数:" << test.size() << endl; cout << endl << "cgh_hash_table的bucket个数:" << test.bucket_count() << endl; std::cout << endl << endl << "遍历cgh_hash_table的所有bucket,如果节点个数不为0,就打印节点个数:" << endl << endl; for (int i = 0; i < test.bucket_count(); ++i) { int n = test.elems_in_bucket(i); if (n != 0) { cout << "bucket[" << i << "] 有 " << n << " 个节点" << endl; } } cout << "-----------------------------" << endl << endl << endl; system("pause"); return 0; }
结果如下所示:
可以看到,hash_table的插入非常均匀。
4.扩展讨论-- Hash与Map的区别
STL中的set和map采用红黑树的方式组织数据;hash_table采用散列的方法组织数据。
Set/map与hash_table各自的优势和应用场景有哪些呢?
有序性:采用红黑树,我们可以方便的得到最小值(向左走到底)和最大值(向右走到底),中序遍历红黑树可以得到key值由小到大的排列;而hash呈散列状,是一种无序状态,要获得最大最小值,或者按key值从小到大排列,我们需要做很多额外工作。
在实际的系统中,例如,需要使用动态规则的防火墙系统,使用红黑树而不是散列表被实践证明具有更好的伸缩性。Linux内核在管理vm_area_struct时就是采用了红黑树来维护内存块的。
如果只需要判断某个值是否存在之类的操作,当然是Hash实现的要更加高效。
如果是需要将两组数据求并集交集差集等大量比较操作,就是map/set更加高效。