【C++】---哈希算法

一、哈希概念

顺序结构以及平衡树中,元素关键码(K,pair等)与 其存储位置 之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。

顺序查找:时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一 一映射 的关系,那么在查找时通过该函数可以很快找到该元素。

说白了:哈希思想就是, 与其 存储位置 之间的映射关系

1、插入和查找

  1. 插入:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放hashi就是对应的存储位置,key就是关键码,table是哈希表
    其对应的映射位置的计算方法是: hashi = key % table.size();

注意:这里%取得是,哈希数组的size(),为什么不取capacity()?

原因很简单:

就是如果你取模取 capacity 的话,他最终得到的hashi可能是大于size小于capacity,但是这块区间的内容是不能访问的,由于vector的概念上面就已经明确的规定了。大于szie小于capacity,这个区间的内容虽然存在为空,但是你无法进行确切的访问!
在这里插入图片描述
画图解释:
在这里插入图片描述
2. 查找元素:
(1)先计算出 hashi (2)再去计算出来的 hashi 位置 进行key的判断 :判断hashi ->key 与我们所要找的是否相等!

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功

2、哈希表

(1)什么是哈希表?

简单的说,就是运用->哈希思想,而形成的数据结构!

运用 “哈希思想” 的函数叫做哈希函数,其关键的核心步骤:就是找出各个数据对应的储存位置的转换公式!

即:在这里插入图片描述
按照这种方法查找不用拿key多次比较,不用像链表2叉树等等那些数据结构进行多次比较才会查找到结果。因此查找的速度比较快。

(2)什么是:哈希冲突?

哈希冲突:就是说,不同的数据,通过哈希函数,来计算出它所对应的映射位置的时候,位置相同,就是哈希冲突!

在这里插入图片描述
比如说这里的10001和11,%10之后,他们所得的值都是1,都应该放在下标为1的位置,这就是哈希冲突!

引起哈希冲突的原因:哈希函数设计不合理。哈希函数设计原则包括:

(1)哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间

(2)哈希函数计算出来的地址能均匀分布在整个空间中

(3)哈希函数应比较简单

3、常见的哈希函数

(1)直接定址法

取关键字的某个线性函数为散列地址:Hash(key)= A*key + B

优点:简单,速度快,节省空间,查找key O(1)的时间复杂度

缺点:当数据范围大时会浪费空间,不能处理浮点数,字符串数据

使用场景:适用于整数,数据范围比较集中

例如计数排序,统计字符串中出现的用26个英文字符统计,给数组分配26个空间,遍历到的字符是谁,就把相应的元素值++

在这里插入图片描述

(2)除留余数法

把数据映射到有限的空间里面。设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将key转换成哈希地址。如第2节哈希表的例子。

哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

二、用闭散列解决哈希冲突

闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。 下一个位置怎样找呢?有以下两种常见方式:1、线性探测法 2、哈希桶

1、线性探测法介绍

(1)如下场景,要插入22,通过哈希函数hashfunc(22) = 22%10=2计算出的地址为2,2的位置已经有数据2了,现在发生了冲突:

在这里插入图片描述
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

①插入:通过哈希函数获取待插入元素在哈希表中的位置。如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

②删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,否则会影响其他元素的搜索。比如删除元素2,如果直接删除掉,22查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素,即给每个位置一个标记,用空(EMPTY)、存在(EXIST)、删除(DELETE)3种状态来区分。

(2)关于扩容,负载因子概念的引入:

负载因子= 哈希表中的有效数据个数 / hash的size()

在开放定址法中,负载因子最多在0.7~0.8之间,大于负载因子 就扩容!

在这里插入图片描述

2、线性探测法实现

(1)状态

区分哈希表的一个位置有没有数据,如果用两种状态表示,在(1)或不在(0),那么就会带来两个问题:

①0表示不在,那么如何存数据0呢?

②如果数据发生冲突,当前位置和后面位置都存放的是冲突数据,加入当前位置的数据被删除了,那么查找key时发现当前位置状态为不在,那么就不会再向后查找了。

因此要用3个状态位分别表示空、已占用、已删除,用枚举表示状态位:

// 关于:储存位置状态的类
	enum State// 标记存储位置的状态,为了便于查找
	{
		EMPTY,
		EXIST,
		DELETE
	};

(2)定义HashData

哈希数据应包含两个成员:数据和状态:

//  关于:所给数据的类
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY; //  初始化
	};

(3)哈希表

