数据结构(十一) 哈希表

目录

哈希

哈希冲突

解决哈希冲突

[闭散列 --- 开放地址法](#闭散列 --- 开放地址法)

[开散列 --- 链地址法](#开散列 --- 链地址法)

哈希表的实现

[闭散列 --- 开放地址法](#闭散列 --- 开放地址法)

[开散列 --- 链地址法(哈希桶)](#开散列 --- 链地址法(哈希桶))


哈希

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树查找的时间复杂度为为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数。

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

插入元素时:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。

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

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

eg: 数据集合 {1,4,5,6,7,9},使用哈希函数将元素映射到哈希表的相应位置,那么查找元素的时候,只需要再次使用哈希函数就可以在O(1)复杂度内找到元素

哈希冲突

不同关键字通过相同哈希函数计算出相同的哈希地址,这种现象称为哈希冲突或哈希碰撞

例如在上面的数据集合中如果再加入 44,使用哈希函数计算存储位置:44 % 10 = 4,此时 4 和 44 计算出的存储位置是相同的,这就是哈希冲突

引起哈希冲突的一个重要原因就是哈希函数设计的不够合理,哈希函数的设计原则:

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

● 哈希函数计算出来的地址应尽可能均匀分布在整个空间中

● 哈希函数应该比较简单

常见的哈希函数

1.直接定址法

● 公式:Hash(key) = a * key + b (a,b为常数)

● 特点:简单、均匀、无冲突

● 局限:要求关键码的分布范围小且连续,若范围很大或不连续,会造成巨大的空间浪费

2.除留余数法

● 公式:Hash(key) = key % p (p通常为哈希表的大小 capacity)

● 特点:简单有效,是其他许多哈希函数的基础步骤

● 关键特点:为了减少关键码的规律性(如递增序列)导致的"聚集"现象,模数 p 最好取一个质数,且最好远离2的幂次数

3.平方取中法

● 步骤:① 将关键码平方;② 取平方结果的中间几位作为哈希地址。

● 特点:适用于关键码的每一位取值都不够均匀或不知道关键码分布的情况。平方后的中间几位通常与关键码的所有位都相关,分布更均匀。

● 示例:key = 123,平方得 123 ^ 2 = 15129,取中间三位 512 作为哈希地址。

4.折叠法

● 步骤:① 将关键码平方;② 取平方结果的中间几位作为哈希地址。

● 特点:适用于关键码的每一位取值都不够均匀或不知道关键码分布的情况。平方后的中间几位通常与关键码的所有位都相关,分布更均匀。

● 示例:key = 123,平方得 123 ^ 2 = 15129,取中间三位 512 作为哈希地址。

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

解决哈希冲突

解决哈希冲突有两种主要的方式:闭散列和开散列

闭散列 --- 开放地址法

当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置的下一个空位置中去。那么如何去找下一个空位置呢?

1. 线性探测

从发生冲突的位置开始,依次挨着向后探测,直到寻找到下一个空位置为止

hash_i = (hash(key) + i) % capacity(i = 0,1,2,...)

**eg:**数据集合 {1,4,5,6,7,9},使用哈希函数将元素映射到哈希表的相应位置,此时再插入 44,计算出存储位置为 44 % 10 = 4,发现下标为4的位置已经有元素了,向后探测,5位置也有元素了,向后探测,直到找到8位置为空,就把 44 存放到 8 位置

**优点:**实现简单

**缺点:**容易产生"一次聚集",连续被占用的位置会形成越来越长的区块,后续的关键码很容易堆积在区块的末尾,导致插入以及查找效率下降。

负载因子

通过线性探测的例子,我们可以发现,随着哈希表中元素的增多,再插入新元素时产生冲突的概率就会明显上升,因此为了降低哈希冲突的概率,需要控制一下哈希表中元素的数量,我们引入了"负载因子"这一概念**,负载因子 = 已存储元素数量 / 哈希表容量,**衡量的是哈希表的装满程度,例如:

● 哈希表容量为100,已存储70个元素 → 负载因子 = 0.7

● 哈希表容量为100,已存储50个元素 → 负载因子 = 0.5

负载因子本质是在时间复杂度和空间复杂度之间取得一个平衡,通过大量的实验和统计,人们发现负载因子在0.7-0.75左右时,哈希表的空间利用率和时间效率达到一个较好的平衡

● 负载因子过大,哈希表中元素过多,冲突概率提高,插入和查找的时间复杂度增加

● 负载因子过小,大量位置没有存放元素,造成空间浪费,空间复杂度增加

2. 二次探测

依旧是从发生冲突的位置开始,向后探测,直到寻找到下一个空位置为止,但探测距离按平方数增长,hash_i = (hash(key) +/- i^2) % capacity,通常取正,即 hash(key) + 1^2,+ 2^2,+ 3^2...

**优点:**缓解了线性探测的"一次聚集"问题

缺点: 也会有"二次聚集"问题:不同关键码如果初始哈希值相同,它们的探测序列将完全一样**;** 可能无法找到空位:即使表中存在空位,二次探测的序列也可能无法覆盖所有空位置,为了保证能探测到所有位置,哈希表的大小最好是一个质数

eg:插入44时映射到位置 4 产生冲突,使用二次探测 下一个探测位置是 4 + 1^2 = 5,依旧冲突,继续探测,下一个位置是 4 + 1^2 = 8,没有冲突,因此最终 44 存放在了 位置 8

开散列 --- 链地址法

首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。因此开散列 / 链地址法 解决哈希冲突所使用的结构我们经常也称作哈希桶

当插入44时,计算44的映射位置:44 % 10 = 4,发现已经有元素了,然后将4头插到已经存在的链表中(尾插还要找尾),由于哈希桶的每个位置是一个链表,理论上只要链表可以无限延伸,那么就可以无限插入元素,因此哈希桶负载因子可以很大,虽然负载因子可以很大,但是随着负载因子的增大,查找性能会下降,因此也要设置负载因子的阈值,达到阈值时进行扩容

哈希桶在极端情况下,所有元素全部挂在一个桶下,查找的时间复杂度会退化为 O(N),此时有一种常见的解决方案是将单链表替换为红黑树,红黑树的根节点存储在哈希表中

但实际上,将 链表替换为红黑树的 场景基本很少,因为我们还有负载因子兜底,当负载因子到达一定阈值后,会先进行哈希表的扩容,扩容之后元素大概率会被进一步打散分布,单个链表太长的情况基本就不会出现了,只有当哈希函数本身设计不太合理时可能才会触发该机制

哈希表的实现

闭散列 --- 开放地址法

哈希表的整体结构

哈希表中的每个位置除了存储元素本身外,还应该存储该位置的状态,我们使用枚举实现

cpp 复制代码
enum Status
{
	EMPTY, //该位置为空, 没有存储元素
	EXIST, //该位置存在元素
	DELETE //该位置元素被删除了
};

为啥需要标识每个位置的状态呢?比如查找的时候,先哈希计算位置,从该位置开始向后线性探测,我们不可能将剩余的哈希表全部遍历一遍,这就失去了哈希表的意义,而是找到了该元素 或者 遇到了空位置就该结束(说明元素在哈希表中不存在)

为啥查找遇到空位置就说明元素不存在呢?因为插入元素时产生冲突我们采取的策略是向后线性探测,直到找到一个空位置就将冲突元素存在该位置,因此查找的逻辑和插入本质是一样的,冲突元素不可能存储在从映射位置向后探测到的第一个空位置之后了!

eg:

查找3:3 % 10 = 3,开始线性探测,发现3位置是 存在状态 并且 和 查找元素相等,找到了!

查找5:5 % 10 = 5,开始线性探测,5位置不为空且和查找元素不相等,再向后查找,6位置为空,查找结束,说明5在哈希表中不存在,反过来考虑,如果5存在于哈希表中,必定存放在6位置,不可能存放在 7以及往后的位置了!

那只要 EMPTY 和 EXIST 两个状态可以吗?是不行的,比如我们先找到 33 元素,然后进行了删除操作,由于只有 EMPTY 和 EXIST 两个状态,因此此时将 4 位置状态 变为 EMPTY,如果此时再去查找元素43,先映射位置,43 % 10 = 3,3位置元素和 查找元素不相等,向后线性探测,此时4位置状态为空,那么就停止了,判定 元素43 在哈希表中不存在,这就出错了!

因此还需要引入一个状态,就是 DELEET,删除完某个位置元素后,就将该位置状态置为 DELETE,查找的时候遇到 DELELE 状态也要继续向后查找,只有为空的时候才能判定查找的元素不存在!

因此,闭散列的哈希表中的每个位置应该存储 数据 和 该位置的状态

cpp 复制代码
template<class K, class V>
struct HashData
{
	pair<K, V> _kv; //存储<key, value>映射关系, 只存储key也可以(对应unordered_map 和 unordered_set)
	Status _s; //状态
};

上面提到,哈希表需要用到负载因子,因此我们再加入一个变量表示哈希表当前存储的元素个数

cpp 复制代码
//哈希表的定义
template<class K, class V>
class HashTable
{
public:
	//构造函数
	HashTable()
	{
		_tables.resize(10); //最开始开10个大小的空间
	}
private:
	vector<HashData<K, V>> _tables; //哈希表
	size_t _n = 0; //存储的关键字的个数(用于控制负载因子)
};

注意: 到现在为止,我们上述举的例子都是哈希表中存储的是整数,因此除留余数法找关键字存储位置时可以直接取模,但如果存储的是字符串呢,我们需要先将字符换转换成整数,再去取模,而将字符换转换成整数,有一套算法,就是字符串哈希算法 字符串哈希算法,而我们自己模拟实现就采取比较简单的做法了

cpp 复制代码
//哈希函数(默认, 整数)
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)
	{
		size_t hash = 0;
		for (auto& e : key)
		{
			hash *= 31; //解决字母完全一样但是顺序不一样而导致冲突的问题
			hash += e;
		}
		return hash;
	}
};

//仿函数
struct HashFuncString
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto& e : key)
		{
			hash *= 31; //解决字母完全一样但是顺序不一样而导致冲突的问题
			hash += e;
		}
		cout << key << ":" << hash << endl;
		return hash;
	}
};

