详解c++---哈希桶

这篇具有很好参考价值的文章主要介绍了详解c++---哈希桶。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

闭散列的回顾

在前面的学习中我们知道了闭散列的运算规则,当两个数据计算得到的位置发生冲突时,它会自动的往后寻找没有发生冲突的位置,比如说当前数据的内容如下:
详解c++---哈希桶,c++详解,哈希算法,c++,算法
当插入的数据为33时计算的位置为3,可是位置3已经被占领了并且4也被占领了,但是位置5没有被占领所以插入数据33就会占领位置5,那么这里的图片就如下:
详解c++---哈希桶,c++详解,哈希算法,c++,算法
这就是闭散列的插入原则,并且每个节点都有一个变量用来表示状态,这样在查找就不会出现漏查的情况,但是这样的实现会存在一个问题,扩容是根据有效数据的个数和vector容量来确定的,但是查找的时候是根据当前元素的状态是否为空来判断后面还有没有要查找的数据,如果为空的话则说明当前元素的后面没有我们要查找的元素,如果为存在或者被删除的话就说明当前元素的后面还有我们要查找的数据,如果我们不停的插入数据并且删除数据的话就会导致容器中的每个元素的状态都变成了被删除这样在查找一个不存的数据时,就会陷入死循环的状态那么这就是我们之前模拟实现的一个缺点,那么这里我们就来看看第二个解决数据不集中的方法:拉链法或者叫哈希桶法。

拉链法/哈希桶的原理

这个方法就是每个位置上都是一个链表,如果有多个位置发生冲突了,那么就挂在这个位置的链表上,这样就不会导致占领别人的位置,当我们要查找的时候就是先找到插入数据的位置,然后再通过这个位置的链表来按照顺序来进行查找,比如说下面的图片
详解c++---哈希桶,c++详解,哈希算法,c++,算法
当我们想要插入一个数据13时就会先计算13对应在哈希表上的位置,根据之前的计算原则这里得到的位置就是3,所以图片就变成了下面这样:
详解c++---哈希桶,c++详解,哈希算法,c++,算法
如果再插入数据23的话这里计算的位置依然是3,但是此时3上已经有元素了,所以这时就会使用链表的头插将数据23插入到13的前面,那么这里的图片就是下面这样:
详解c++---哈希桶,c++详解,哈希算法,c++,算法
如果再插入数据33的话计算的位置依然是3,所以就会把33放到3号位置对应的链表的头部,那么这里的图片就变成下面这样:
详解c++---哈希桶,c++详解,哈希算法,c++,算法
那么这就是哈希桶的插入规则,找到对应位置的链表将数据插入到头部即可,如果要查找的话也是相同的原理先找到数据对应的链表然后循环遍历这个链表找到出现的数据即可,删除也是相同的道理,先找到数据对应的下标然后根据下标找到对应的链表,最后遍历链表找到要删除的数据进行链表的删除即可,那么这就是哈希桶的实现思路接下来我们就来看看这种方法的准备工作。

准备工作

哈希的底层是一个vector的数组,数组中的每个节点都有一个pair类型的数据,其次还有一个指针指向当前链表节点的下一个节点,所以每个节点中有个一个pair类型的数据和一个指向节点的指针,所以这里得创建一个类来描述每个节点,并且类中有一个构造函数来初始化节点,这里的构造函数就需要一个pair类型的参数,在构造函数里面就将指针初始化为nullptr将pair类型的参数赋值为传递过来的参数,有因为这里的节点可能要存储各种各样的数据,所以这里得创建个模板来接收各种各样的参数,并且模板的参数得是两个,那么这里的代码就如下:

template<class K,class V>
struct HashNode
{
	HashNode(const pair<K,V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{}
	pair<K, V> _kv;
	HashNode* _next;
};

根据前面的学习我们知道要想计算数据对应在哈希表上的位置就得添加对应的仿函数,那么这里的代码就如下

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t res = 0;
		for (auto& ch : s)
		{
			res *= 131;
			res += ch;
		}
		return res;
	}
};