哈希表包含两个成员:哈希数据、存储的有效数据的个数

模板有3个参数K、V、HashFunc(仿函数)。

①由于不知道key是K还是pair,所以需要定义两个模板参数K、V来包含key是K或pair的两种情况

②由于不知道key的数据类型是int还是string、pair、struct,计算key的映射位置时需要取模,但是只能对int型取模,string、struct、pair无法取模,HashFunc作为仿函数,它的实例可以分别应对这些类型的取模。

    template<class K, class V, class HashFunc>
	class HashTable
	{
    private:
		vector<HashData<K, V>> _table;//哈希表
		size_t _n = 0;//存储有效数据的个数
	};

(4)插入

①先查看key查看在不在,在就插入失败

②第一次插入时,哈希表的的是0,所以第一次插入时就要让表扩容

③还需要判断负载因子是否>0.7,如果表满了,就要开一个新表,并把旧表的数据都插入到新表上

④当计算的位置有数据时,就向后探测,直到探测到空位置即可存入数据

	// 一、插入
		bool Insert(const pair<K, V>& kv)
		{
			// 防止数据冗余,重复的不能插进去!
			if (Find(kv.first))
				return false;


			// 1、但是如果负载因子超标了 就需要扩容!
			if (_n * 10 / _tables.size() >= 7) // 一般>=0,7就要扩容,因为_n/_tables.size()不会是浮点数,所以两边就要*10
			{
				// 现代写法:复用 insert 让他来帮我计算,重新映射数值所对应的位置

				HashTable<K, V> newHT;// 创建了一个新的哈希表:newHT
				newHT._tables.resize(_tables.size() * 2);

				for (size_t i = 0; i < _tables.size(); i++)// 把原来的hashtable里面的值,依次重新映射到newHT中
				{
					if (_tables[i]._state == EXIST)
					{
						newHT.Insert(_tables[i]._kv);// 为啥不是 直接插入:_tables[i]???
						// 因为 _tables[i]是HashData对象,里面包括:_kv和_state,而且insert函数接收的参数是kv,不是HashData
					}
				}

				_tables.swap(newHT._tables);

			}
			// 2、插入

			Hash hs;


			size_t hashi = hs(kv.first) % _tables.size();
			while (_tables[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _tables.size();// 防止下标超出范围,%后会让hashi不断在这个范围内循环!
			}

			// 走出循环后,说明已经找到了空位!
			_tables[hashi]._kv = kv;
			// 为啥不是这样写:_tables[hashi] = kv; _tables[hashi]和kv难道不是一个等级吗?
			// 因为_tables[hashi]是数据、是HashData对象里面包括:_kv和_state
			_tables[hashi]._state = EXIST;
			++_n;

			return true;
		}

在这里插入图片描述
在这里插入图片描述

(5)查找

①无论传给哈希表的数据是K还是pair,查找时,都需要用K做key来进行查找

②计算元素位置

③如果当前位置元素为key,那么就返回该元素,否则可能发生了冲突,继续向后探测

// 二、查找
		HashData<K, V>* Find(const K& key)
		{

			Hash hs;

			size_t hashi = hs(key) % _tables.size();
			// 正常情况下找到hashi就会找到其对应的值,由于hash冲突,不得不往后遍历
			while (_tables[hashi]._state != EMPTY)
			{
				// 因为下面的删除函数并不是 把数值真的删除了,而是标记了状态,所以为了避免删除后你还能找到,这里的if判断要加一个存在(EXIST)!
				if (_tables[hashi]._state == EXIST &&
					_tables[hashi]._kv.first == key)
					// 查找的时候,这个值必须  存在 && 相等 才算找到
				{
					return &_tables[hashi];
				}

				++hashi;
				hashi %= _tables.size();
			}
			return nullptr;
		}

(6)删除

利用假删除,将状态标记为删除即可:

// 三、删除
		bool Erase(const K& key)
		{
			// 这个删除函数,不是真正意义上的删除,只是对状态进行标记,数值并没有删除!
			HashData<K, V>* ret = Find(key);

			if (ret == nullptr)
			{
				return false;
			}
			else
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
		}

(7)仿函数

1、仿函数的目的是为了让不同类型的数据能够取模,方便计算数据位置

类的仿函数模板,默认支持int:

// 针对所给数据是:整数、负数、浮点数......(将数据强制转化为 size_t类型,才能%算下标) 的仿函数!!!
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

2、string类型的仿函数,不能用上述仿函数的类模板,因为字符不能取模。string类型的仿函数用来做key的数值尽量要找不重复的,否则会导致发生冲突的概率比较高


	// string转int的仿函数 (以后所说的:仿函数 都指的是一个类!因为仿函数的定义就是:使用起来像函数的一个类!)
	struct StringHashFunc
	{
		// BKDR

		size_t hash = 0;
		size_t operator()(const string& str)
		{
			for (auto e : str)
			{
				hash *= 131;// 每次遍历str中的一个字符就让 hash*131,最后再加起来!
				hash += e;
			}
			return hash;
		}
	};

任意类型(pair、结构体)都可以做key,key尽量选择不容易重复的成员,跟一个把这个类型对象转换成整形的仿函数。比如一个类型做map/set的key,那就要求该类型能支持比较大小。又比如一个类型做unordered_map/unordered_set的key,那就要求该类型能支持转换成整形+相等比较。

(8)完整代码段

namespace open_address
{
	// 关于:储存位置状态的类
	enum State// 标记存储位置的状态,为了便于查找
	{
		EMPTY,
		EXIST,
		DELETE
	};

	//  关于:所给数据的类
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY; //  初始化
	};


	// string转int的仿函数 (以后所说的:仿函数 都指的是一个类!因为仿函数的定义就是:使用起来像函数的一个类!)
	struct StringHashFunc
	{
		// BKDR

		size_t hash = 0;
		size_t operator()(const string& str)
		{
			for (auto e : str)
			{
				hash *= 131;// 每次遍历str中的一个字符就让 hash*131,最后再加起来!
				hash += e;
			}
			return hash;
		}
	};


	//  关于:哈希表的 类
	template<class K, class V, class Hash = HashFunc<K>>// 给一个缺省值,默认数据就是int类型
	class HashTable
	{

		// newHT 是哈希表整个类对象、_tables类成员变量(相当于数组)
	public:
		// 构造函数
		HashTable()
		{
			_tables.resize(10);
		}

		// 一、插入
		bool Insert(const pair<K, V>& kv)
		{
			// 防止数据冗余,重复的不能插进去!
			if (Find(kv.first))
				return false;


			// 1、但是如果负载因子超标了 就需要扩容!
			if (_n * 10 / _tables.size() >= 7) // 一般>=0,7就要扩容,因为_n/_tables.size()不会是浮点数,所以两边就要*10
			{
				// 现代写法:复用 insert 让他来帮我计算,重新映射数值所对应的位置

				HashTable<K, V> newHT;// 创建了一个新的哈希表:newHT
				newHT._tables.resize(_tables.size() * 2);

				for (size_t i = 0; i < _tables.size(); i++)// 把原来的hashtable里面的值,依次重新映射到newHT中
				{
					if (_tables[i]._state == EXIST)
					{
						newHT.Insert(_tables[i]._kv);// 为啥不是 直接插入:_tables[i]???
						// 因为 _tables[i]是HashData对象,里面包括:_kv和_state,而且insert函数接收的参数是kv,不是HashData
					}
				}

				_tables.swap(newHT._tables);

			}
			// 2、插入

			Hash hs;


			size_t hashi = hs(kv.first) % _tables.size();
			while (_tables[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _tables.size();// 防止下标超出范围,%后会让hashi不断在这个范围内循环!
			}

			// 走出循环后,说明已经找到了空位!
			_tables[hashi]._kv = kv;
			// 为啥不是这样写:_tables[hashi] = kv; _tables[hashi]和kv难道不是一个等级吗?
			// 因为_tables[hashi]是数据、是HashData对象里面包括:_kv和_state
			_tables[hashi]._state = EXIST;
			++_n;

			return true;
		}

		// 二、查找
		HashData<K, V>* Find(const K& key)
		{

			Hash hs;

			size_t hashi = hs(key) % _tables.size();
			// 正常情况下找到hashi就会找到其对应的值,由于hash冲突,不得不往后遍历
			while (_tables[hashi]._state != EMPTY)
			{
				// 因为下面的删除函数并不是 把数值真的删除了,而是标记了状态,所以为了避免删除后你还能找到,这里的if判断要加一个存在(EXIST)!
				if (_tables[hashi]._state == EXIST &&
					_tables[hashi]._kv.first == key)
					// 查找的时候,这个值必须  存在 && 相等 才算找到
				{
					return &_tables[hashi];
				}

				++hashi;
				hashi %= _tables.size();
			}
			return nullptr;
		}

		// 三、删除
		bool Erase(const K& key)
		{
			// 这个删除函数,不是真正意义上的删除,只是对状态进行标记,数值并没有删除!
			HashData<K, V>* ret = Find(key);

			if (ret == nullptr)
			{
				return false;
			}
			else
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n;// 表里面 有效数据个数
	};
}

三、用开散列解决哈希冲突

1、开散列(哈希桶)的介绍:

开散列也叫拉链法,先对所有key用散列函数计算散列地址,把有相同地址的key每个key都作为一个桶,通过单链表链接在哈希表中。

在这里插入图片描述
因此,开散列的每个桶中存放的都是哈希冲突的元素,负载因子较低。当桶超过一定长度时,就把冲突最多的桶就换成红黑树。实际中哈希桶的结构更实用,因为哈希桶空间利用率高,并且在极端情况下还有解决方案

2、哈希桶的实现

哈希桶作为指针数组,数组的每个元素是一个结点的指针,链表不需要带哨兵位,且头插的效率比较高。

(1)哈希仿函数

// 针对所给数据是:整数、负数、浮点数......
// (将数据强制转化为 size_t类型,才能%算下标) 的仿函数!!!
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

// 针对所给数据是:string(将string转化为size_t类型,才能%算下标!) 
// 利用字符串哈希算法(BKDR)

//模版的特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t hash = 0;// 临时变量 用来标记string中每个字符对应的ASC码值,
		// 相乘相加后得到 hash 并返回便于%
		for (auto e : str)
		{
			hash *= 131;// 字符串哈希算法之BKDR
			hash += e;
		}

		return hash;
	}
};