namespace open_address
{
	//标识哈希表某个位置的状态
	enum Status
	{
		EMPTY, //空
		EXIST, //存在
		DELETE //删除
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv; //存储<key, value>映射关系, 只存储key也可以(对应unordered_map 和 unordered_set)
		Status _s; //状态
	};

	//哈希表的定义
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		//构造函数
		HashTable()
		{
			_tables.resize(10); //最开始开10个大小的空间
		}
	private:
		vector<HashData<K, V>> _tables; //哈希表
		size_t _n = 0; //存储的关键字的个数(用于控制负载因子)
	};
}

哈希表插入元素

特别注意,哈希表扩容之后,之前的映射关系就会被打乱,需要重新计算映射位置!

cpp 复制代码
bool Insert(const pair<K, V> &kv)
{
    if (Find(kv.first)) return false; //1.哈希表中不允许出现重复的值

    // 2.负载因子达到阈值, 进行扩容
    if (_n * 10 / _tables.size() == 7)
    {
        size_t newSize = _tables.size() * 2;
        HashTable<K, V, Hash> newHT;
        newHT._tables.resize(newSize); // 将新的哈希表扩容到newSize;
        // 遍历旧表,重新映射值的关系
        for (size_t i = 0; i < _tables.size(); i++)
        {
            if (_tables[i]._s == EXIST)
            {
                newHT.Insert(_tables[i]._kv); // 不是递归,只是复用代码
            }
        }
        // 创建完新的哈希表之后,和旧的哈希表进行交换即可!
        _tables.swap(newHT._tables);
    }

    // 3.插入逻辑
    Hash hf;
    size_t hashi = hf(kv.first) % _tables.size(); // 注意不是取模capaicty, 否则越界访问直接报错
    while (_tables[hashi]._s == EXIST) // 存在值就找下一个位置,为空/删除就可以放值
    {
        hashi++;                 // 线性探测法
        hashi %= _tables.size(); // 走到结尾之后
    }
    _tables[hashi]._kv = kv;   // 找到空/删除位置之后放值
    _tables[hashi]._s = EXIST; // 修改状态成存在
    ++_n;                      // 关键字的个数++
    return true;
}