最后就是哈希bucket类的准备工作,首先这个类有一个模板,模板中有三个参数,前两个表示存储数据的类型,最后一个表示的是仿函数,因为哈希的地城是vector数组,所以这里得添加一个vector容器用来存储数据和一个整型变量用来记录容器中有效字符的个数即可,并且vector中的每个元素都是节点类型,那么该类的构造函数就将vector容器的resize到10个元素即可,那么这里的代码就如下:

template<class K, class V, class Hash = HashFunc<K>>
class BucketTable
{
	typedef HashNode<K, V> Node;
public:
typedef HashNode<K, V> Node;
	BucketTable()
		:_n(0)
	{
		_tables.resize(10);
	}
private:
	vector<Node*> _tables;
	size_t _n;
};

看到这里我们的准备工作就完成了接下来就要实现哈希的每个函数。

find函数

find函数就是先根据传递过来参数找到这个参数可能出现的位置,找到了位置就意味着找了一个链表的头节点,所以这个时候就可以通过循环遍历的方式来一一对比链表中是否含有想要查找的数据,如果存在的话就返回这个节点所在的地址,如果不存在的话就返回一个空指针,所以该函数的第一步就创建一个仿函数对象,并计算数据出现的位置:

Node* Find(const K& key)
{
	Hash hf;
	size_t pos = hf(key) % _tables.size();
	Node* cur=_tables[pos]
}

cur指向的是链表的第一个元素,所以接下来就可以使用while循环一个一个的遍历每个元素,每次循环都会改变cur的指向让其指向下一个元素,知道cur本身变为空就停止查找,在循环体的内部如果出现了跟查找变量一样的值就直接返回这个节点的地址,如果循环结束了也没有找到想要的值的话就返回一个空指针,那么这里的代码就如下:

Node* Find(const K& key)
{
	Hash hf;
	size_t pos = hf(key) % _tables.size();
	Node* cur = _tables[pos];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			return cur;
		}
		else
		{
			cur = cur->_next;
		}
	}
	return nullptr;
}

插入函数

将数据插入的链表的时候得先判断一下要插入的元素当前是否已经存在,所以这里可以使用find函数来进行查找,根据find函数的返回值来判断是否存在,那么这里的代码就如下:

bool insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
	{
		return false;
	}
}

如果当前的元素不存在的话就开始插入数据,这种实现方法也得根据传递过来的元素找到应该插入的位置,所以该函数的第一步就是创建一个仿函数对象然后根据传递过来的参数计算得出应该插入的位置,找到插入位置之后就使用头插来插入对应的数据,这里的头插就是先让newnode的_next指向当前位置的链表,然后修改vector中当前位置的指向使其指向newnode,那么这里的代码就如下:

bool insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
	{
		return false;
	}
	Hash hf;
	size_t pos = hf(kv.first) % _tables.size();
	Node* newnode = new HashNode<K,V>(kv);
	newnode->_next = _tables[pos];
	_tables[pos] = newnode;
	++_n;
	return true;
}

这里依然得添加负载因子,官方库中可以通过一些函数来得到当前容器的负载因子和最大的负载因子,如果负载因子等于1了我们就扩容,将其容量变为之前的两倍,但是扩容不能直接把链表对应的移动到新的容器上去因为这里的映射关系已经改变了比如说当前容器的容量为10则数据18对应的位置就是8上面的链表,如果容器发生了扩容使得容量变成了20的话18就对应的不再是8而是18上面的链表,所以我们这里解决的方法就是创建一个新的哈希表,然后遍历容器中的每个位置,如果当前位置不为空就往这个位置里面进行遍历对每个元素都进行插入操作,如果当前位置为空的话就跳转到下一个元素,那么这里的代码就如下:

bool insert(const pair<K, V>& kv)
{
	if (!Find(kv.first))
	{
		return false;
	}
	if (_n / _tables.size() == 1)//平衡因子为1就更新
	{
		vector<Node*> newBH;
		newBH._tables.resize(_n * 2);
		for (auto& cur : _tables)
		{
			while (cur)
			{
				newBH.insert(cur->_kv);
				cur = cur->_next;
			}
		}
		_tables.swap(newBH._tables);

	}
	Hash hf;
	size_t pos = hf(kv.first) % _tables.size();
	Node* newnode = new HashNode<K,V>(kv);
	newnode->_next = _tables[pos];
	_tables[pos] = newnode;
	++_n;
	return true;
}