(2)哈希桶节点

哈希桶只需要2个成员:数据、下一个桶指针

就相当于单链表!!!

// 关于桶节点的类:HashNode
	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)
		{

		}
	};

(3)哈希表

哈希表有两个成员:哈希表、有效数据的个数

	template<class K,class V,class HashFunc=Hash<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
    private:
		vector<Node*> _table;//哈希表
		size_t _n;//有效数据个数
	};

(4)查找

先计算key在哈希表中的位置,然后后再该位置的哈希桶中遍历查找:

Node* Find(const K& key)
		{

			Hash hs;
			size_t hashi = hs(key) % _tables.size();

			Node* cur = _tables[hashi];
			while (cur)
			{
				// 找到了就返回!
				if (cur->_kv.first == key)
					return cur;
				// 没找到 就继续往下遍历!
				cur = cur->_next;
			}
			return nullptr;
		}

(5)插入

①查找key在不在哈希表中(去重)

②不在就要先判断哈希表是否满了(扩容)

③若哈希表满了就要重新开一个新的哈希表,将旧表数据全部头插到新表中

④插入数据

bool Insert(const pair<K, V>& kv)
		{
			// 扩容:
			// 负载因子==1的时候就扩容!


			 方法一:太浪费空间了,new10个空间,最后又释放了10个空间。
			//if (_n == _tables.size())
			//{
			//	HashTable<K, V> newHT;
			//	newHT._tables.resize(_tables.size() * 2);

			//	// 将旧表重新计算 负载到新表!
			//	for (size_t i = 0; i < _tables.size(); i++)
			//	{
			//		Node* cur = _tables[i];
			//		while (cur)
			//		{
			//			newHT.Insert(cur->_kv); // 复用
			//			cur = cur->_next;
			//		}
			//	}
			//	_tables.swap(newHT._tables);

			//}


			if(Find(kv.first))
				return false;

			// 方法二:遍历旧数组、直接将旧表的数据计算好位置,然后直接插入到新表。

			if (_n == _tables.size())
			{
				vector<Node*> new_table(_tables.size() * 2, nullptr);
				

				//遍历旧数组
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;// 先保存一下 next

						// 头插到新表相应位置!

						// 老套路:先计算位置
						size_t hashi = cur->_kv.first % new_table.size();
						// 头插:不要搞错了:cur是旧表中将要插入的数据!
						cur->_next = new_table[hashi];// new_table[hashi]是桶的第一个!!!
						cur= new_table[hashi];

						cur = next;// 继续对原数组进行遍历查找出所有带有桶节点的。

					}
					_tables[i] = nullptr;// 防止 有两个位置 指向同一个节点!对原数组的数据进行指向空
				}
				_tables.swap(new_table);

			}
			Hash hs;

			size_t hashi = hs(kv.first )% _tables.size();

			Node* newnode = new Node(kv);
			// 头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;

			++_n;
			return true;
		}