哈希表查找元素

cpp 复制代码
HashData<K, V>* Find(const K &key)
{
    Hash hf; 
    size_t hashi = hf(key) % _tables.size(); //映射位置

    while (_tables[hashi]._s != EMPTY) // 存在状态和删除状态都要继续往后找
    {
        // 必须先要判断是否存在,否则会导致
        // 1.该位置元素已经删除了, 还返回该位置指针, 查找逻辑出错
        // 2.该位置删除之后再想插入元素无法插入(因为插入函数开始就会查找该元素决定是否能插入)
        if (_tables[hashi]._s == EXIST && _tables[hashi]._kv.first == key)
        {
            return &_tables[hashi];
        }
        hashi++;                 // 线性探测法
        hashi %= _tables.size(); // 走到结尾之后
    }
    return nullptr;
}

哈希表删除元素

cpp 复制代码
// 伪删除法, 只是要删除的位置标记为 DELETE
bool Erase(const K &key)
{
    HashData<K, V> *ret = Find(key);
    if (ret) //找到了
    {
        ret->_s = DELETE;
        --_n;
        return true;
    }
    else
    {
        return false;
    }
}

测试哈希表逻辑

在 HashTable 类中加入 打印函数:

cpp 复制代码
void Print()
{
	for (size_t i = 0; i < _tables.size(); i++)
	{
		if (_tables[i]._s == EXIST)
		{
			cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
		}
		else if (_tables[i]._s == EMPTY)
		{
			printf("[%d]->\n", i);
		}
		else
		{
			printf("[%d]->D\n", i);
		}
	}
	cout << endl;
}