erase函数

erase函数也是分为三步来完成,首先找到节点对应的链表,然后再找链表中是否存在该元素,如果不存在的话就返回false,如果存在的话就对该链表的该节点进行删除,因为这里删除的时候得保证链表之间的上下链接,所以这里创建一个指向指向被删除节点的前一个节点,以此来保证删除前后的链接,这里大家要注意的一点就是当删除的节点是头节点时,得改变vector容器中的指针的指向,那么这里的代码就如下:

bool erase(const K& key)
{
	HashFunc<K> HF;
	size_t pos = HF(key) % _tables.size();
	Node* cur = _tables[pos];
	Node* prev = cur;
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			if (cur == _tables[pos])
			{
				_tables[pos] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			delete cur;
			_n--;
			return true;
		}
		else
		{
			prev = cur;
			cur = cur->_next;
		}
	}
	return false;
}

析构函数

之前的写法当中不需要添加析构函数是因为vector中本身就含有析构函数,所以编译器自己生成的析构函数能够自动的调用vector的析构函数来释放空间,但是这里不一样这里得写析构函数,因为vector释放之后不会释放里面的节点所申请的空间,所以这里的析构函数要干的事情就是虚幻遍历每个节点并且一个一个的释放每个节点申请的空间,那么这里的代码就如下:

~BucketTable()
{
	for (int i = 0; i < _tables.size(); i++)
	{
		Node* cur = _tables[i];
		while (cur)
		{
			Node* next = cur->_next;
			delete cur;
			cur = next;
		}
		_tables[i] = nullptr;
	}
}

代码测试

可以通过下面的代码来进行相应的测试看看我们上面写的代码是否是正确的:

void TestHT1()
{
	BucketTable<int, int> ht;
	int a[] = { 18, 8, 7, 27, 57, 3, 38, 18 };
	for (auto e : a)
	{
		ht.insert(make_pair(e, e));
	}
	ht.insert(make_pair(17, 17));
	ht.insert(make_pair(5, 5));
	if (ht.Find(7)) { cout << "存在" << endl; }
	else { cout << "不存在" << endl; }
	ht.erase(7);
	if (ht.Find(7)) { cout << "存在" << endl; }
	else { cout << "不存在" << endl; }
}
int main()
{
	TestHT1();
	return 0;
}

代码的运行结果如下:
详解c++---哈希桶,c++详解,哈希算法,c++,算法
我们可以再用下面的代码来进行一下测试:

void TestHT2()
{
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//HashTable<string, int, HashFuncString> countHT; 
	BucketTable<string, int> countHT;
	for (auto& e : arr)
	{
		HashNode<string, int>* ret = countHT.Find(e);
		if (ret)
		{
			ret->_kv.second++;
		}
		else
		{
			countHT.insert(make_pair(e, 1));
		}
	}
}

这段代码的运行结果如下:
详解c++---哈希桶,c++详解,哈希算法,c++,算法
符合我们的预期说明我们上面的代码实现的是正确的。

insert函数的改进

我们上面实现的insert函数在扩容的时候都是采用的从10开始每次都扩容2倍的方法来进行扩容,可是哈希表这里有一个性质就是当容器的大小为素数的时候可以在一定的程度上降低哈希冲突的可能性使得数据分布的更加的均匀,所以在扩容的时候和在规定起始空间的时候最好选择将容器的大小扩容为素数,依然每次扩容成为两倍但是最终的容量为两倍数据的附近的一个素数,那我们如何来找到这个数呢?如果我们真的想实现这个功能的话得需要写一个算法来找到这个数,并且这个算法还很麻烦,所以我们采用的解决方法就是用空间来换取时间,我们之间将所有范围内的素数列举出来不就可以吗?比如说下面的数组:

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, 3221225473, 4294967291
};

然后我们就可以用这个数组封装成为一个函数,这个函数的功能就是找到一个数据附件的素数,这里就可以通过循环遍历的方式来进行查找,那么这里的代码如下:

inline unsigned long __stl_next_prime(unsigned long n)
{
	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, 3221225473, 4294967291
	};
	for (int i = 0; i < __stl_num_primes; ++i)
	{
		if (__stl_prime_list[i] > n)
		{
			return __stl_prime_list[i];
		}
	}
	return __stl_prime_list[__stl_num_primes - 1];
}

