哈希详解以及实现(开放定址法和拉链法)

我们在平时使用的顺序结构(顺序表等)和平衡树中,元素的关键码和其存储位置之间没有对应关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序表的时间复杂度为O(N),平衡树的时间复杂度为为树的高度,即O(log2N),搜索的效率取决于搜索过程中元素的比较次数。
一种理想的搜索方法:可以不经过比较,一次直接得到要搜索的元素,如果构造一种存储结构,通过某种函数,使元素的存储位置和关键码能够建立一一映射的关系,那么在查找时,只需要通过该函数便可以很快得到该元素。
也就是说,在我们向该结构中插入或者搜索元素时,根据元素的关键码,通过某种函数去计算得到一个存储位置(哈希地址),然后直接用得到的位置来进行插入或者搜索等操作。这种方法就叫做哈希 Hash(散列),这个函数叫做哈希函数(HashFunc)。

假如现在有两个不同的关键码,通过一种哈希函数计算之后,得到了相同的哈希地址,这种现象叫做哈希冲突。
这种情况说明哈希函数设计的不够合理,哈希函数的设计原则如下:
1. 哈希函数的定义域必须包含全部要存储的关键码,如果哈希结构有m的地址时,其值域必须在0到m-1之间。
2. 哈希函数计算出来的地址能够均匀的分布在整个空间中。
3. 哈希函数要简单。

常见的几种哈希函数:

1. 直接定制法:
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
2. 除留余数法:
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
3. 平方取中法:
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。

开放定址法

开放定址法也叫闭散列,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
我们来看一个例子:假设现在有一个关键码集合{1,4,5,6,7,9},哈希结构的容量为10,哈希函数为 Hash(key)=key%10。将所有关键码插入到该哈希结构中,如图。
哈希详解以及实现(开放定址法和拉链法)_第1张图片

假如现在有一关键码24要插入该结构中,使用哈希函数求得哈希地址为4,但是该地址处已经存放了元素,此时发生哈希冲突。

线性探测:从发生哈希冲突位置开始,依次向后探测,直到找到下一个空位置为止。例如上面的例子,插入关键码24时,进行线性探测,插入后如下图。
哈希详解以及实现(开放定址法和拉链法)_第2张图片
 

 接下来我们采用除留余数法来实现开放定址法的哈希,在实现之前我们先搞清楚几个问题
1. 用该方法需要关键码必须为整型才能被模,所以我们需要实现将非整型转化为整型。
2. 模的数值最好为素数,需要我们创建一个素数表。
3. 增容问题。
散列表的载荷因子定义为: α=填入表中的元素个数/散列表的长度。
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大:反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散表的平均查找长度是载荷因子α的函数。只是不同处理冲突的方法有不同的函数。
对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将resize散列表。

HashFunc.h:
 

#pragma once
#include
#include
using namespace std;
template
class HashFunc   //使用仿函数实现
{
public:
	size_t operator()(const K& key)
	{
		return key;
	}
};
template<>
class HashFunc   //特化处理string类型关键码
{
public:
	size_t operator()(const string& str)
	{
		return BKDRHash(str.c_str());
	}

	size_t BKDRHash(const char * str)
	{
		unsigned int seed = 131; // 31 131 1313 13131 131313
		unsigned int hash = 0;
		while (*str)
		{
			hash = hash * seed + (*str++);
		}
		return (hash & 0x7FFFFFFF);
	}
};
// 素数表的定义
const int PRIMECOUNT = 28;       
const size_t primeList[PRIMECOUNT] =
{
	53ul, 97ul, 193ul, 389ul, 769ul,
	1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
	49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
	1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
	50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
	1610612741ul, 3221225473ul, 4294967291ul
};

 在哈希表中的每一个存储位置,应该包含两个因子,一个是要存储的数据,一个是存储状态。存储状态分为,空EMPTY,已存储EXIT,已删除DELETE。因为我们在删除数据时,不能直接删除,否则会影响其他元素的搜索(因为使用线性探测来处理哈希冲突的元素,例如上面的例子:删除元素4之后可能会影响元素24的搜索)

HashTable.h:
 