测试函数:

cpp 复制代码
#include "HashTable.h"

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

	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[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//open_address::HashTable<string, int, HashFuncString> ht; //仿函数
	open_address::HashTable<string, int> ht; //类模版特化
	for (auto& e : arr)
	{
		//auto ret = ht.Find(e);
		open_address::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();
}

int main()
{
	TestHT1();
	TestHT2();
	return 0;
}

附开放地址法实现完整代码

cpp 复制代码
//哈希函数(默认, 整数)
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)
	{
		size_t hash = 0;
		for (auto& e : key)
		{
			hash *= 31; //解决字母完全一样但是顺序不一样而导致冲突的问题
			hash += e;
		}
		return hash;
	}
};

//仿函数
struct HashFuncString
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto& e : key)
		{
			hash *= 31; //解决字母完全一样但是顺序不一样而导致冲突的问题
			hash += e;
		}
		cout << key << ":" << hash << endl;
		return hash;
	}
};

namespace open_address
{
	//标识哈希表某个位置的状态
	enum Status
	{
		EMPTY, //空
		EXIST, //存在
		DELETE //删除
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv; //存储<key, value>映射关系, 只存储key也可以(对应unordered_map 和 unordered_set)
		Status _s = EMPTY; //状态
	};

	//哈希表的定义
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		//构造函数
		HashTable()
		{
			_tables.resize(10); //最开始开10个大小的空间
		}

		//插入元素
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first)) return false; //1.哈希表中不允许出现重复的值

			// 2.负载因子达到阈值, 进行扩容
			if (_n * 10 / _tables.size() == 7)
			{
				size_t newSize = _tables.size() * 2;
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(newSize); // 将新的哈希表扩容到newSize;
				// 遍历旧表,重新映射值的关系
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._s == EXIST)
					{
						newHT.Insert(_tables[i]._kv); // 不是递归,只是复用代码
					}
				}
				// 创建完新的哈希表之后,和旧的哈希表进行交换即可!
				_tables.swap(newHT._tables);
			}

			// 3.插入逻辑
			Hash hf;
			size_t hashi = hf(kv.first) % _tables.size(); // 注意不是取模capaicty, 否则越界访问直接报错
			while (_tables[hashi]._s == EXIST) // 存在值就找下一个位置,为空/删除就可以放值
			{
				hashi++;                 // 线性探测法
				hashi %= _tables.size(); // 走到结尾之后
			}
			_tables[hashi]._kv = kv;   // 找到空/删除位置之后放值
			_tables[hashi]._s = EXIST; // 修改状态成存在
			++_n;                      // 关键字的个数++
			return true;
		}

		//查找元素
		HashData<K, V>* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size(); //映射位置
			while (_tables[hashi]._s != EMPTY) // 存在状态和删除状态都要继续往后找
			{
				// 必须先要判断是否存在,否则会导致
				// 1.该位置元素已经删除了, 还返回该位置指针, 查找逻辑出错
				// 2.该位置删除之后再想插入元素无法插入(因为插入函数开始就会查找该元素决定是否能插入)
				if (_tables[hashi]._s == 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) // 找到了就将状态改为Delete
			{
				ret->_s = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}

		void Print()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._s == EXIST)
				{
					cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
				}
				else if (_tables[i]._s == EMPTY)
				{
					printf("[%d]->\n", i);
				}
				else
				{
					printf("[%d]->D\n", i);
				}
			}
			cout << endl;
		}
	private:
		vector<HashData<K, V>> _tables; //哈希表
		size_t _n = 0; //存储的关键字的个数(用于控制负载因子)
	};
}
开散列 --- 链地址法(哈希桶)

