数据结构:散列表(哈希表)开放定址法原理及C++实现

前言:

        最近写了下RabinKarp算法,当中使用到了一定的Hash方法,我顺就复习了一下Hash表的实现原理,要不然感觉自己都要忘记了~~

原理:

散列表:
        散列表也称哈希表,是一种键值对应的数据结构(key-value),也就是说任何一个存储在Hash表中的 数据(value)都会有一个对应的 键值(key)用来存储它。我们可以这样来理解他:在我们去电影院看电影的时候,我们显然是得买电影票的,而电影票上面由我们的座位号,看电影的时候我们就需要对号入座;在这里我们可以把人看成是数据(value),电影院中的座位则是我们的键(key),可以发现每个人都有一个特定的作为与之对应,这就是Hash的思想。
         对于开放定址法实现的Hash表来说,所有的数据都储存在一个结构体数组TheCells中,这个数组中的每一个元素都是Hash结构体,当中储存了该结构储存的元素,也就是我们的数据(value),同时它还储存了我们该结构体的状态,分别是:空,使用的,删除的,这么做的原因是因为我们将采用懒惰删除的方法实现元素的删除操作。


查找&插入:
        那么作为一个数据结构,它最重要的操作显然是插入或者查找,对于Hash表来说,查找和插入是至关重要的操作,因为他会预先对一个将要储存的数据进行处理,查找其对应的位置。对于一个Hash表,我们将知道这个 Hash表的大小TableSize,也就是其结构体数组的大小。那我对于我们存入其中的元素,我们将对其进行 取值(Element)的操作。
 1. 若储存数据为int型,那么它的值Element就是他本身;

2. 若为string类型,那么值就是就是每个字符对应的ASCII码之和;
3. 若为其它类型,也可以根据自己的需要定义;

        那么当我们得到了对应元素的Element,我们应该将其存储在哪个地方呢?有一种简单的方法是将其存储的位置,也就是对应的键值key = Element % TableSize,这样既可以找到对应的位置,同时也保证了key在数组的存储范围之内。但是这也会遇到一些问题,当我们的TableSize不为素数的时候,我们会发现key值很容易重复,一个解决方法就是将TableSize申请的时候就设置为素数,这是这一部分的实现代码:
/* 判断素数函数:判断输入值是否为素数
 * 返回值:bool型:素数返回true,否则返回false
 * 参数:num:需要判断的int型输入
 */
bool isPrime(int num) {
	if (num == 2 || num == 3) // 判断是否可以被2或者3整除
		return true;
	if (num % 6 != 1 && num % 6 != 5) // 除余数为1和5以外都可以被2,3整除
		return false;
	for (int i = 5; i*i < num; i += 6) // 判断是否可以被余数为1或5的数整除
		if (num % i == 0 || num % (i + 2) == 0)
			return false;
	return true;
}

/* 下一个素数函数:得到输入值的下一个素数
 * 返回值:int型:下一给素数
 * 参数:num:用于比较的初位素数
 */
int NextPrime(int num) {
	int n = num + 1;
	while (!isPrime(n)) // 判断下一位是否为素数
	{
		n++;
	}
	return n;
}

上诉代码用来处理素数问题,它提供了选择素数以及判断素数的方法,其中判断素数的方法大家可以看一看,我认为是比较高效的。那么除此以外我们还需要对结构体数组TheCells进行一次初始化,代码如下:
/* 构建函数:构建一个TheCells,用于储存数据
 * 返回值:无
 * 参数:tablesize:构建的TheCells的大小
 */
void HashMap::Init(int tablesize) {
	if (tablesize < MinTableSize) // 判断输入大小是否合法
	{
		cout << "哈希表大小过小!" << endl;
		return; // 直接返回
	}

	TableSize = NextPrime(tablesize); // 大小为比输入大的第一个素数
	TheCells = new HashEntry[TableSize]; // 申请动态空间
	if (TheCells == NULL) // 检测申请是否成功
		cout << "申请内存失败!" << endl;

	for (int i = 0; i < TableSize; i++) // 初始化储存单元状态
		TheCells[i].Info = Empty;
}

        当我们申请完TheCells数组,就可以开始我们的查找和插入操作了,但是还有一个问题需要解决,素数的TableSize为我们解决很大一部分的key值重合问题,但这并不代表不会重合,比如当我们向一个TableSize为11的Hash表中插入3和14的时候,他们的key值都为3,但是一个节点中显然不能同时存储两个元素,那么我们又该怎么办呢?有一个简单的想法是当TheCells[key]已经被占用的时候我们选择存储到TheCells[key + 1]中,直到找到一个空的节点,再将此元素储存到当中;但是这显然是一个很不合适的方法,因为当Hash表中已经储存了3,4,5,6时,在储存14显然要找到key = 7的位置才能储存这显然是很低效的,因此我们采用另外一种方法, 设一个函数H(x)表示x取得key值,H(xi - 1) = H(x0) + i^2,其中i是寻找重新key值的次数;在刚才的情况中,当我们插入14时会先搜索key = 3的位置,再搜索key = 4,最后搜索key = 7的位置然后插入元素,这样我们就可以很好的解决刚才的问题了。
        那么对于查找操作也是一样的,若对应的key值中元素和我们查找的元素不同,这查找下一个key值,直到查找到我们想要的元素或者是一个未占用的节点,代码如下:
