C++ - 哈希

news2024/11/15 17:23:43

   在顺序结构以及平衡树中,由于元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较;比如顺序表中需要从表头开始依次往后比对寻找,查找时间复杂度为 O(N),平衡树中需要从第一层开始逐层往下比对寻找,查找时间复杂度为 O(logN);即搜索的效率取决于搜索过程中元素的比较次数。

   尽管平衡树的查找方式已经很快了,但我们仍然认为该方法不够极致,我们最理想的查找方式:不像二叉树那样经过层层比较,能够用O(1)的时间复杂度直接查询到我们需要的元素。

  如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

一.哈希思想

 1.1 哈希思想的概念 

    构造一种存储结构我们通过某种方法使元素的存储位置与其查找的元素建立某种映射关系,从而利用O(1)的时间复杂度一次性查找到我们需要的元素,这就是哈希思想。

   当向该结构中:

  • 插入元素时:根据待插入元素的特性,提取出一个关键码,以此通过转换函数计算出该元素的存储位置并按此位置进行插入。
  • 搜索元素时:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

  该方法即为 哈希 (散列) 方法,哈希方法中使用的转换函数称为哈希 (散列) 函数,构造出来的结构称为哈希表 (Hash Table) (或者称散列表)。

  ps:我们上面提到的不管是顺序搜索、平衡树搜索还是哈希搜索,其 key 值都是唯一的,也就是说,搜索树中不允许出现相同 key 值的节点,哈希表中也不允许出现相同 key 值的元素,我们下文所进行的所有操作也都是在这前提之上进行的。

假设  数据集合 {1 , 7 , 6 , 4 , 5 , 9} ; 存储空间为10

哈希函数设置为: hash(key)   = key % capacity ;

capacity 为存储元素底层空间总的大小。

 

我们通过 查找元素与下标关系 构建一个数组 ,当我们查找1 时,可以直接定位到下标1这个位置,实现O(1)时间内的查找。 

 1.2 哈希函数

哈希函数有如下设计原则

  1. 哈希函数应该要满足待插入的所有元素使用,其值域如果在0到m-1之间,那么他就必须有m个空间。
  2. 哈希函数计算出来的地址要尽量能均匀分布在整个空间中;
  3. 哈希函数应该比较简单

我们有几个比较常见的哈希函数:
1.2.1 直接定址法 

直接定址法是最简单的哈希函数,顾名思义,直接定址就是根据 key 值直接得到存储位置,最多再进行一个简单的常数之间的转换,其定义如下:

Hash(Key)= A*Key + B (A B 均为常数)

 这个如果不懂,假设A=0,B=0,Key为常数,即根据Key值直接确定存储位置。

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况

直接定址法不适用于数据范围分散的情况,因为这样会导致哈希表的空间利用率很低,会浪费很多空间 

比如:int arr[] = { 123, 126, 125, 138, 122331, 1}; 假设A,B都为0

 Hash(1)= 1;

Hash(12231)=12231;

那么我们根据哈希函数的定义,至少要用12231个int大小的数组来建立哈希表,那么它就会占用很多存储空间。

1.2.2 除留余数法 (最常用)

  为了应对数据范围分散的情况,有人设计出了除留余数法 – 设哈希表中允许的地址数为m,取一个不大于m,但最接近或者等于m的素数p作为除数。

Hash(key) = key % p (p<=m)

  简单来说就是用 key 值除以哈希表的大小得到的余数作为哈希映射的地址,将 key 保存到该地址中;除留余数的优点是可以处理数据范围分散的数据,缺点是会引发哈希冲突(下文会提及).

例如对于数据集合 {1,7,6,4,5,9},存储空间为10 ,它的哈希表如下:

ps: 接下来我们在文章中如果提到哈希函数,默认为除留余数法。 

1.3 哈希冲突

  如何有两个元素 x!=y,但是 hash(x)==hash(y),这种明明不同的元素,但是经过哈希函数计算后得到的结果一致,我们就将这种情况称为哈希冲突。

哈希冲突有两种常见的解决办法:

  闭散列 (开放定址法):当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的 “下一个” 空位置中去;

  
  开散列 (链地址法):首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码 (哈希冲突) 归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中;也就是说,当发生哈希冲突时,把 key 直接链接在该位置的下面。

假设我们在 插入一个11和21 :

二、闭散列法的哈希表

  闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的 “下一个” 空位置中去;那如何寻找下一个空位置呢?有两种方法 – 线性探测法(常用)和二次探测法。

2.1线性探测法

  线性探测法是指从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