#include
#include
#include"HashFunc.h"
using namespace std;
enum State   //三种状态
{
	EMPTY,
	EXIT,
	DELETE
};
template  //每个存储位置的结构体
struct Elem
{
	pair kv;
	State state;
};
template>
class HashTable
{
public:
	HashTable(size_t size)
		: _size(0)
	{
		ht.resize(size);
		for (size_t i = 0; i < size; i++)  //初始化给每个存储位置设置空状态
		{
			ht[i].state = EMPTY;
		}
	}
	bool insert(const pair& _kv)
	{
		CheckCapacity();    //检测容量是否需要增容
		size_t index = _HashFunc(_kv.first);//通过哈希函数获取哈希地址
		while (ht[index].state != EMPTY)
		{
			if (ht[index].state == EXIT&&ht[index].kv.first == _kv.first)
			{
				return false;
			}
			index++;
			if (index == ht.size())  //如果哈希地址为最后一个存储位置时还不为空,从头开始寻找
			{
				index = 0;
			}
		}
		ht[index].kv = _kv;
		ht[index].state = EXIT;
		_size++;
		return true;
	}
	int find(const K& key)
	{
		size_t index = _HashFunc(key);
		while (ht[index].state != EMPTY)
		{
			if (ht[index].state == EXIT&&ht[index].kv.first == key)
			{
				return index;
			}
			index++;
			if (index == ht.size())
			{
				index = 0;
			}
		}
		return -1;
	}
	bool erase(const K& key)
	{
		int index = find(key);
		if (-1 == index)
		{
			return false;
		}
		ht[index].state = DELETE;
		_size--;
		return true;
	}
	size_t size() const
	{
		return _size;
	}
	bool empty() const
	{
		return _size == 0;
	}
private:
	size_t _HashFunc(const K& key)   //哈希函数
	{
		return HashFunc()(key) % ht.size(); //通过仿函数来获得整型关键码。
	}                                          //然后取余数作为哈希地址
	void Swap(HashTable& _ht)
	{
		swap(ht, _ht.ht);
		swap(_size, _ht._size);
	}

    //增容时需要用到,获取下一个比当前容量大的素数作为新的容量
	size_t GetNextPrime(size_t prime)   
	{
		size_t i = 0;
		for (; i < PRIMECOUNT; ++i)
		{
			if (primeList[i] > prime)
				return primeList[i];
		}

		return primeList[i];
	}
	void CheckCapacity()
	{
		if (_size * 10 / ht.size() > 7)
		{
			HashTable newht(GetNextPrime(ht.size())); //新建一个容量大的哈希表
			for (size_t i = 0; i < ht.size(); i++)
			{
				if (ht[i].state == EXIT)
				{
					newht.insert(ht[i].kv);  //将原来的哈希表中的元素插入到新哈希表中
				}
			}
			Swap(newht);   //交换两个哈希表
		}
	}
	vector> ht;
	size_t _size;
};
void testHashTable()
{
	HashTable HT(3);
	HT.insert(make_pair(1, 1));
	HT.insert(make_pair(5, 5));
	HT.insert(make_pair(2, 2));
	HT.insert(make_pair(4, 4));
	HT.insert(make_pair(3, 3));
	HT.find(2);
	HT.erase(1);
}

 

拉链法

拉链法又叫开散列,首先将关键码根据哈希函数来计算出哈希地址,对相同的哈希地址放在某一子集合中,每个子集合叫做一个桶,每个通放的都是哈希冲突元素,每个桶中的元素通过单链表连接,每个链表的头节点存放在哈希表中,这种结构叫做哈希桶。

哈希详解以及实现(开放定址法和拉链法)_第3张图片
哈希详解以及实现(开放定址法和拉链法)_第4张图片

HashFunc.h
 

#pragma once
#include
#include
using namespace std;
template
class HashFunc
{
public:
	size_t operator()(const K& key)
	{
		return key;
	}
};
template<>
class HashFunc
{
public:
	size_t operator()(const string& str)
	{
		return BKDRHash(str.c_str());
	}

	size_t BKDRHash(const char * str)
	{
		unsigned int seed = 131; // 31 131 1313 13131 131313
		unsigned int hash = 0;
		while (*str)
		{
			hash = hash * seed + (*str++);
		}
		return (hash & 0x7FFFFFFF);
	}
};
const int PRIMECOUNT = 28;
const size_t primeList[PRIMECOUNT] =
{
	53ul, 97ul, 193ul, 389ul, 769ul,
	1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
	49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
	1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
	50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
	1610612741ul, 3221225473ul, 4294967291ul
};