(6)删除

①计算key在表中的位置

②要删除的数据是不是该位置的第一个哈希桶,如果是,那就让哈希表的第一个节点变成第二个桶,否则让这个桶的前一个桶指向这个桶的下一个桶
(这就是:写while 和 判断的原因!)

bool Erase(const K& key)
		{
			// 删除就相当于在单链表中进行删除,需要保存前一个位置:prev

			// 大致分两种情况:①删除头结点 ②删除中间 和 尾结点
			

			Hash hs;

			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];

			while (cur)
			{
				if (cur->_kv.first == key)
				{
					// 可以删除了
					
					// ①如果删除的是头结点
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

关于 :删除的理解,主要是一定要搞清楚!变量的类型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(7)关于哈希桶的细节总结、以及注意事项

  1. 报错:无法将参数进行转换:就需要我们自己再写一个构造函数在这里插入图片描述

  2. 哈希桶为啥要自己写一个析构函数?在这里插入图片描述

  3. 为啥在哈希桶里面的扩容,要舍弃“线性探测”的扩容方法在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    所以我们直接采用方法二:
    哈希桶扩容采用,方法二:遍历旧数组、直接将旧表的数据计算好位置,然后直接插入到新表

在这里插入图片描述

(8)完整代码段

HashTable.h

namespace hash_bucket
{
	// 关于桶节点的类:HashNode
	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:
		vector<Node*> _tables; // 指针数组!
		size_t _n;

	public:
		// 构造
		HashTable()
		{
			_tables.resize(10, nullptr);
			_n = 0;
		}


		// 析构函数:针对于 自定义类型(实际上就是链表的析构)

		~HashTable()// 就遍历数组、看哪个地方有桶就对它进行delete
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}

				_tables[i] = nullptr;
			}
		}


		// 1、插入
		bool Insert(const pair<K, V>& kv)
		{
			// 扩容:
			// 负载因子==1的时候就扩容!


			 方法一:太浪费空间了,new10个空间,最后又释放了10个空间。
			//if (_n == _tables.size())
			//{
			//	HashTable<K, V> newHT;
			//	newHT._tables.resize(_tables.size() * 2);

			//	// 将旧表重新计算 负载到新表!
			//	for (size_t i = 0; i < _tables.size(); i++)
			//	{
			//		Node* cur = _tables[i];
			//		while (cur)
			//		{
			//			newHT.Insert(cur->_kv); // 复用
			//			cur = cur->_next;
			//		}
			//	}
			//	_tables.swap(newHT._tables);

			//}


			if(Find(kv.first))
				return false;

			// 方法二:遍历旧数组、直接将旧表的数据计算好位置,然后直接插入到新表。

			if (_n == _tables.size())
			{
				vector<Node*> new_table(_tables.size() * 2, nullptr);
				

				//遍历旧数组
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;// 先保存一下 next

						// 头插到新表相应位置!

						// 老套路:先计算位置
						size_t hashi = cur->_kv.first % new_table.size();
						// 头插:不要搞错了:cur是旧表中将要插入的数据!
						cur->_next = new_table[hashi];// new_table[hashi]是桶的第一个!!!
						cur= new_table[hashi];

						cur = next;// 继续对原数组进行遍历查找出所有带有桶节点的。

					}
					_tables[i] = nullptr;// 防止 有两个位置 指向同一个节点!对原数组的数据进行指向空
				}
				_tables.swap(new_table);

			}
			Hash hs;

			size_t hashi = hs(kv.first )% _tables.size();

			Node* newnode = new Node(kv);
			// 头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;

			++_n;
			return true;
		}


		// 2、查找:
		Node* Find(const K& key)
		{

			Hash hs;
			size_t hashi = hs(key) % _tables.size();

			Node* cur = _tables[hashi];
			while (cur)
			{
				// 找到了就返回!
				if (cur->_kv.first == key)
					return cur;
				// 没找到 就继续往下遍历!
				cur = cur->_next;
			}
			return nullptr;
		}


		// 3、删除:
		bool Erase(const K& key)
		{
			// 删除就相当于在单链表中进行删除,需要保存前一个位置:prev

			// 大致分两种情况:①删除头结点 ②删除中间 和 尾结点
			

			Hash hs;

			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];

			while (cur)
			{
				if (cur->_kv.first == key)
				{
					// 可以删除了
					
					// ①如果删除的是头结点
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}
	};
}

在这里插入图片描述

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小能软糖_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值