插入11和21后: 

2.2 哈希表的基本框架

//通过空和存在判断插入,通过删除状态判断查找
enum State {
	EXIST,
	EMPTY,
	DELETE
};


template<class K,class V>
struct HashData {
	pair<K, V> _kv; //每个节点存储KV结构
	State _state = EMPTY;  //默认为空
};

template<class K, class V>
class HashTable {
	typedef HashData<K, V> Data;

	HashTable()
		: _n(0)
	{
		//将哈希表的大小默认给为10
		_tables.resize(10);
	}
private:

	//把哈希函数封装一下
	size_t Hashifunction(const K& key){
		//这里我们采用除留余数法
		return key% _tables.size();
	}
private:
	vector<Data> _tables;
	size_t _n;  //记录表中有效数据的个数
};

  如上,为了方便,在哈希表中我们使用了 vector 来存储数据,并增加了一个变量 n 来记录表中有效数据的个数,这是我们哈希表的底层实现。

 同时,我们在哈希表的每个位置的数据中还增加了一个 state 变量来记录该位置的状态,但为什么要有三个变量呢?我们不是只需要存在或者不存在不就足够了吗?这是为了以后哈希表的查找做基础。

2.3 哈希表的插入

  • 插入:通过哈希函数得到余数即数组下标,如果该下标的状态为删除或为空则插入,如果为存在则向后寻找下一个状态为删除/空的位置进行插入。(扩容一会单独讲)
	bool Insert(const pair<K,V> & kv){

		if (find(kv.first)){
			//先判断是否查找到,因为不能存储相同的结构,如果查找到返回false
			return false;
		}
		//根据除留余数法判断该插入数组的哪个位置
		size_t hashi = Hashifunction(kv.first);
		while (_tables[hashi]._state == EXIST) {
			++hashi;
			hashi = hashi % _tables.size();  //如果探测到末尾则从头开始重新探测
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}

2.4 哈希表的查找

  查找:通过哈希函数得到余数即数组下标,取出小标位置的key与目标key进行比较,相等就返回该位置的地址,不相等就继续往后查找,如果查找到状态为空的下标位置就返回 nullptr;注意:这里有三个细节:


  当遇到状态为空的下标位置才返回 nullptr,而不是遇到状态为 删除的位置就返回 nullptr,因为你要查找的数据可能在已删除位置的后面,这也是我们需要添加一个删除状态的原因,如果你把一个位置的元素删除并设置为EMPTY状态,那么我们该如何进行查找呢?我们插入时,其元素可能在删除元素后面,例如:
  

如果是这样,那么你该如何查找呢? 

  
  将查找函数返回值定义为 Data*,而不是 bool,这样可以方便我们进行删除和修改 (修改 key 对应的 value) – 查找到之后直接通过指针解引用来修改 value 与 state;


  哈希表经过不断插入删除,最终可能会出现一种极端情况 – 哈希表中元素的状态全为 EXIST 和 DELETE,此时如果我们找空就会造成死循环,所以我们需要对这种情况单独进行处理.

	Data* find(const K& key) {

		size_t hashi = Hashifunction(key);
		//增加一个数字 如果查找一周 返回false
		size_t start = hashi;

		while (_tables[hashi]._state!= EMPTY)//如果不为空则继续查找
		{
			if (_tables[hashi]._state==EXIST&&_tables[hashi]._kv.first == key){
				return &_tables[hashi];
			}
			hashi++;
			//此时已经查找了一圈,退出循环
			if (hashi == start) {
				break;
			}
			hashi %= _tables.size();
		}
		//为空查找失败
		return nullptr;
	}

 2.5 哈希表的删除

 删除:这里直接复用查找函数,查找到就通过查找函数的返回值将小标位置数据的状态置为 删除,找不到就返回 false。

  这里采用伪删除法,将状态改为DELETE就好,毕竟我们重新插入时也是只看位置的状态,伪删除法还减少了删除消耗。

	bool Erase(const K& key)
	{
		HashData<K, V>* ret = find(key);
		if(ret){
			ret->_state = DELETE;
			--_n;
			return true;
		}
		else {
			return false;
		}
	}

2.6 哈希表的扩容

   哈希表的扩容和顺序表的不同,它并不是存储空间满了的时候才开始扩容,而是依据负载因子

 其本质原因就是因为有哈希冲突的存在,当我们插入时,如果发生了哈希冲突,那么我们的插入,查找,删除效率最低都可以达到O(n)级别,那就将我们哈希表的各种优势都变得很微弱,甚至是变成劣势(相比红黑树等来说,有可能退化为顺序表)。

  因此,我们引入了负载因子这一概念,其本质为一个阈值,定义为:

负载因子=散列表中的元素个数/散列表的长度

    这里我们定义我们的负载因子为0.7,并且当负载因子每次超过这个值时,我们都将其容量扩大为2倍。

  同时,注意我在代码区编写的注意事项

代码如下:

	void HashExpansion(const size_t& fac)
	{
		//开始扩容
		if (fac >= 7)
		{
			//我们直接采用老板思维,创建一个长度为2*n的新表,然后一个个插入,最后直接交换
	        //因为扩容后,数组长度发生变化,哈希函数也会变化,哈希表对应的位置也会变化
			//因此不能单纯的把数组长度变为2倍,需要一个个借助新表的插入函数,重新建立映射关系
			HashTable<K, V> newData;
			newData._tables.resize(_tables.size() * 2);
			
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
					newData.Insert(_tables[i]._kv);
				}
			}


			_tables.swap(newData._tables);
		}
	}