有了这个游戏之后就可以对insert函数进行改进,但是这里先不要急还有一个地方需要我们改进的就是插入数据的时候,上面扩容在插入数据的时候是创建一个哈希桶然后再调用哈希桶来插入原来哈希桶的每个数据,如果这么做的话,在新的哈希桶里面又会不断地创建地节点,并且在函数结束地时候又会删除节点,如果节点的个数非常多的话这就会导致效率低下,所以我们这里就有一个改进思路就是能不能用已有的节点来插入到新创建的哈希表呢?答案是可以的,我们依然是创建一个新的哈希表然后改变其内部的大小,然后遍历之前的老哈希表找到里面的元素并计算他在新表上的位置,然后修改其节点内部指针的指向,那么这里的代码如下:

if (_n / _tables.size() == 1)//平衡因子为1就更新
{
	/*vector<Node*> newBH;;
	newBH.resize(_n * 2);
	for (auto& cur : _tables)
	{
		while (cur)
		{
			newBH.insert(cur->_kv);
			cur = cur->_next;
		}
	}
	_tables.swap(newBH._tables);*/
	vector<Node*> newBH;
	newBH._tables.resize(__stl_next_prime(_tables.size()));
	for (int i = 0; i < _tables.size(); i++)
	{
		Node* cur = _tables[i];
		while (cur)
		{
			Node* next = cur->_next;
			size_t pos = Hash()(cur->_kv.first);
			cur->_next = newBH[pos];
			newBH[pos] = cur;
			cur = next;
		}
		_tables[i] = nullptr;
	}
}

那么这就是本篇文章的全部内容,这里完整的代码就如下:文章来源地址https://www.toymoban.com/news/detail-517287.html

#pragma once
#include<iostream>
#include<vector>
using namespace std;
template<class K,class V>
struct HashNode
{
	HashNode(const pair<K,V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{}
	pair<K, V> _kv;
	HashNode* _next;
};
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t res = 0;
		for (auto& ch : s)
		{
			res *= 131;
			res += ch;
		}
		return res;
	}
};
template<class K, class V, class Hash = HashFunc<K>>
class BucketTable
{
	typedef HashNode<K, V> Node;
public:
	BucketTable()
		:_n(0)
	{
		_tables.resize(__stl_next_prime(_tables.size()));
	}
	Node* Find(const K& key)
	{
		Hash hf;
		size_t pos = hf(key) % _tables.size();
		Node* cur = _tables[pos];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				return cur;
			}
			else
			{
				cur = cur->_next;
			}
		}
		return nullptr;
	}
	bool insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))
		{
			return false;
		}
		if (_n / _tables.size() == 1)//平衡因子为1就更新
		{
			/*vector<Node*> newBH;
			newBH._tables.resize(_n * 2);
			for (auto& cur : _tables)
			{
				while (cur)
				{
					newBH.insert(cur->_kv);
					cur = cur->_next;
				}
			}
			_tables.swap(newBH._tables);*/
			vector<Node*> newBH;
			newBH.resize(__stl_next_prime(_tables.size()));
			for (int i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					size_t pos = Hash()(cur->_kv.first);
					cur->_next = newBH[pos];
					newBH[pos] = cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}
		Hash hf;
		size_t pos = hf(kv.first) % _tables.size();
		Node* newnode = new HashNode<K,V>(kv);
		newnode->_next = _tables[pos];
		_tables[pos] = newnode;
		++_n;
		return true;
	}
	bool erase(const K& key)
	{
		HashFunc<K> HF;
		size_t pos = HF(key) % _tables.size();
		Node* cur = _tables[pos];
		Node* prev = cur;
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (cur == _tables[pos])
				{
					_tables[pos] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				_n--;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}
	~BucketTable()
	{
		for (int i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
	}
	inline unsigned long __stl_next_prime(unsigned long n)
	{
		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, 3221225473, 4294967291
		};
		for (int i = 0; i < __stl_num_primes; ++i)
		{
			if (__stl_prime_list[i] > n)
			{
				return __stl_prime_list[i];
			}
		}
		return __stl_prime_list[__stl_num_primes - 1];
	}


private:
	vector<Node*> _tables;
	size_t _n;
};

