STL简单hashtable的实现

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更加高效。

时间: 2024-10-10 03:52:08

STL简单hashtable的实现的相关文章

STL简单&lt;stl_algorithms.h&gt;算法的实现

1.简介 STL标准中,没有区分基本算法和复杂算法,然而SGI STL却把常用的算法定义在<stl_algorithms.h>中.本文介绍部分<stl_algorithms.h>算法的实现,给出实现代码和测试代码. 本文介绍的算法包括: 1.      mismatch:比较两个序列,指出两者之间第一个不匹配的点,返回一对迭代器,分别指向两序列中不匹配的点: 2.      equal:如果两个序列在 [first, last ] 区间内相等,equal() 返回true,忽略第二

STL的hashtable默认支持的模板类型

<STL源码剖析>的5.7.7 hash function一节中介绍了<stl_hash_fun.h>中定义了数个现成的hash函数,全都是仿函数.这些hash函数支持的模板类型包括:char*, const char*, char, unsigned char, signed char, short, unsigned short, int , unsigned int, long, unsigned long.这些不同类型的hash函数是通过对一下类模板实例化得到的: temp

STL简单 copy 算法的实现

1.简介 不论是对客户端或对STL内部而言,copy() 都是一个常常被调用的函数.由于copy进行的是复制操作,而复制操作不外乎运用赋值运算符(assignment operator)或复制构造函数(copy constructor),但是某些元素的类型是trivial assignment operator,因此如果能使用内存直接进行复制(例如使用C标准函数memmove.memcpy),便能节约大量时间.为此,copy算法用尽各种办法,包括函数重载(function overloading

11.STL简单set和multiset的实现

set的特性是所有元素都会根据键值自动排序,set的元素不像map那样同时拥有实值(value)和键值(key),set元素的键值就是实值,实值就是键值.Set不允许两个元素拥有相同的键值.不能通过迭代器修改set元素的值. multiset和set的唯一区别在于multiset允许键值重复. 我们采用红黑树作为set和multiset的底层数据结构,set和multiset的实现完完全全是在红黑树的基础上封装的一层接口,所有的set和multiset操作都转而调用红黑树的API.有关STL红黑

STL之hashtable源代码剖析

// Filename: stl_hashtable.h //////////////////////////////////////////////////////////////////////////////// // 本实作的hashtable採用的是开链法, 其内存布局例如以下 //////////////////////////////////////////////////////////////////////////////// // 对于产生哈希冲突的结点, 我们採取在其位置

STL之hashtable源码剖析

// Filename: stl_hashtable.h //////////////////////////////////////////////////////////////////////////////// // 本实作的hashtable采用的是开链法, 其内存布局如下 //////////////////////////////////////////////////////////////////////////////// // 对于产生哈希冲突的结点, 我们采取在其位置维护

9.STL简单binary heap的实现

我用VS2013写的程序(github ),queue版本的代码位于cghSTL/version/cghSTL-0.3.6.rar 所谓binary heap就是一种完全二叉树,也就是说,整颗binary tree除了对底层的叶节点外,是填满的,而最底层的叶节点由左至右不能有空隙. 完全二叉树内没有任何节点漏洞,这带来一个极大的好处:我们可以利用vector来存储所有节点.我们把vector的0号元素保留,那么当完全二叉树的某个节点位于vector的i处时,其左子树必然位于2i处,右子树必然位于

3.STL简单的迭代器实现(含源码)

我使用vs2015写的程序(源码下载) STL的中心思想在于将容器(container)和算法(algorithms)分开,彼此独立设计,最后再以一贴胶着剂将它们撮合在一起,而这个胶着剂就是迭代器(iterator). 迭代器是访问容器的工具.注意,先有容器,才有访问容器的工具.迭代器需要了解容器的特性才能实现,这决定了迭代器必须要深入到容器内部,于是STL干脆把迭代器的开发交给容器的设计者. 迭代器的开发分为三步: 1.      元素的设计: 2.      装载元素的容器的设计: 3.  

&lt;&lt;C++标准程序库&gt;&gt;中的STL简单学习笔记

0. 内容为个人学习笔记, 仅供参考, 如有错漏, 欢迎指正! 1. STL中的所有组件都是由模板构成的, 所以其元素可以是任意型别的. 组件有: - 容器: 管理某类对象的集合. 不同的容器有各自的优缺点. - 迭代器: 用来在一个对象集群(Collection of Objects) 的元素上进行遍历. 这个CoB可以是容器/容器的一部分. 每种容器都提供了自己的迭代器. - 算法(Algorithm): 用来处理集群内的元素(比如: 查询,修改,排序等). - 适配器(adapter) -