更改后的insert代码:

	bool Insert(const pair<K,V> & kv){

		if (find(kv.first)){
			//先判断是否查找到,因为不能存储相同的结构,如果查找到返回false
			return false;
		}
		//根据除留余数法判断该插入数组的哪个位置
		size_t hashi = Hashifunction(kv.first);
		
		//这里我们通过一点小学知识将0.7 化为整数7
		HashExpansion(_n * 10 / _tables.size());

		while (_tables[hashi]._state == EXIST) {
			++hashi;
			hashi = hashi % _tables.size();  //如果探测到末尾则从头开始重新探测
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}

2.7 哈希表的仿函数

 我们的哈希表就已经完成的差不多了,还有很多细节问题,比如说,我们如果key类型是一个string类型的对象,我们该如何经过除留余数法,得到我们应该对应的下标呢?

 我们这里建议创建几个仿函数,分别对应不同的类型,得到不同的求解方法。

 关于整形,我们的仿函数模板如下,直接放回其对应的整形值:

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

关于其它类型,我们先将其转化为整形,然后再将其返回,如字符串类型:

template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		// BKDR转换方法

		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}

		//cout << key << ":" << hash << endl;
		return hash;
	}
};

  但是请注意,其他类型,不同的值可能会经过转换的结果是相同的,这是因为int或者其它整形一共就几十个bit位,而string等等类型我只能说无穷无尽也。

  因此,建议用一些专用的转换方法,这些是专业string转换整形方法的链接:各种字符串Hash函数 - clq - 博客园

模板更改和函数更改: 

2.8 测试代码 

 打印函数:

	void Print(){
		for (size_t i = 0; i < _tables.size(); i++){
			if(_tables[i]._state==EXIST){
				//cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
				printf("_tables._state[%d] == ", i);
				cout << _tables[i]._kv.first <<"-> "<< _tables[i]._kv.second<< endl;
			}
			else if (_tables[i]._state == DELETE) {
				printf("_tables._state[%d] == DELETE\n", i);
			}
			else {
				printf("_tables._state[%d] == EMPTY\n", i);
			}
		}
		cout << endl << endl;
	}

测试代码1:
 


void TestHT1()
{
	HashTable<int, int> ht;
	int a[] = { 4,14,24,34,5,7,1 };
	for (auto e : a)
	{
		ht.Insert(make_pair(e, e));
	}

	ht.Insert(make_pair(3, 3));
	ht.Insert(make_pair(3, 3));
	ht.Insert(make_pair(-3, -3));
	ht.Print();

	ht.Erase(3);
	ht.Print();

	if (ht.find(3))
	{
		cout << "3存在" << endl;
	}
	else
	{
		cout << "3不存在" << endl;
	}

	ht.Insert(make_pair(3, 3));
	ht.Insert(make_pair(23, 3));
	ht.Print();
}

结果为:
 

 

 

测试代码2:
 

void TestHT2()
{
	string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//HashTable<string, int, HashFuncString> ht;
	HashTable<string, int> ht;
	for (auto& e : arr)
	{
		//auto ret = ht.Find(e);
		HashData<string, int>* ret = ht.find(e);
		if (ret)
		{
			ret->_kv.second++;
		}
		else
		{
			ht.Insert(make_pair(e, 1));
		}
	}

	ht.Print();

	ht.Insert(make_pair("apple", 1));
	ht.Insert(make_pair("sort", 1));

	ht.Insert(make_pair("abc", 1));
	ht.Insert(make_pair("acb", 1));
	ht.Insert(make_pair("aad", 1));

	ht.Print();
}