HashBucket.h
 

#include
#include
#include"HashFunc.h"
using namespace std;

template
struct Node
{
	pair kv;
	Node* next = nullptr;
};
template>
class HashBucket
{
public:
	HashBucket(size_t size = 10)
		:_size(0)
	{
		hb.resize(size, nullptr); //初始化给每个桶设置为空
	}
	Node* insert(const pair& _kv)
	{
		CheckCapacity();
		size_t bucket = _HashFunc(_kv.first);
		Node* cur = hb[bucket];
		while (cur)
		{
			if (cur->kv.first == _kv.first)
			{
				return cur;
			}
			cur = cur->next;
		}
		cur = new Node;  //插入的时候选择头插,减少遍历的时间
		cur->kv = _kv;
		cur->next = hb[bucket];
		hb[bucket] = cur;
		_size++;
		return cur;
	}
	Node* find(const K& key)
	{
		size_t bucket = _HashFunc(key);
		Node* cur = hb[bucket];
		while (cur)
		{
			if (cur->kv.first == key)
			{
				return cur;
			}
			cur = cur->next;
		}
		return nullptr;
	}
	Node* erase(const K& key)
	{
		size_t bucket = _HashFunc(key);
		Node* prev = nullptr;
		Node* cur = hb[bucket];
		Node* result = nullptr;
		while (cur)
		{
			if (cur->kv.first == key)
			{
				if (hb[bucket] == cur)
				{
					hb[bucket] = cur->next;
				}
				else
				{
					prev->next = cur->next;
				}
				result = cur->next;
				delete cur;
				_size--;
				return result;

			}
			prev = cur;
			cur = cur->next;
		}
		return nullptr;
	}
	size_t size() const
	{
		return _size;
	}
	bool empty() const
	{
		return _size == 0;
	}
	~HashBucket()
	{
		clear();
	}
private:
	size_t _HashFunc(const K& key)
	{
        //通过仿函数获取整型key值,然后去余数作为哈希地址
		return HashFunc()(key) % hb.size();  
	}
	void clear()
	{
		for (size_t i = 0; i < hb.size(); i++)
		{
			Node* cur = hb[i];
			while (cur)
			{
				hb[i] = cur->next;
				delete cur;
				cur = hb[i];
			}
		}
		_size = 0;
	}
	void Swap(HashBucket& _hb)
	{
		swap(hb, _hb.hb);
		swap(_size, _hb._size);
	}
	size_t GetNextPrime(size_t prime)
	{
		size_t i = 0;
		for (; i < PRIMECOUNT; ++i)
		{
			if (primeList[i] > prime)
				return primeList[i];
		}

		return primeList[i];
	}
	void CheckCapacity()
	{
		if (_size == hb.size())
		{
            //增容的时候是新建一个哈希桶,然后将原来的哈希桶的节点拆下来放到新的哈希桶中,即不建    
            //立新的节点。
			HashBucket newhb(GetNextPrime(hb.size()));

			for (size_t i = 0; i < hb.size(); i++)
			{
				Node* cur = hb[i];
				while (cur)
				{
					hb[i] = cur->next;
					//计算该元素在新的哈希桶中的位置
					size_t bucket = newhb._HashFunc(cur->kv.first);
					Node* pos = newhb.hb[bucket];
					while (pos)
					{
						if (pos->kv.first == cur->kv.first)
						{
							break;
						}
						pos = pos->next;
					}
					if (nullptr == pos)
					{
						cur->next = newhb.hb[bucket];
						newhb.hb[bucket] = cur;
					}
					else
					{
						cur->next = pos->next;
						pos->next = cur;
					}
					cur = hb[i];
				}
			}
			newhb._size = _size;
			Swap(newhb);
		}
	}
private:
	vector*> hb;
	size_t _size;
};

void testHashBucket()
{
	HashBucket HB(3);
	HB.insert(make_pair(2, 2));
	HB.insert(make_pair(4, 4));
	HB.insert(make_pair(5, 5));
	HB.insert(make_pair(1, 1));
	HB.insert(make_pair(3, 3));
	HB.find(5);
	HB.erase(3);

}

 

你可能感兴趣的:(C++)