哈希桶结构

cpp 复制代码
namespace hash_bucket
{
	template<class K, class V>
	class HashNode
	{
	public:
		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()
		{
			_tables.resize(10);
		}

		//析构函数(Node*是内置类型,编译器不做处理, 因此需要手动写析构函数)
		~HashTable()
		{
			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;
			}
		}
	private:
		vector<Node*> _tables;
		size_t _n; //当前哈希表中的元素个数
	};
}

查找函数

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

插入函数

依旧先检查要插入的元素在哈希表中是否已经存在,存在则插入失败,直接返回 false

cpp 复制代码
if (Find(kv.first)) return false;

插入元素之前要先判断是否需要扩容,哈希桶的扩容有两种方式:

**方式一:**和开放地址法的扩容逻辑一样,复用 Insert 函数 让原哈希表的节点插入新哈希表中,但复用 Insert 函数会重新开辟很多节点,还要将旧表的节点全部释放了,消耗很大

cpp 复制代码
if (_n == _tables.size()) //哈希桶的负载因子我们设置为1
{
	size_t newSize = _tables.size() * 2;
	HashTable<K, V> newHT;
	newHT._tables.resize(newSize);
	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);
}

**方式二:**遍历旧表的元素,直接头插到新表上,然后交换两个 vector 即可

cpp 复制代码
if (_n == _tables.size()) //哈希桶的负载因子我们设置为1
{
	size_t newSize = _tables.size() * 2;
	vector<Node*> newTables;
	newTables.resize(newSize);
	for (size_t i = 0; i < _tables.size(); i++)
	{
		Node* cur = _tables[i];
		while (cur)
		{
			Node* next = cur->_next;
			size_t hashi = hf(cur->_kv.first) % newSize;
			cur->_next = newTables[hashi];
			newTables[hashi] = cur;
			cur = next;
		}
		_tables[i] = nullptr;
	}
	_tables.swap(newTables);
}

节点的插入可以 头插 / 尾插,但尾插还要找尾,时间复杂度较高,我们直接选择头插

cpp 复制代码
size_t hashi = hf(kv.first) % _tables.size();
Node* newNode = new Node(kv);
newNode->_next = _tables[hashi];
_tables[hashi] = newNode;
++_n;
return true;

删除函数

cpp 复制代码
bool Erase(const K &key)
{
    Hash hf;
    size_t hashi = hf(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;
        }
        prev = cur;
        cur = cur->_next;
    }
    return false;
}

测试哈希桶逻辑

在 HashTable 类中加入 打印函数:

cpp 复制代码
void Print()
{
	for (int i = 0; i < _tables.size(); i++)
	{
		printf("[%d]: ", i);
		Node* cur = _tables[i];
		while (cur)
		{
			cout << "->[" << cur->_kv.first << ":" << cur->_kv.second << "]";
			cur = cur->_next;
		}
		printf("->NULL\n");
	}
	cout << endl;
}

测试函数:

cpp 复制代码
#include "HashTable.h"

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

	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[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//hash_bucket::HashTable<string, int, HashFuncString> ht; //仿函数
	hash_bucket::HashTable<string, int> ht; //类模版特化
	for (auto& e : arr)
	{
		//auto ret = ht.Find(e);
		hash_bucket::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();
}

int main()
{
	TestHT1();
	TestHT2();
	return 0;
}

附链地址法实现完整代码

cpp 复制代码
//哈希函数(默认, 整数)
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)
	{
		size_t hash = 0;
		for (auto& e : key)
		{
			hash *= 31; //解决字母完全一样但是顺序不一样而导致冲突的问题
			hash += e;
		}
		return hash;
	}
};

//仿函数
struct HashFuncString
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto& e : key)
		{
			hash *= 31; //解决字母完全一样但是顺序不一样而导致冲突的问题
			hash += e;
		}
		cout << key << ":" << hash << endl;
		return hash;
	}
};

//哈希桶
namespace hash_bucket
{
	template<class K, class V>
	class HashNode
	{
	public:
		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()
		{
			_tables.resize(10);
		}

		//析构函数(Node*是内置类型,编译器不做处理, 因此需要手动写析构函数)
		~HashTable()
		{
			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;
			}
		}
		
		//查找函数
		Node* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			Node* cur = _tables[hashi]; 
			while (cur)
			{
				if (cur->_kv.first == key)
					return cur;
				cur = cur->_next;
			}
			return nullptr;
		}

		//插入函数(扩容方式一)
		//bool Insert(const pair<K, V>& kv)
		//{
		//	if (Find(kv.first)) return false;
		//	Hash hf;
		//	//扩容方式一:
		//	if (_n == _tables.size()) //哈希桶的负载因子我们设置为1
		//	{
		//		size_t newSize = _tables.size() * 2;
		//		HashTable<K, V> newHT;
		//		newHT._tables.resize(newSize);
		//		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);
		//	}
		//	//头插新节点
		//	size_t hashi = hf(kv.first) % _tables.size();
		//	Node* newNode = new Node(kv);
		//	newNode->_next = _tables[hashi];
		//	_tables[hashi] = newNode;
		//	++_n;
		//	return true;
		//}

		//插入函数(扩容方式二)
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first)) return false;
			Hash hf;
			//扩容方式二:
			if (_n == _tables.size()) //哈希桶的负载因子我们设置为1
			{
				size_t newSize = _tables.size() * 2;
				vector<Node*> newTables;
				newTables.resize(newSize);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hf(cur->_kv.first) % newSize;
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}
			//头插新节点
			size_t hashi = hf(kv.first) % _tables.size();
			Node* newNode = new Node(kv);
			newNode->_next = _tables[hashi];
			_tables[hashi] = newNode;
			++_n;
			return true;
		}

		//删除函数
		bool Erase(const K& key)
		{
			Hash hf;
			size_t hashi = hf(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;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		//打印函数
		void Print()
		{
			for (int i = 0; i < _tables.size(); i++)
			{
				printf("[%d]: ", i);
				Node* cur = _tables[i];
				while (cur)
				{
					cout << "->[" << cur->_kv.first << ":" << cur->_kv.second << "]";
					cur = cur->_next;
				}
				printf("->NULL\n");
			}
			cout << endl;
		}
	private:
		vector<Node*> _tables;
		size_t _n; //当前哈希表中的元素个数
	};
}
相关推荐
Bdygsl2 小时前
数据结构 —— 栈
数据结构
客梦2 小时前
数据结构--队列
数据结构·笔记
xiaoxue..2 小时前
二叉搜索树 BST 三板斧:查、插、删的底层逻辑
javascript·数据结构·算法·面试
程序员小白条2 小时前
提前实习的好处有哪些?有坏处吗?
java·开发语言·数据结构·数据库·链表
蒙奇D索大2 小时前
【数据结构】排序算法精讲 | 快速排序全解:分治思想、核心步骤与示例演示
数据结构·笔记·学习·考研·算法·排序算法·改行学it
七夜zippoe2 小时前
Python高级数据结构深度解析:从collections模块到内存优化实战
开发语言·数据结构·python·collections·内存视图
sin_hielo11 小时前
leetcode 2483
数据结构·算法·leetcode
大头流矢12 小时前
归并排序与计数排序详解
数据结构·算法·排序算法
一路往蓝-Anbo12 小时前
【第20期】延时的艺术:HAL_Delay vs vTaskDelay
c语言·数据结构·stm32·单片机·嵌入式硬件