代码结果为:

2.9 完整代码 

#pragma once
#include<iostream>
#include<vector>
using namespace std;


template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

// 11:46继续
//HashFunc<string>
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		// BKDR
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}
		//cout << key << ":" << hash << endl;
		return hash;
	}
};



//通过空和存在判断插入,通过删除状态判断查找
enum State {
	EXIST,
	EMPTY,
	DELETE
};


template<class K,class V>
struct HashData {
	pair<K, V> _kv; //每个节点存储KV结构
	State _state = EMPTY;  //默认为空
};

//template<class K, class V>
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
	typedef HashData<K, V> Data;
public:
	HashTable()
		: _n(0)
	{
		//将哈希表的大小默认给为10
		_tables.resize(10);
	}

	bool Insert(const pair<K,V> & kv){

		if (find(kv.first)){
			//先判断是否查找到,因为不能存储相同的结构,如果查找到返回false
			return false;
		}
		//根据除留余数法判断该插入数组的哪个位置
		size_t hashi = Hashifunction(kv.first);
		
		//这里我们通过一点小学知识将0.7 化为整数7
		HashExpansion(_n * 10 / _tables.size());

		while (_tables[hashi]._state == EXIST) {
			++hashi;
			hashi = hashi % _tables.size();  //如果探测到末尾则从头开始重新探测
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}

	Data* find(const K& key) {

		size_t hashi = Hashifunction(key);
		//增加一个数字 如果查找一周 返回false
		size_t start = hashi;

		while (_tables[hashi]._state!= EMPTY)//如果不为空则继续查找
		{
			if (_tables[hashi]._state==EXIST&&_tables[hashi]._kv.first == key){
				return &_tables[hashi];
			}
			hashi++;
			//此时已经查找了一圈,退出循环
			if (hashi == start) {
				break;
			}
			hashi %= _tables.size();
		}
		//为空查找失败
		return nullptr;
	}

	bool Erase(const K& key)
	{
		HashData<K, V>* ret = find(key);
		if(ret){
			ret->_state = DELETE;
			--_n;
			return true;
		}
		else {
			return false;
		}
	}

	void HashExpansion(const size_t& fac)
	{
		//开始扩容
		if (fac >= 7)
		{
			//我们直接采用老板思维,创建一个长度为2*n的新表,然后一个个插入,最后直接交换
	        //因为扩容后,数组长度发生变化,哈希函数也会变化,哈希表对应的位置也会变化
			//因此不能单纯的把数组长度变为2倍,需要一个个借助新表的插入函数,重新建立映射关系
			HashTable<K, V> newData;
			newData._tables.resize(_tables.size() * 2);
			
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
					newData.Insert(_tables[i]._kv);
				}
			}


			_tables.swap(newData._tables);
		}
	}

	void Print(){
		for (size_t i = 0; i < _tables.size(); i++){
			if(_tables[i]._state==EXIST){
				//cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
				printf("_tables._state[%d] == ", i);
				cout << _tables[i]._kv.first <<"-> "<< _tables[i]._kv.second<< endl;
			}
			else if (_tables[i]._state == DELETE) {
				printf("_tables._state[%d] == DELETE\n", i);
			}
			else {
				printf("_tables._state[%d] == EMPTY\n", i);
			}
		}
		cout << endl << endl;
	}

private:

	//把哈希函数封装一下
	size_t Hashifunction(const K& key){
		//这里我们采用除留余数法
		Hash kot;
		return kot(key)% _tables.size();
	}
private:
	vector<Data> _tables;
	size_t _n;  //记录表中有效数据的个数
};

void TestHT1()
{
	HashTable<int, int> ht;
	int a[] = { 4,14,24,34,5,7,1 };
	for (auto e : a)
	{
		ht.Insert(make_pair(e, e));
	}

	ht.Insert(make_pair(3, 3));
	ht.Insert(make_pair(3, 3));
	ht.Insert(make_pair(-3, -3));
	ht.Print();

	ht.Erase(3);
	ht.Print();

	if (ht.find(3))
	{
		cout << "3存在" << endl;
	}
	else
	{
		cout << "3不存在" << endl;
	}

	ht.Insert(make_pair(3, 3));
	ht.Insert(make_pair(23, 3));
	ht.Print();
}

