C++进阶:哈希结构

哈希相关概念

哈希:用来进行高效查找的一种数据结构
首先,我们查找的方式有:
(1)顺序查找,它的时间复杂度是O(n)
(2)二分查找(有序),它的时间复杂度是O(log2N)
(3)利用二叉平衡搜索树(AVL、红黑树):时间复杂度是O(log2N)
(4)哈希,时间复杂度是O(1)
前三种方法元素之间都要进行比较,因此时间复杂度降不下来,而哈希元素之间不需要比较(最差情况下只需少许比较即可找到),因此时间复杂度小。
那么哈希的原理就是通过某种方式,将元素与其在空间中的位置建立一一对应的关系,例如:
给一个容量为capacity的空间,按照func(x)=x%capacity的方法来存储元素,比如存储21、67、90、33、5,func(21)=21%10=1,也就是21放在1号位置;func(67)=67%10=7,放在7号位置,func(90)=0,放在0号位置,func(33)=3,放在3号位置,func(5)=5,放在5号位置,这样就把元素放好了,也就是一个表格;
然后进行查找,第一步就是通过func(x)找元素在表格中的存储位置,然后验证是否为所找的元素即可;
这种思想就是哈希的思想,也可以称作为散列,func(x)称为哈希函数,建立的表格就是哈希表。
但是这种方法有一个缺陷,例如向这个哈希表中放入一个11,func(11)=1,应该将11放在1号位置,而这时1号位置已经放了一个元素,这时如果再向里面放元素就发生了覆盖(冲突),也就是不同的元素计算出相同的哈希地址,这种情况称为哈希冲突
哈希冲突的解决方式
(1)哈希函数可能会导致哈希冲突:可以重新设计哈希函数:
注意:哈希地址必须在哈希表格的范围内,产生的哈希地址尽可能的均匀分布,哈希函数尽可能简单;但是一个哈希函数无论设计的多精妙,都无法完全解决哈希冲突,只能将发生哈希冲突的概率降低。
(2)存放的元素
常见的哈希函数:
(1)直接定值法:Hash(key)=A*key+B,它的优点是简单和均匀,缺点是事先要知道关键字的分布,适用于查找比较小连续的情况,例如在字符串中找第一个只出现一次的字符