到了这里,关于详解c++---哈希桶的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【SM3哈希算法】算法原理

    参考: SM3算法是一种密码散列函数标准,由国家密码管理局发布。它的安全性和SHA-256相当,适用于商用密码应用中的数字签名和验证、消息认证码生成和验证、随机数生成等。 将输入的消息分成512位的分组,并对每个分组进行填充、分组、扩展、迭代压缩等操作,最后输出

    2024年02月08日
    浏览(30)
  • 【数据结构与算法】哈希—— 位图 | 布隆过滤器 | 哈希切割

    🐱作者:一只大喵咪1201 🐱专栏:《数据结构与算法》 🔥格言: 你只管努力,剩下的交给时间! 哈希是一种映射思想,这里再讲解两种应用哈希思想的数据结构。 问题: 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数

    2024年02月02日
    浏览(44)
  • 加密算法、哈希算法及其区别+国密简介

    现代加密算法是信息安全领域中常用的算法,用于保护数据的机密性和完整性。以下是一些常用的现代加密算法: 目标 :加密算法的主要目标是保密性(Confidentiality),它用于将明文数据转换为密文数据,以确保只有授权的用户或实体可以解密和访问数据。加密算法的目标

    2024年02月07日
    浏览(30)
  • 07. 算法之一致性哈希算法介绍

    哈希算法在程序开发中的很多地方都能看到他的身影,但是哈希有他的局限性,比如如果两个key哈希到同一个位置的时候,此时就不好处理。本节我们介绍一下常规处理方式。 哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。

    2024年02月06日
    浏览(40)
  • RIPEMD算法:多功能哈希算法的瑰宝

    RIPEMD(RACE Integrity Primitives Evaluation Message Digest)算法是由欧洲研究项目RACE发起,由Hans Dobbertin、Antoon Bosselaers和Vincent Rijmen共同设计的一种哈希算法。RIPEMD算法最早发布于1996年,旨在提供一种安全、高效的数据完整性验证工具。随后的RIPEMD-128、RIPEMD-160、RIPEMD-256和RIPEMD-320等版

    2024年03月10日
    浏览(45)
  • 【数据结构与算法】前缀和+哈希表算法

    关于前缀和和哈希这两个概念大家都不陌生,在之前的文章中也有过介绍:前缀和与差分算法详解 而哈希表最经典的一题莫过于 两数之和 题目链接 题目描述: 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它

    2024年02月01日
    浏览(104)
  • 哈希表C++哈希表详解(知识点+相关LeetCode题目)

    目录 前言 一、什么是哈希表 二、哈希表的操作 2.1 操作时间复杂度 2.2 哈希表通用API 2.3 建立哈希表 2.3 哈希表常见结构介绍 Set(集合) Map(映射) unordered_map(哈希表) 三、哈希表的力扣经典题目 有效的字母异位词242 两个数组的交集 349 两数之和1 四数相加II 454 三数之和

    2024年03月20日
    浏览(38)
  • 【算法】原地哈希与快速幂

    直接看例题:题目链接 题目描述: 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 示例 1: 输入:nums = [1,2,0] 输出:3 示例 2: 输入:nums = [3,4,-1,1] 输出:2 示例 3: 输入:n

    2024年02月09日
    浏览(36)
  • 区块链中的:哈希算法

    哈希算法,又称散列算法,它是一个单向函数,可以把任意长度的输入数据转化为固定长度的输出: h=H(x)h=H(x)h=H(x) 例如,对  morning  和  bitcoin  两个输入进行某种哈希运算,得到的结果是固定长度的数字: 我们通常用十六进制表示哈希输出。 因为哈希算法是一个 单向函

    2024年02月06日
    浏览(34)
  • 谈谈一致性哈希算法

    一致性哈希算法是1997年由麻省理工的几位学者提出的用于解决分布式缓存中的热点问题。大家有没有发现,我们之前介绍的例如快排之类的算法是更早的六七十年代,此时分布式还没有发展起来, 大家往往还在提高单机性能。但是九十年代开始,逐渐需要用分布式集群来解

    2024年02月07日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包