void TestHT2()
{
	string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//HashTable<string, int, HashFuncString> ht;
	HashTable<string, int> ht;
	for (auto& e : arr)
	{
		//auto ret = ht.Find(e);
		HashData<string, int>* ret = ht.find(e);
		if (ret)
		{
			ret->_kv.second++;
		}
		else
		{
			ht.Insert(make_pair(e, 1));
		}
	}

	ht.Print();

	ht.Insert(make_pair("apple", 1));
	ht.Insert(make_pair("sort", 1));

	ht.Insert(make_pair("abc", 1));
	ht.Insert(make_pair("acb", 1));
	ht.Insert(make_pair("aad", 1));

	ht.Print();
}

三. 开散列表法的哈希表

  开散列法又叫 链地址法 (开链法),首先对关键码集合用散列函数计算散列地址,即 key 映射的下标位置,具有相同地址的关键码 (哈希冲突) 归于同一子集合,每一个子集合称为一个桶 (哈希桶),各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中;也就是说,当发生哈希冲突时,把 key 作为一个节点直接链接到通过哈希函数转换后对应下标的哈希桶中。

3.1 哈希表的基本框架

//两个仿函数,直接CV
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//HashFunc<string>
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		// BKDR
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}
		//cout << key << ":" << hash << endl;
		return hash;
	}
};

//这里就不用状态表示了,因为我们无论节点是否存在,都可以进行插入
/*enum State {
	EXIST,
	EMPTY,
	DELETE
};*/

//节点定义
template<class K,class V>
struct HashNode {
	pair<K, V> _kv;
	HashNode<K, V>* _next;

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

//哈希表的基本框架
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
	typedef HashNode<K, V> Node;
private:
	//把哈希函数封装一下
	size_t Hashifunction(const K& key) {
		//这里我们采用除留余数法
		Hash kot;
		return kot(key) % _tables.size();
	}
private:
	vector<Node*> _tables;  //指针数组
	size_t _n;  //表中有效数据的个数
};

3.2 哈希表的插入函数

  开散列插入的前部分和闭散列一样,根据哈希函数得到映射的下标位置,与闭散列不同的是,由于哈希表中每个下标位置都是一个哈希桶,即一个单链表,那么对于发现哈希冲突的元素我们只需要将其链接到哈希桶中即可,这里一共有两种链接方式:

  将元素链接到单链表的末尾,即尾插;
  将元素链接到单链表的开头,即头插。


这里显然是选择元素进行头插,因为尾插还需要找尾,会导致效率降低,插入部分代码如下:
 

	bool Insert(const pair<K, V>& kv) {
		if (find(kv.first)) {
			return false;
		}
		//这里待会需要补一个扩容
		size_t hashi = Hashifunction(kv.first);
		//哈希桶头插
		Node* newnode = new Node(kv);
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_n;
		return true;
	}

 3.3 开散列的查找

  开散列的查找也很简单,根据哈希函数找到下标,由于下标位置存储的是链表首元素地址,所以我们只需要取出首元素地址,然后顺序遍历单链表即可:

Node* find(const K& key) {
	size_t  hashi = Hashifunction(key);
	Node* cur = _tables[hashi];
	while (cur) {
		if (cur->_kv.first == key){
			return cur;
		}
		cur = cur->_next;
	}
	return nullptr;
}

3.4 开散列的删除

  开散列的删除不能单纯的依靠查找函数来进行直接删除,因为在删除函数中,我们不仅要对本应查找到的节点进行删除,还要改变其父节点的指向,让他指向删除节点的下一个节点。

	bool erase(const K& key) {
		size_t  hashi = Hashifunction(key);
		Node* cur = _tables[hashi];
	    //记录上一个节点的父节点
		Node* prev = nullptr;
		while (cur) {
			if (cur->_kv.first == key) {
				if (cur == _tables[hashi]) {
					_tables[hashi] = cur->_next;
				}
				else {
					prev->_next = cur->_next;
				}
				delete cur;
				--_n;
				return true;
			}
			prev = cur;
			cur = cur->_next;
		}
		return false;
	}

3.5 开散列的扩容

开散列的扩容可以和闭散列的扩容一样借用insert函数,但是我们有更好的方法。

方法一:
  借用insert函数。