class Solution {
public:
    int firstUniqChar(string s) {
        int count[256]={0};//一个字符有256种状态
        //统计每个字符出现的次数
        for(int i=0;i

(2)除留余数法,也就是上述我们所说的func(x)=x%capacity的方法,也就是Hash(key)=key%p(如果p是素数,出现哈希冲突的概率较低)
(3)平方取中法(了解)例如关键字是1234,对他取平方得到1522756,取227作为他的关键字
(4)折叠法(5)随机数法(6)数学分析法

解决哈希冲突的方法
  • 闭散列
    也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,那么如何找到下一个空位置呢?
    1、线性探测
    就是从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
    那么怎么区分要插入的位置是否存在元素或者为空?
    我们可以给个标记:EMPTY表示没有元素,EXIST表示有元素
    如图:
    C++进阶:哈希结构_第1张图片
    (1)插入:
    通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素,如上图插入44,44%10=4,但是4这个位置已经放了值为4的元素,因此要向后找下一个空位置插入
    (2)查找:通过哈希函数计算元素在哈希表中的位置,检测该位置是否有元素,然后检测该元素是否为要查找元素
    (3)删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索,例如上图如果将4删除,将标记改为EMPTY,会影响44的搜索,导致44找不到(44的哈希函数计算出来应该在4位置,但是此时4位置标记没有元素,所以会影响)因此不能这样进行删除,因此线性探测采用标记的伪删除法来删除一个元素,也就是使用DELETE标记这个位置删除了元素
    当删除一个元素,理论上这个位置就可以插入元素了,但是有特殊情况,例如上图,将5删除,此时如果要插入44,按理来说可以插到5这个位置,但是此时哈希表中已经有44这个元素,就会冲突(如果哈希表要求元素唯一性)
    (4)判断是否增容?
    那么在插入操作时,什么时候增容,怎么增容,这里涉及到散列表的负载因子(填入表中的个数/散列表的长度),负载因子越大,表示填入表中的元素越多,产生冲突的可能性就越大;负载因子越小,填入表中的元素越少,产生冲突的可能性就越小;
    对于线性探测,负载因子一般控制在0.7到0.8之间,这里我们实现增容将负载因子控制在0.7,在进行扩容时,我们不能使用传统的方法进行扩容(传统的是将容量扩大,然后原封不动地将元素拷贝过来),因为在容量发生改变后,哈希函数也会发生改变,这样在扩容之后,就找不到元素了,因此我们必须再创建一个哈希表,然后设置它的容量是原来哈希表的容量的扩大版(我们这里是扩大2倍),然后将原来哈希表中存在的元素(即状态是EXIST的元素)插入到新的哈希表中,然后将2个哈希表交换即可(这里实现交换函数先交换内容,再交换有效元素个数)
    最终的代码是:
#pragma once
#include 
using namespace std;
#include 
//假设哈希表格中的元素是唯一的
enum State
{
	EMPTY, EXIST, DELETE
};
template
struct Elem
{
	T _value;//元素值域
	State _state;//状态
};
template
class HashTable
{
public:
	HashTable(size_t capacity = 10)
		:_ht(capacity)
		, _size(0)
	{
		for (auto& e : _ht)
		{
			e._state = EMPTY;//将表格的初始状态初始化为空
		}
	}
	bool Insert(const T& val)
	{
		//检测是否需要扩容
		CheckCapacity();

		//通过哈希函数计算元素在哈希表中的存储位置
		size_t HashAddr = HashFunc(val);
		//检测该位置是否可以插入元素
		//发生哈希冲突,使用线性探测来解决
		while (_ht[HashAddr]._state != EMPTY)
		{
			if (EXIST == _ht[HashAddr]._state && val == _ht[HashAddr]._value)
			{
				//就不用插入了,冲突
				return false;
			}
			//使用线性探测继续往后找,直到找到空位
			++HashAddr;
			if (HashAddr == _ht.capacity())
				HashAddr = 0;//如果找到最后一个还没有找到空位,从头开始
		}
		//肯定不会让哈希表中的元素放的太多,因为发生冲突的概率会提高,这样哈希表查找的效率就会降低
		//所以不用考虑造成死循环的情况
		//找到空的位置,进行插入
		_ht[HashAddr]._value = val;
		_ht[HashAddr]._state = EXIST;
		++_size;
		return true;
	}
	int Find(const T& val)
	{
		size_t HashAddr = HashFunc(val);//计算哈希地址
		while (_ht[HashAddr]._state != EMPTY)//这个位置可能有元素
		{
			if (_ht[HashAddr]._state == EXIST && _ht[HashAddr]._value == val)
			{
				return HashAddr;
			}
			//如果这个位置是删除或者不等于要找的值,就哈希冲突,线性探测
			HashAddr++;
			if (HashAddr == _ht.capacity())
				HashAddr = 0;
		}
		return -1;//没有这个元素
	}
	bool Erase(const T& val)
	{
		int index = Find(val);//在哈希表中找这个元素的位置
		if (index != -1)//找到了
		{
			_ht[index]._state = DELETE;
			_size--;
			return true;
		}
		return false;
	}
	void Swap(HashTable& ht)
	{
		_ht.swap(ht._ht);//先交换内容
		swap(_size, ht._size);
	}
private:
	size_t HashFunc(const T& val)
	{
		return val%_ht.capacity();
	}
	void CheckCapacity()
	{
		//有效元素与容量的比率称为负载因子,因为_size/_ht.capacity()永远是0,因为都是整形,所以给_size乘10/容量>7即可
		if (_size*10 / _ht.capacity() >= 7)//需要扩容
		{
			//无法使用原来的哪种方法扩容,因为现在哈希函数是val%容量,扩容使容量发生改变,哈希函数也就会变化,
			//如果采用原来的方法将元素进行搬移,可能导致元素找不到了,因此要重新找一种方法来扩容
			//涉及两个问题:元素怎么搬移和搬移哪些元素(只搬移状态为存在的元素)
			//(1)构造新的哈希表,将容量给为新容量(这里是原容量的2倍)
			HashTable newHT(_ht.capacity() * 2);
			//(2)将原哈希表中状态为存在的元素插入到新的哈希表中
			for (size_t i = 0; i < _ht.capacity(); ++i)
			{
				if (_ht[i]._state == EXIST)
					newHT.Insert(_ht[i]._value);
			}
			//(3)交换两个哈希表
			Swap(newHT);
		}
	}
private:
	std::vector> _ht;
	size_t _size;//哈希表中有效元素个数
};
void TesthashTable()
{
	HashTable ht;
	ht.Insert(4);
	ht.Insert(7);
	ht.Insert(8);
	ht.Insert(27);
	ht.Insert(9);
	ht.Insert(5);
	ht.Insert(3);
	ht.Insert(1);

	ht.Erase(2);
	ht.Erase(8);
}

线性探测的优点:实现比较简单
线性探测的缺点:容易产生数据堆积,一旦发生冲突,发生冲突的元素可能会连在一起,因为线性探测解决哈希冲突的方式是从发生哈希冲突的位置挨着往后逐次找的。
线性探测解决哈希冲突:源代码(github)
https://github.com/wangbiy/C-3/commit/39295e148283facd9865e395f476f484ce241ba2
2、二次探测
由于线性探测的缺陷是产生的数据堆积在一起,是因为解决哈希冲突的方式是从发生冲突的位置逐次往后找的,因此我们从这里着手,二次探测为了避免这个问题,找下一个空位置的方法是:比如H0是第一次计算出的哈希地址,Hi代表第i次的哈希地址,Hi=H0+i ^ 2或者Hi=H0-i ^ 2
我们可以利用数学方法将第i+1次的哈希地址算出来,Hi=H0+i ^2,Hi+1=H0+(i+1) ^2,Hi+1-Hi=2i+1,则Hi+1=Hi+2i+1,采用二次探测来解决的话,如果越界不采用线性探测直接将哈希地址置为0的方法,而是只要让哈希地址取模容量即可,这样结果肯定不一样。
线性探测和二次探测多必须考虑负载因子,超过0.7-0.8就增容,增大效率,其中删除是惰性删除,即只标记删除记号。
即实现代码:

bool Insert(const T& val)
	{
		//检测是否需要扩容
		CheckCapacity();
		//通过哈希函数计算元素在哈希表中的存储位置
		size_t HashAddr = HashFunc(val);
		int i = 0;//代表第i次探测
		//检测该位置是否可以插入元素
		//发生哈希冲突,使用二次探测来解决
		while (_ht[HashAddr]._state != EMPTY)
		{
			if (EXIST == _ht[HashAddr]._state && val == _ht[HashAddr]._value)
			{
				//就不用插入了,冲突
				return false;
			}
			i++;
			if (IsLine)//使用线性探测
			{
				++HashAddr;
				if (HashAddr == _ht.capacity())
					HashAddr = 0;//如果找到最后一个还没有找到空位,从头开始
			}	
			else//使用二次探测
			{
				HashAddr = HashAddr + 2 * i + 1;//利用数学方法
				//不能直接使用线性探测越界的方法,因为很有可能造成死循环
				HashAddr %= _ht.capacity();//我们采用这种取模的方式来解决越界,这样每次的取模的结果都是不一样的
			}
			
		}
		//肯定不会让哈希表中的元素放的太多,因为发生冲突的概率会提高,这样哈希表查找的效率就会降低
		//所以不用考虑造成死循环的情况
		//找到空的位置,进行插入
		_ht[HashAddr]._value = val;
		_ht[HashAddr]._state = EXIST;
		++_size;
		return true;
	}
	int Find(const T& val)
	{
		size_t HashAddr = HashFunc(val);//计算哈希地址
		int i = 0;
		while (_ht[HashAddr]._state != EMPTY)//这个位置可能有元素
		{
			if (_ht[HashAddr]._state == EXIST && _ht[HashAddr]._value == val)
			{
				return HashAddr;
			}
			i++;
			if (IsLine)//使用线性探测
			{
				++HashAddr;
				if (HashAddr == _ht.capacity())
					HashAddr = 0;//如果找到最后一个还没有找到空位,从头开始
			}
			else//使用二次探测
			{
				HashAddr = HashAddr + 2 * i + 1;//利用数学方法
				//不能直接使用线性探测越界的方法,因为很有可能造成死循环
				HashAddr %= _ht.capacity();//我们采用这种取模的方式来解决越界,这样每次的取模的结果都是不一样的
			}
		}
		return -1;//没有这个元素
	}

在进行插入和查找时判断使用线性探测还是二次探测,即多增加一个IsLine类型来判断。
但是二次探测的缺陷是0000000,让他的负载因子再小一点即可。
闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷

  • 开散列
    开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶(哈希桶),各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
    开散列中每个桶中放的都是发生哈希冲突的元素,这里哈希桶中放的是链表第一个结点的地址(元素的类型就是Node*)。
    但是如果每个桶是单链表的结构,桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此,我们的哈希桶可以用红黑树实现,这样的效率就比较高,会缓解这种极端的压力(但是一般不使用这种方式),又或者我们可以对当前的哈希表进行扩容,那么我们在什么时机进行增容呢?开散列最优的情况是每个哈希桶中刚好挂一个元素,再进行插入元素时,每一次都会发生冲突,因此,在元素数刚好等于哈希桶的个数时,就开始给哈希表增容。
    开散列的实现:
#pragma once
#include 
using namespace std;
#include 
template 
//默认哈希表中的元素是唯一的
struct HBNode
{
	T _data;
	HBNode* _pNext;
	HBNode(const T& data)
		:_data(data)
		, _pNext(nullptr)
	{}
};
template 
class HashBucket
{
public:
	HashBucket(size_t capacity)
		:_table(capacity,nullptr)
		, _size(0)
	{}
	~HashBucket()
	{
		clear();
	}
	bool Insert(const T& data)
	{
		CheckCapacity();
		size_t bucketNo = HashFunc(data);//计算桶号,即第一个结点
		//检测该元素是否在桶中
		HBNode* pCur = _table[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return false;
			pCur = pCur->_pNext;
		}
		//插入元素
		pCur = new HBNode(data);
		//头插,性能比较高
		pCur->_pNext = _table[bucketNo];
		_table[bucketNo] = pCur;
		++_size;
		return true;
	}
	HBNode* Find(const T& data)
	{
		size_t bucketNo = HashFunc(data);
		HBNode* pCur = _table[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return pCur;
			pCur = pCur->_pNext;
		}
		return nullptr;
	}
	bool Erase(const T& data)
	{
		size_t bucketNo = HashFunc(data);
		HBNode* pCur = _table[bucketNo];
		HBNode* pPre = nullptr;
		while (pCur)
		{
			if (pCur->_data == data)//找到了想要删除的数
			{
				if (pCur == _table[bucketNo])//如果想要删的数的位置在这个桶的第一个结点,也就是_table[bucketNo]
				{
					_table[bucketNo] = pCur->_pNext;//删除的是第一个结点,也就是头删
				}
				else//删除的不是第一个结点,就是任意位置删除
				{
					pPre->_pNext = pCur->_pNext;
				}
				delete pCur;
				--_size;
				return true;
			}
			pPre = pCur;
			pCur = pCur->_pNext;
		}
		return false;
	}
	size_t Size()const
	{
		return _size;
	}
	void Swap(HashBucket& hb)
	{
		_table.swap(hb._table);
		swap(_size, hb._size);
	}
	void clear()
	{
		for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
		{
			HBNode* pCur = _table[bucketNo];
			while (pCur)
			{
				//头删
				_table[bucketNo] = pCur->_pNext;
				delete pCur;
				pCur = _table[bucketNo];
			}
		}
		_size = 0;
	}
	void Print()
	{
		for (size_t i = 0; i < _table.capacity(); ++i)
		{
			cout << "H[" << i << "]" << ":";
			HBNode* pCur = _table[i];
			while (pCur)
			{
				cout << pCur->_data << "----->";
				pCur = pCur->_pNext;
			}
			cout << "NULL" << endl;
		}
	}
private:
	//哈希函数
	size_t HashFunc(const T& data)
	{
		return data % _table.capacity();
	}
	void CheckCapacity()
	{
		if (_size == _table.capacity())
		{
			HashBucket newHB(_size * 2);
			for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
			{
				HBNode* pCur = _table[bucketNo];
				但是这种方式不太好,效率比较低,因为又重新创建了结点
				//while (pCur)
				//{
				//	newHB.Insert(pCur->_data);
				//	pCur = pCur->_pNext;
				//}
				while (pCur)
				{
					//1、计算当前结点在新哈希桶中的新桶号
					size_t newbucketNo = newHB.HashFunc(pCur->_data);
					//2、将结点从哈希表的哈希桶中拆下来
					_table[bucketNo] = pCur->_pNext;//将pCur拿出来
					//头插
					pCur->_pNext = newHB._table[newbucketNo];
					newHB._table[newbucketNo] = pCur;
					pCur = _table[bucketNo];//让pCur到原哈希桶的下一个节点
				}
			}
			Swap(newHB);
		}
	}
private:
	vector*> _table;//哈希表
	size_t _size;
};

但是这个还是有缺陷,由于我们的哈希函数使用的是除留余数法,最好模一个素数,因此如果哈希函数中快速找到具有两倍关系的素数,这样的就比较高效,还有一个问题就是我们实现的开散列只能存放整形元素,现在对他进行改造让哈希桶可以存储任意类型的元素
(1)我们使用一个比当前元素的两倍的素数的集合即可,即common.cpp实现这一功能
(2)我们想要存储任意类型的元素,可以使用仿函数,最终的代码就是:
common.h

#pragma once
size_t GetNextPrime(size_t prime);//获得prime的下一个素数

common.cpp

#include "common.h"
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
};
size_t GetNextPrime(size_t prime)
{
	size_t i = 0;
	for (; i < PRIMECOUNT; ++i)
	{
		if (primeList[i] > prime)
			return primeList[i];
	}
	return primeList[PRIMECOUNT-1];
}

HashBucket.hpp

#pragma once
#include "common.h"
#include 
using namespace std;
#include 
#include 
template 
//默认哈希表中的元素是唯一的
struct HBNode
{
	T _data;
	HBNode* _pNext;
	HBNode(const T& data)
		:_data(data)
		, _pNext(nullptr)
	{}
};
//T--->整形系列
template 
class DFDef//实现仿函数
{
public:
	T operator()(const T& data)
	{
		return data;
	}
};
//T--->string
size_t BKDRHash(const char* str)
{
	register size_t hash = 0;
	while (size_t ch = (size_t)*str++)
	{
		hash = hash * 131 + ch;
	}
	return hash;
}
class StringToINT
{
public:
	size_t operator()(const string& s)
	{
		return BKDRHash(s.c_str());//直接返回地址
	}
};
template >//HF设置为按照默认为整数的方式来处理
class HashBucket
{
public:
	HashBucket(size_t capacity=10)
		:_table(GetNextPrime(capacity), nullptr)
		, _size(0)
	{}
	~HashBucket()
	{
		clear();
	}
	bool Insert(const T& data)
	{
		CheckCapacity();
		size_t bucketNo = HashFunc(data);//计算桶号,即第一个结点
		//检测该元素是否在桶中
		HBNode* pCur = _table[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return false;
			pCur = pCur->_pNext;
		}
		//插入元素
		pCur = new HBNode(data);
		//头插,性能比较高
		pCur->_pNext = _table[bucketNo];
		_table[bucketNo] = pCur;
		++_size;
		return true;
	}
	HBNode* Find(const T& data)
	{
		size_t bucketNo = HashFunc(data);
		HBNode* pCur = _table[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return pCur;
			pCur = pCur->_pNext;
		}
		return nullptr;
	}
	bool Erase(const T& data)
	{
		size_t bucketNo = HashFunc(data);
		HBNode* pCur = _table[bucketNo];
		HBNode* pPre = nullptr;
		while (pCur)
		{
			if (pCur->_data == data)//找到了这个桶
			{
				if (pCur == _table[bucketNo])//找到了
				{
					_table[bucketNo] = pCur->_pNext;//删除的是第一个结点,也就是头删
				}
				else//删除的不是第一个结点,就是任意位置删除
				{
					pPre->_pNext = pCur->_pNext;
				}
				delete pCur;
				--_size;
				return true;
			}
			pPre = pCur;
			pCur = pCur->_pNext;
		}
		return false;
	}
	size_t Size()const
	{
		return _size;
	}
	void Swap(HashBucket& hb)
	{
		_table.swap(hb._table);
		swap(_size, hb._size);
	}
	void clear()
	{
		for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
		{
			HBNode* pCur = _table[bucketNo];
			while (pCur)
			{
				//头删
				_table[bucketNo] = pCur->_pNext;
				delete pCur;
				pCur = _table[bucketNo];
			}
		}
		_size = 0;
	}
	void Print()
	{
		for (size_t i = 0; i < _table.capacity(); ++i)
		{
			cout << "H[" << i << "]" << ":";
			HBNode* pCur = _table[i];
			while (pCur)
			{
				cout << pCur->_data << "----->";
				pCur = pCur->_pNext;
			}
			cout << "NULL" << endl;
		}
	}
private:
	//哈希函数
	size_t HashFunc(const T& data)
	{
		return HF()(data) % _table.capacity();//仿函数的方式
	}
	void CheckCapacity()
	{
		size_t oldCapacity = _table.capacity();
		if (_size == oldCapacity)
		{
			HashBucket newHB(GetNextPrime(oldCapacity));//改为两倍的素数关系
			for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
			{
				HBNode* pCur = _table[bucketNo];
				但是这种方式不太好,效率比较低,因为又重新创建了结点
				//while (pCur)
				//{
				//	newHB.Insert(pCur->_data);
				//	pCur = pCur->_pNext;
				//}
				while (pCur)
				{
					//1、计算当前结点在新哈希桶中的新桶号
					size_t newbucketNo = newHB.HashFunc(pCur->_data);
					//2、将结点从哈希表的哈希桶中拆下来
					_table[bucketNo] = pCur->_pNext;//将pCur拿出来
					//头插
					pCur->_pNext = newHB._table[newbucketNo];
					newHB._table[newbucketNo] = pCur;
					pCur = _table[bucketNo];//让pCur到原哈希桶的下一个节点
				}
			}
			Swap(newHB);
		}
	}
private:
	vector*> _table;//哈希表
	size_t _size;
};
void TestHashBucket1()
{
	HashBucket ht(10);
	ht.Insert(3);
	ht.Insert(8);
	ht.Insert(4);
	ht.Insert(0);
	ht.Insert(7);
	ht.Insert(13);
	ht.Insert(33);
	cout << ht.Size() << endl;
	ht.Print();

	ht.Erase(13);
	ht.Print();

	ht.Erase(3);
	if (nullptr == ht.Find(3))
		cout << "3 is not in" << endl;
	else
		cout << "3 is in" << endl;
	ht.clear();
	cout << ht.Size() << endl;
}
void TestHashBucket2()
{
	HashBucket ht;
	ht.Insert("hello");
	ht.Insert("CCP");
	ht.Insert("I");
	ht.Insert("Love");
	ht.Insert("You");
	ht.Print();
	cout << ht.Size() << endl;

	ht.Erase("I");
	ht.Print();

	ht.Erase("hello");
	if (nullptr == ht.Find("hello"))
	{
		cout << "hello is not in" << endl;
	}
	else
		cout << "hello is in" << endl;
	cout << ht.Size() << endl;
}

创建两个仿函数类,一个是默认的整形,一个是由字符串转为整形,使用提供的哈希方法BKDRHash来讲字符串转换为整形,然后增加一个模板类型HF,在哈希函数那里按照仿函数调用的方法将元素按照类型实现即可。
(3)接下来我们要给这个开散列增加迭代器操作
我们先封装迭代器类,然后实现即可

//实现迭代器的操作
template >//通过KeyOfValue实现通过key来获取value
class HashBucket;
template //通过KeyOfValue实现通过key来获取value
struct HBIterator
{
	typedef HBNode Node;
	typedef HBIterator Self;
public:
	HBIterator(Node* pNode,HashBucket* ht)//构造函数
		:_pNode(pNode)
		, _ht(ht)
	{}
	T& operator*()
	{
		return _pNode->_data;
	}
	T* operator->()
	{
		return &(operator*());
	}
	//迭代器移动,不能--,因为哈希桶的结构是单链表
	//遍历存在的桶,将一个桶的链表遍历完成后再遍历下一个桶的链表
	Self& operator++()//前置++
	{
		Next();
		return *this;
	}
	Self& operator++(int)
	{
		Self tmp(*this);
		Next();
		return tmp;
	}
	void Next()
	{
		if (_pNode->_pNext)//不为空,当前链表还没有处理完成
		{
			_pNode = _pNode->_pNext;
		}
		else//找下一个存在的桶
		{
			size_t bucketNo = _ht->HashFunc(_pNode->_data) + 1;//哈希函数是哈希桶类的私有成员函数,需要使用友元类
			for (; bucketNo < _ht->BucketCount(); ++bucketNo)//BucketCount表示桶的数量
			{
				if (_ht->_table[bucketNo])
				{
					_pNode = _ht->_table[bucketNo];
					return;
				}
			}
			_pNode = nullptr;
		}
	}
	bool operator!=(const Self& s)const
	{
		return _pNode != s._pNode && _ht == s._ht;//同一个哈希桶的不同结点
	}
	bool operator==(const Self& s)const
	{
		return !(*this != s);
	}
private:
	Node* _pNode;
	HashBucket* _ht;
};

分析:定义成员变量_pNode和哈希桶,(1)对指针的操作(operator* 和operator->)进行重载,然后进行迭代器的移动,最后实现迭代器的比较(记得是同一个哈希桶中结点的比较);(2)在哈希桶类中typedef实现的迭代器名 Iterator,然后实现Begin和End即可。
实现代码:

#pragma once
#include "common.h"
#include 
using namespace std;
#include 
#include 
template 
//默认哈希表中的元素是唯一的
struct HBNode
{
	T _data;
	HBNode* _pNext;
	HBNode(const T& data)
		:_data(data)
		, _pNext(nullptr)
	{}
};
//T--->整形系列
template 
class DFDef//实现仿函数
{
public:
	T operator()(const T& data)
	{
		return data;
	}
};
//T--->string
size_t BKDRHash(const char* str)
{
	register size_t hash = 0;
	while (size_t ch = (size_t)*str++)
	{
		hash = hash * 131 + ch;
	}
	return hash;
}
class StringToINT
{
public:
	size_t operator()(const string& s)
	{
		return BKDRHash(s.c_str());//直接返回地址
	}
};
//实现迭代器的操作
template >//通过KeyOfValue实现通过key来获取value
class HashBucket;
template //通过KeyOfValue实现通过key来获取value
struct HBIterator
{
	typedef HBNode Node;
	typedef HBIterator Self;
public:
	HBIterator(Node* pNode,HashBucket* ht)//构造函数
		:_pNode(pNode)
		, _ht(ht)
	{}
	T& operator*()
	{
		return _pNode->_data;
	}
	T* operator->()
	{
		return &(operator*());
	}
	//迭代器移动,不能--,因为哈希桶的结构是单链表
	//遍历存在的桶,将一个桶的链表遍历完成后再遍历下一个桶的链表
	Self& operator++()//前置++
	{
		Next();
		return *this;
	}
	Self& operator++(int)
	{
		Self tmp(*this);
		Next();
		return tmp;
	}
	void Next()
	{
		if (_pNode->_pNext)//不为空,当前链表还没有处理完成
		{
			_pNode = _pNode->_pNext;
		}
		else//找下一个存在的桶
		{
			size_t bucketNo = _ht->HashFunc(_pNode->_data) + 1;//哈希函数是哈希桶类的私有成员函数,需要使用友元类
			for (; bucketNo < _ht->BucketCount(); ++bucketNo)
			{
				if (_ht->_table[bucketNo])
				{
					_pNode = _ht->_table[bucketNo];
					return;
				}
			}
			_pNode = nullptr;
		}
	}
	bool operator!=(const Self& s)const
	{
		return _pNode != s._pNode && _ht == s._ht;//同一个哈希桶的不同结点
	}
	bool operator==(const Self& s)const
	{
		return !(*this != s);
	}
private:
	Node* _pNode;
	HashBucket* _ht;
};
template //HF设置为按照默认为整数的方式来处理
class HashBucket
{
	friend HBIterator;//是迭代器类的友元,迭代器类可以访问哈希桶类的私有成员
	typedef HashBucket Self;
public:
	typedef HBIterator Iterator;//给迭代器取别名
public:
	HashBucket(size_t capacity=10)
		:_table(GetNextPrime(capacity), nullptr)
		, _size(0)
	{}
	~HashBucket()
	{
		clear();
	}
	Iterator Begin()
	{
		for (size_t bucketNo = 0; bucketNo < BucketCount(); ++bucketNo)
		{
			if (_table[bucketNo])
				return Iterator(_table[bucketNo],this);
		}
		return End();
	}
	Iterator End()
	{
		return Iterator(nullptr, this);
	}
	bool Insert(const T& data)
	{
		CheckCapacity();
		size_t bucketNo = HashFunc(data);//计算桶号,即第一个结点
		//检测该元素是否在桶中
		HBNode* pCur = _table[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return false;
			pCur = pCur->_pNext;
		}
		//插入元素
		pCur = new HBNode(data);
		//头插,性能比较高
		pCur->_pNext = _table[bucketNo];
		_table[bucketNo] = pCur;
		++_size;
		return true;
	}
	HBNode* Find(const T& data)
	{
		size_t bucketNo = HashFunc(data);
		HBNode* pCur = _table[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return pCur;
			pCur = pCur->_pNext;
		}
		return nullptr;
	}
	bool Erase(const T& data)
	{
		size_t bucketNo = HashFunc(data);
		HBNode* pCur = _table[bucketNo];
		HBNode* pPre = nullptr;
		while (pCur)
		{
			if (pCur->_data == data)//找到了这个桶
			{
				if (pCur == _table[bucketNo])//找到了
				{
					_table[bucketNo] = pCur->_pNext;//删除的是第一个结点,也就是头删
				}
				else//删除的不是第一个结点,就是任意位置删除
				{
					pPre->_pNext = pCur->_pNext;
				}
				delete pCur;
				--_size;
				return true;
			}
			pPre = pCur;
			pCur = pCur->_pNext;
		}
		return false;
	}
	size_t Size()const
	{
		return _size;
	}
	size_t BucketCount()
	{
		return _table.capacity();
	}
	void Swap(HashBucket& hb)
	{
		_table.swap(hb._table);
		swap(_size, hb._size);
	}
	void clear()
	{
		for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
		{
			HBNode* pCur = _table[bucketNo];
			while (pCur)
			{
				//头删
				_table[bucketNo] = pCur->_pNext;
				delete pCur;
				pCur = _table[bucketNo];
			}
		}
		_size = 0;
	}
	void Print()
	{
		for (size_t i = 0; i < _table.capacity(); ++i)
		{
			cout << "H[" << i << "]" << ":";
			HBNode* pCur = _table[i];
			while (pCur)
			{
				cout << pCur->_data << "----->";
				pCur = pCur->_pNext;
			}
			cout << "NULL" << endl;
		}
	}
private:
	//哈希函数
	size_t HashFunc(const T& data)
	{
		//HF表示按照仿函数的方式来进行数据类型的实现,如果整形,就是默认的,否则就是字符串转换为整形的元素
		return HF()(data) % _table.capacity();//仿函数的方式
	}
	void CheckCapacity()
	{
		size_t oldCapacity = _table.capacity();
		if (_size == oldCapacity)
		{
			Self newHB(GetNextPrime(oldCapacity));//改为两倍的素数关系
			for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
			{
				HBNode* pCur = _table[bucketNo];
				但是这种方式不太好,效率比较低,因为又重新创建了结点
				//while (pCur)
				//{
				//	newHB.Insert(pCur->_data);
				//	pCur = pCur->_pNext;
				//}
				while (pCur)
				{
					//1、计算当前结点在新哈希桶中的新桶号
					size_t newbucketNo = newHB.HashFunc(pCur->_data);
					//2、将结点从哈希表的哈希桶中拆下来
					_table[bucketNo] = pCur->_pNext;//将pCur拿出来
					//头插
					pCur->_pNext = newHB._table[newbucketNo];
					newHB._table[newbucketNo] = pCur;
					pCur = _table[bucketNo];//让pCur到原哈希桶的下一个节点
				}
			}
			Swap(newHB);
		}
	}
private:
	vector*> _table;//哈希表
	size_t _size;
};
//这两个测试用例是没有增加KeyOfValue的方法时测试的
void TestHashBucket1()
{
	HashBucket ht(10);
	ht.Insert(3);
	ht.Insert(8);
	ht.Insert(4);
	ht.Insert(0);
	ht.Insert(7);
	ht.Insert(13);
	ht.Insert(33);
	ht.Insert(53);
	cout << ht.Size() << endl;
	auto it = ht.Begin();
	while (it != ht.End())
	{
		cout << *it<<" ";
		++it;
	}
	cout << endl;
	ht.Print();

	ht.Erase(13);
	ht.Print();

	ht.Erase(3);
	if (nullptr == ht.Find(3))
		cout << "3 is not in" << endl;
	else
		cout << "3 is in" << endl;
	ht.clear();
	cout << ht.Size() << endl;
}
void TestHashBucket2()
{
	HashBucket ht;
	ht.Insert("hello");
	ht.Insert("CCP");
	ht.Insert("I");
	ht.Insert("Love");
	ht.Insert("You");
	ht.Print();
	cout << ht.Size() << endl;

	ht.Erase("I");
	ht.Print();

	ht.Erase("hello");
	if (nullptr == ht.Find("hello"))
	{
		cout << "hello is not in" << endl;
	}
	else
		cout << "hello is in" << endl;
	cout << ht.Size() << endl;
}

这个就是哈希桶的实现,也就是开散列的总体实现。

下一节我们将使用这个哈希桶类封装unordered_map结构,此时实现的哈希桶并不能封装unordered_map和unordered_set,因为unordered_map是键值对结构,unordered_set是key结构,因此我们必须实现KeyOfValue的仿函数,在unordered_map中表示从value中提取出key,在unordered_set表示单个key。

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