/* 查找函数:查找对应的元素,并返回其位置;若不存在此元素,则返回其可以插入的位置(平方探测法)
 * 返回值:无
 * 参数:key:想要查找的元素
 */
Position HashMap::Find(int key) {
	Position CurPos; // 储存元素位置
	int CollisionNum = 0; // 储存F(i)

	CurPos = Hash(key); // 获取对应元素的初始位置
	while(TheCells[CurPos].Info == Legitimate && TheCells[CurPos].Element != key) // 判断是否查找到元素,或其不存在
	{
		CurPos += 2 * ++CollisionNum - 1; // 查找下一个位置:H(i) = H(0) + F(i); F(i) = i^2
		if (CurPos >= TableSize)
			CurPos -= TableSize;
	}

	return CurPos; // 返回对应位置
}

        插入操作将变得很简单,我们直接查找对应元素应该在的位置,然后进行插入并改变节点状态:
/* 插入函数:插入对应的元素(平方探测法)
 * 返回值:无
 * 参数:key:想要插入的元素
 */
void HashMap::Insert(int key) {
	Position Pos; // 储存插入位置

	Pos = Find(key); // 获取插入位置
	if (TheCells[Pos].Info != Legitimate) // 判断key是否已经存在
	{
		TheCells[Pos].Info = Legitimate; // 更改储存单元状态
		TheCells[Pos].Element = key;
		CurSize++; // 增加已使用单元大小
	}
	if (CurSize > TableSize / 2) // 判断再散列条件
		ReHash();
}


删除操作:

        删除操作也将变得很简单,我们只需要查找到对应元素所在节点,将该节点的状态变为Deleted,也就是被删除状态即可,并不需要对其存储元素的值进行操作,这就是我们的懒惰删除,将会对我们的编程节省很多的操作:
/* 删除函数:删除对应的元素
 * 返回值:无
 *参数:key:想要删除的元素
 */
void HashMap::Remove(int key) {
	Position Pos; // 储存对应元素位置
	
	Pos = Find(key); // 查找对于元素位置
	if (TheCells[Pos].Info == Legitimate) // 判断元素是否存在
	{
		TheCells[Pos].Info = Deleted;
		CurSize--; // 减少已使用单元大小
	}
}


再散列:

        也许细心的朋友已经看到了,插入操作的最后有一个叫eHash()的函数,它是我们Hash表重要的一个功能,叫做再散列,它可以使我们的Hash表中一半的key值在使用后,可以扩展Hash表的存储位置。我们之所以这么做,是因为已经有大量的实际表面,当Hash表中的使用空间超过一半以后,其插入以及查找的效率将会变得很低下,所以我们通过扩展其空间的方法重新申请一个新的TheCells并删除旧的,同时将原来的元素插入新的TheCells中。当然新表的TableSize为原来的两倍。
        以下是实现代码:
/* 再散列函数:对HashMap进行再散列操作
 * 返回值:无
 * 参数:无
 */
void HashMap::ReHash() {
	int oldSize = TableSize; // 储存旧HashMap大小
	HashEntry *oldCells = TheCells; // 储存旧TheCells

	Init(2 * TableSize); // 构建新的TheCells
	CurSize = 0;
	for (int i = 0; i < oldSize; i++) // 重新插入旧表元素
		if (oldCells[i].Info == Legitimate)
			Insert(oldCells[i].Element);

	delete oldCells; // 删除旧TheCells
	oldCells = NULL;
}


那么散列表(Hash表)的回顾到这里就结束啦,欢迎大家一起讨论~~

参考文献:《数据结构与算法分析——C语言描述》

~~转载请注明出处

猜你喜欢

转载自blog.csdn.net/weixin_41427400/article/details/80039530