方法二:

   将原本链表挨个头插入新的哈希表。 

	if (fac > 7) {
		//这里我们有两种方法
		//一是借用 以前我们开散列中的insert插入方法
		//此种实现比较简单,但相比第二种有其对应的缺点
		//实现
	/*	HashTable<K, V> newTable;
		newTable._tables.resize(_tables.size() * 2,nullptr);
		for (int i = 0; i < _tables.size(); i++) {
			Node* cur = _tables[i];
			while (cur) {
				newTable.Insert(cur);
				cur = cur->_next;
			}
		}
		_tables.swap(newTable._tables);
		}*/

		//二是直接把原先的哈希表中的每个单链表 按照新的哈希函数
		//直接头插进新链表
		//这个方法我们可以看出,少借用insert的一部分
		//也就是说,我们没有创建节点的消耗。
		//是真正的空间转移 因此,效率比第一种方法高很多,我们以后用这个方法
		vector<Node*> newtable;
		newtable.resize(_tables.size() * 2, nullptr);
		for (int i = 0; i < _tables.size(); i++) {
			Node* cur = _tables[i];
			while (cur) {
				Node* next = cur->_next;
				size_t hashi = Hashifunction(cur->_kv.first, newtable);
				cur->_next = newtable[hashi];
				newtable[hashi] = newtable;
				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newtable);
	}
}

 3.6 开散列的测试代码

print函数和以前也要有所不同。


		void Print() {

			for (int i = 0; i < _tables.size(); i++) {
				Node* cur = _tables[i];
				cout << i << ":";
				while (cur) {
					cout << "[" << cur->_kv.first << "-> " << cur->_kv.second << "]" << "    ";
					cur = cur->_next;
				}
				cout << endl;
			}
			cout << endl;
		}

两段测试代码: 

	void TestHT1()
	{
		HashTable<int, int> ht;
		int a[] = { 4,14,24,34,5,7,1 };

		for(auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(-3, -3));
		ht.Print();

		ht.erase(3);
		ht.Print();

		if (ht.find(3))
		{
			cout << "3存在" << endl;
		}
		else
		{
			cout << "3不存在" << endl;
		}

		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(23, 3));
		ht.Print();
}

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

	ht.Print();

	ht.Insert(make_pair("apple", 1));
	ht.Insert(make_pair("sort", 1));

	ht.Insert(make_pair("abc", 1));
	ht.Insert(make_pair("acb", 1));
	ht.Insert(make_pair("aad", 1));

	ht.Print();
}

代码一结果:

测试代码二结果:

3.7 完整代码 

#pragma once
#include<iostream>
#include<vector>
using namespace std;

namespace BucketHash {

	//两个仿函数,直接CV
	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	//HashFunc<string>
	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& key)
		{
			// BKDR
			size_t hash = 0;
			for (auto e : key)
			{
				hash *= 31;
				hash += e;
			}
			//cout << key << ":" << hash << endl;
			return hash;
		}
	};

	//这里就不用状态表示了,因为我们无论节点是否存在,都可以进行插入
	/*enum State {
		EXIST,
		EMPTY,
		DELETE
	};*/

	//节点定义
	template<class K,class V>
	struct HashNode {
		pair<K, V> _kv;
		HashNode<K, V>* _next;

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

	//哈希表的基本框架
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable {
		typedef HashNode<K, V> Node;

	public:

		HashTable()
			:_n(0)
		{
			_tables.resize(10, nullptr);
		}

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

			HashExpansion(_n * 10 / _tables.size());
			//这里待会需要补一个扩容
			size_t hashi = Hashifunction(kv.first,_tables.size());
			//哈希桶头插
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

		Node* find(const K& key) {
			size_t  hashi = Hashifunction(key,_tables.size());
			Node* cur = _tables[hashi];
			while (cur) {
				if (cur->_kv.first == key){
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		bool erase(const K& key) {
			size_t  hashi = Hashifunction(key,_tables.size());
			Node* cur = _tables[hashi];
		    //记录上一个节点的父节点
			Node* prev = nullptr;
			while (cur) {
				if (cur->_kv.first == key) {
					if (cur == _tables[hashi]) {
						_tables[hashi] = cur->_next;
					}
					else {
						prev->_next = cur->_next;
					}
					delete cur;
					--_n;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		void HashExpansion(const size_t& fac){
			if (fac > 7) {
				//这里我们有两种方法
				//一是借用 以前我们开散列中的insert插入方法
				//此种实现比较简单,但相比第二种有其对应的缺点
				//实现
			/*	HashTable<K, V> newTable;
				newTable._tables.resize(_tables.size() * 2,nullptr);
				for (int i = 0; i < _tables.size(); i++) {
					Node* cur = _tables[i];
					while (cur) {
						newTable.Insert(cur);
						cur = cur->_next;
					}
				}
				_tables.swap(newTable._tables);
				}*/

				//二是直接把原先的哈希表中的每个单链表 按照新的哈希函数
				//直接头插进新链表
				//这个方法我们可以看出,少借用insert的一部分
				//也就是说,我们没有创建节点的消耗。
				//是真正的空间转移 因此,效率比第一种方法高很多,我们以后用这个方法
				//但是哈希函数就不好处理了,这里我建议直接把哈希函数改一下,直接传入表的长度
				vector<Node*> newtable;
				newtable.resize(_tables.size() * 2, nullptr);
				for (int i = 0; i < _tables.size(); i++) {
					Node* cur = _tables[i];
					while (cur) {
						Node* next = cur->_next;
						size_t hashi = Hashifunction(cur->_kv.first, newtable.size());
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newtable);
			}
		}


		void Print() {

			for (int i = 0; i < _tables.size(); i++) {
				Node* cur = _tables[i];
				cout << i << ":";
				while (cur) {
					cout << "[" << cur->_kv.first << "-> " << cur->_kv.second << "]" << "    ";
					cur = cur->_next;
				}
				cout << endl;
			}
			cout << endl;
		}

	private:
		//把哈希函数封装一下
		size_t Hashifunction(const K& key,size_t size) {
			//这里我们采用除留余数法
			Hash kot;
			return kot(key) % size;
		}
	private:
		vector<Node*> _tables;  //指针数组
		size_t _n;  //表中有效数据的个数
	};
	void TestHT1()
	{
		HashTable<int, int> ht;
		int a[] = { 4,14,24,34,5,7,1 };

		for(auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(-3, -3));
		ht.Print();

		ht.erase(3);
		ht.Print();

		if (ht.find(3))
		{
			cout << "3存在" << endl;
		}
		else
		{
			cout << "3不存在" << endl;
		}

		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(23, 3));
		ht.Print();
}

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

	ht.Print();

	ht.Insert(make_pair("apple", 1));
	ht.Insert(make_pair("sort", 1));

	ht.Insert(make_pair("abc", 1));
	ht.Insert(make_pair("acb", 1));
	ht.Insert(make_pair("aad", 1));

	ht.Print();
}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1311105.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通俗理解什么是 LSTM 神经网络

大家好啊&#xff0c;我是董董灿。 刚开始做程序开发时&#xff0c;在公司提交代码前&#xff0c;都需要让大佬们 review(评审)&#xff0c;大佬们看完&#xff0c;总会在评论区打出一串"LGTM"。 当时作为小白的我&#xff0c;天真地以为大佬觉得我提交的代码还不错…

浮动的魅力与挑战:如何在前端设计中巧妙运用浮动(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

【UE5.1 MetaHuman】使用mixamo_converter把Mixamo的动画重定向给MetaHuman使用

目录 前言 效果 步骤 一、下载mixamo_converter软件 二、Mixamo动画重定向 三、导入UE 四、动画重定向 五、使用重定向后的动画 前言 上一篇&#xff08;【UE5】初识MetaHuman 创建虚拟角色&#xff09;中我们已经制作了一个MetaHuman&#xff0c;本篇博文将介绍如何…

Unity 关于Rigidbody刚体组件的理解

一、基本了解 刚体Rigidbody因具体物理相关的属性&#xff0c;使得实际应用中更有真实感。应用也多&#xff1a; Rigidbody它可以受到重力、碰撞或者力的作用&#xff0c;所以我们可以用它模拟物体的真实物理行为&#xff0c;如受到重力的作用、与其他刚体对象进行碰撞&#…

【infiniband】ibdump抓包

ibdump用于捕获和转储InfiniBand网络的流量。 这种工具通常用于调试和分析InfiniBand网络问题&#xff0c;包括性能瓶颈和配置错误。ibdump工具在Mellanox InfiniBand环境中较为常用&#xff0c;现由NVIDIA提供支持。 使用ibdump的基本步骤 请注意&#xff0c;您需要在安装了…

11.关注、取消关注 + 关注列表、粉丝列表

目录 1.关注、取消关注 1.1 把数据存到 Redis 中&#xff0c;构造 key 1.2 开发业务层 1.3 开发表现层 1.4 显示正确的关注数据 2.关注列表、粉丝列表 2.1 业务层 2.2 表现层 1.关注、取消关注 需求&#xff1a;开发关注、取消关注功能&#xff1b;统计用户的关注数…

飞天使-docker知识点5-资源限制与容器的不同状态

文章目录 cpu和内存的限制内存限制的部分参数容器的不同状态docker images 的分层docker registry制作镜像 cpu和内存的限制 默认情况下&#xff0c;容器没有资源限制&#xff0c;可以使用主机内核调度程序允许的尽可能多的 给定资源&#xff0c;Docker 提供了控制容器可以限制…

wordpress 修改社交图标

要去掉标记的图标&#xff0c;死活找不到在那里配置。后来找到了&#xff0c;下图&#xff08;wordpress 小白&#xff0c;特此记录&#xff09;

【开源软件】最好的开源软件-2023-第17名 Gravite

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

事务--03---TCC空回滚、悬挂、幂等解决方案

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 Seata TCC 模式设计思路TCC存在的问题1、空回滚以及解决方案解决方案&#xff1a; 2、幂等问题以及解决方案解决方案&#xff1a; 3、悬挂问题以及解决方案解决方案…

档案馆数字化建设实施方案

档案馆数字化建设实施方案主要包括以下几个方面的内容&#xff1a; 1. 目标与规划&#xff1a;明确数字化建设的目标和规划&#xff0c;确定数字化建设的优先领域和重点工作&#xff0c;制定长期和短期的发展规划。 2. 技术设施建设&#xff1a;建设专久智能数字化档案管理系统…

gradle-5.4.1-all下载时出现了Connect timed out

问题描述&#xff1a;最近在学习如何在手机端部署YOLO&#xff0c;出现了许多错误&#xff0c;其中之一的错误&#xff1a;gradle-5.4.1-all下载时出现了Connect timed out&#xff0c;大家都知道这是从国外网站下载网络问题导致的。 解决办法: 在我们创建项目中的一个路径下…

回溯热门问题

关卡名 回溯热门问题 我会了✔️ 内容 1.组合总和问题 ✔️ 2.分割回文串问题 ✔️ 3.子集问题 ✔️ 4.排列问题 ✔️ 5.字母全排列问题 ✔️ 6.单词搜索 ✔️ 1. 组合总和问题 LeetCode39题目要求&#xff1a;给你一个无重复元素的整数数组candidates和一个目标整数 ta…

【十】python复合模式

10.1 复合模式简介 在前面的栏目中我们了解了各种设计模式。正如我们所看到的&#xff0c;设计模式可分为三大类:结构型、创建型和行为型设计模式。同时&#xff0c;我们还给出了每种类型的相应示例。然而&#xff0c;在软件实现中&#xff0c;模式并是不孤立地工作的。对于所…

【c】数组元素移动

本题的难点之处就是不让你创建新的数组&#xff0c;而且移动的距离也没有给限制&#xff0c;比如有7个数&#xff0c;本题没有限制必须移动距离小于7&#xff0c;也可能移动的距离大于7&#xff0c;甚至更多&#xff0c;下面附上我的代码 #include<stdio.h>int main() {…

C++模板编程浅析

函数模板 声明与定义函数模板 #include <iostream> using namespace std; template <class T> void swap_new(T& a, T& b);int main() {int a 1, b 2;float c 1.5, d 3.6;swap_new(a, b);swap_new(c, d);cout << a << " " &…

【Qt5】ui文件最后会变成头文件

2023年12月14日&#xff0c;周四下午 我也是今天下午偶然间发现这个的 在使用Qt的uic&#xff08;User Interface Compiler&#xff09;工具编译ui文件时&#xff0c;会生成对应的头文件。 在Qt中&#xff0c;ui文件是用于描述用户界面的XML文件&#xff0c;而头文件是用于在…

binkw32.dll丢失怎么办?这5个方法都可以解决binkw32.dll丢失问题

binkw32.dll文件是什么&#xff1f; binkw32.dll是一个动态链接库文件&#xff0c;它是Windows操作系统中的一个重要组件。它包含了许多用于处理多媒体文件的函数和资源&#xff0c;如视频、音频等。当我们在电脑上打开或播放某些多媒体文件时&#xff0c;系统会调用binkw32.d…

刘家窑中医医院鲁卫星主任:冬季守护心脑血管,为社区居民送去健康关爱

随着冬季的来临&#xff0c;气温逐渐降低&#xff0c;心脑血管疾病的风险也随之增加。为了提高公众对心脑血管疾病的认知和预防意识&#xff0c;北京刘家窑中医医院于近日成功举办了冬季守护心脑血管公益义诊活动。 本次义诊活动主要针对社区居民中的中老年人&#xff0c;特别是…

利用闭包与高阶函数实现缓存函数的创建

缓存函数是一种用于存储和重复利用计算结果的机制。其基本思想是&#xff0c;当一个函数被调用并计算出结果时&#xff0c;将该结果存储在某种数据结构中 (通常是一个缓存对象)以备将来使用。当相同的输入参数再次传递给函数时&#xff0c;不再执行实际的计算&#xff0c;而是直…