【C++】哈希表

1. 哈希表的概念

哈希(hash)又称散列,哈希就其实是英文有读音翻译过来的,他的意思就有变形什么的杂乱等翻译。哈希本质就是类似我们将已知东西由算法编辑成编码,定位到对应位置做到在O(1)直接访问内容的情况。

类似我是一个水果店老板,我们在一堆的清单中要找到对定位置水果的价格,我们利用哈希算法,例如:西瓜经过哈希算法就得出134,在清单中那就找到134位置就对应就是西瓜价格,如果是其他水果就不能出来的数值就不是134,就是其他的数值。

1.1 直接定址法

直接定址法是哈希构造的一种,适合关键字范围集中的场景,通过关键字映射数组下标,没有哈希冲突,访问效率 O (1)。哈希函数一般为 H (key)=key 或 H (key)=key - 基准值。例如小写字母,用字符 ASCII 减去 'a' 得到 0~25 的下标,开辟大小 26 的数组存放数据,计数排序也是这个思路。缺点是关键字范围很大但数据少时,会浪费大量空间。

cpp 复制代码
#include <stdio.h>
int main()
{
    int hash[26] = {0};
    char ch = 'c';
    int idx = ch - 'a';
    hash[idx] = 1;
    printf("%d", hash[idx]);
    return 0;
}

1.2 哈希冲突

及时哈希算法在怎么避免还是会出现,两个不一样的内容算出相同的数值,如果哈希表的可容纳内容太小的情况,出现哈希冲突的情况就越发明显,类似只有11个内容空间,有可能哈希算法计算出很多相同的位置 0 的内容。

1.3 负载因子

负载因子:哈希表已经映射在内的内容为 N 个,如果负载因子越多哈希冲突就越有可能,可容纳为 M 个,负载因子= N/M 。

1.4 将关键字转为整数

哈希表使用前,常需要把关键字转化为整数,得到哈希码,之后再通过除留余数等方法映射为哈希表下标。若关键字本身是整数,可直接作为哈希码;字符串、对象等类型则需要哈希函数计算。

1.5 哈希函数

哈希函数用于将关键字映射为哈希地址。先把关键字转换为整数哈希码,再通过除留余数法、平方取中法等得到表内下标。它是多对一映射,会产生哈希冲突,冲突可用链地址法、开放寻址法解决。设计目标是让地址均匀分布,减少冲突。

1.5.1 除法散列法/除法取余数法(最常用的)

  • 除法散列散列也叫做除法取余数法,假设哈希表的最大空间为 M ,key 为关键数值,哈希函数就是 H(key)= key % M ;
  • 哈希表的最大空间不是什么数值就可以的,M 一定要符合是质数,其中最要避免是 2^x,10^x 等。

1.6 处理哈希冲突

发生问题总要解决,发生哈希冲突也一样,也要对应的处理问题,就出现了对应以下三种解决问题。线性探索,二测探索 和 双重散列(了解)。

1.6.1 开放定地址法

线性探索

  • 从发生哈希冲突位置,往下找还有空闲出来的位置,有就插入映射,没有继续查找,到内容未就从头开始直到发生哈希冲突位置。
  • H(key)= hash0 = key % M,hash0 发生哈希冲突;
  • hc(key,i)= hashi = (hash0 + i)% M,i = {1,2,3,4,5,6,......, M-1}
  • 但这样也有坏处如果出现了 M 个内容哈希函数计算出来都是相同内容的时候,就会导致查找内容速度低下的情况,这种情况就叫堆积、集群。
cpp 复制代码
{2,16,83,55,79,34}

插入结果:

二测探索

该方法就比上一个方法进步了一定出现重复发生哈希冲突的可能就变少一点。

  • H(key)= hash0 = key % M,hash0 发生哈希冲突;
  • hc(key,i)= hashi = (hash0 + i^2)% M,i = {1,2,3,4,5,6,......, M-1}
  • hc(key,i)= hashi = (hash0 - i^2)% M,i = {1,2,3,4,5,6,......, M-1}

通过计算 i 的平方数的优先插入正数的 i^2 ,如果发生哈希冲突就尝试负数的 i^2

cpp 复制代码
{89,45,67,34,60,71}

插入结果:

双重散列(了解)

先比起上面的方法是对 + - 和 i 做出平方处理,这个方法就单纯对 i 进行很大的改动。

  • H(key)= hash0 = key % M,hash0 发生哈希冲突;
  • hc(key,i)= hashi = (hash0 + i * H2(key))% M,i = {1,2,3,4,5,6,......, M-1}
  • H2(key) = key % (M - 1)+ 1

1.6.2 开放顶定址代码实现

开放定址法的哈希表结构

cpp 复制代码
	enum State // 状态
	{
		EXIST,// exist  存在
		EMPTY,// empty  为空
		DELETE// delete 删除

	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

扩容

扩容逻辑(核心)

  1. 插入前检查 (size + 1) / capacity > 0.75,满足则调用 resize(capacity * 2)

  2. resize 内 calloc 新桶数组;分配失败则保留旧表返回 -1

  3. 遍历旧桶,对每个节点按新容量重新计算索引并复用节点头插/尾插到新桶

  4. 替换 buckets、更新 capacity,释放旧桶数组(不释放节点)

  5. 查找/删除命中后链表断裂时正确 free 节点并 size--

key 不能取模问题

  1. C++ 里哈希函数算出的 hash 可能为负,不能直接 hash % cap,会得到负下标,数组越界。
  2. 安全写法:(hash % cap + cap) % cap,保证下标落在 [0, cap-1]。
  3. 若桶容量是 2 的幂,可用 hash & (cap - 1) 代替取模,更快;质数容量只能取模。
  4. 字符串 / 自定义对象 key:先算出 hash 整数,再求下标,key 本身不能取模。

所以需要特殊情况特殊处理,负值取负符号 size_t ,string 的取char 的元素进行取值相加得出 hash。

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;
	}
};

完整代码

cpp 复制代码
#pragma once
#include <string>
#include <vector>
#include <iostream>
using namespace std;

// 请完成哈希表的如下操作
// 哈希函数采用除留余数法
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;
	}
};

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 

 
// 以下采用开放定址法,即线性探测解决冲突
namespace open_address
{
	enum State
	{
		EXIST,// exist  存在
		EMPTY,// empty  为空
		DELETE// delete 删除

	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			Hash  hash;
			if (_n == _tables.size())
			{
				return false;
			}
			if (Find(kv.first) != nullptr)
			{
				return false;
			}
			size_t hash0 = hash(kv.first) % _tables.size();
			size_t i = 0;
			while (i != _tables.size() && _tables[(hash0 + i) % _tables.size()]._state != EMPTY)
			{
				i++;
			}
			if (_tables[hash0 + i]._state != EXIST)
			{
				_tables[hash0 + i]._kv = kv;
				_tables[hash0 + i]._state = EXIST;
				_n++;
				return true;
			}
			else
			{
				return false;
			}
		}
		HashData<K, V>* Find(const K& key)
		{
			Hash hash;
			size_t hash0 = hash(key) % _tables.size();
			size_t i = 0;
			while (_tables[(hash0 + i) % _tables.size()]._state != EXIST && i != _tables.size())
			{
				i++;
			}
			if (_tables[hash0 + i]._state == EXIST)
			{
				return &_tables[hash0 + i];
			}
			else
			{
				return nullptr;
			}
		}
		bool Erase(const K& key)
		{
			Hash hash;
			size_t hash0 = hash(key) % _tables.size();
			size_t i = 0;
			while (_tables[(hash0 + i) % _tables.size()]._state != EXIST && i != _tables.size())
			{
				i++;
			}
			if (_tables[hash0 + i]._state == EXIST)
			{
				_tables[hash0 + i]._state = DELETE;
				_n--;
				return true;
			}
			else
			{
				return false;
			}
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;  // 表中存储数据个数
	};
}

1.6.3 链接址法

解决代码冲突的思路

与前面的思想不同,该哈希表每一个成员都是一个 单向 list 的 头结点,发生哈希冲突就往节点构造新的节点

1.6.4 链地址法代码实现

cpp 复制代码
namespace hash_bucket
{
	// Node 节点
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;
		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{
		}
	};
	template<class K,class T>
	struct KeyFunc
	{
		const K& operator()(const T& kv)
		{
			return kv.first;
		}
	};
	// K 为 T 中key的类型
	// T 可能是键值对,也可能是K
	// KeyOfT: 从T中提取key
	// Hash将key转化为整形,因为哈市函数使用除留余数法
	template<class K, class T,class KeyOfT = KeyFunc<K,T>, class Hash = HashFunc<K>>
	class HashTable
	{

		typedef HashNode<T> Node;
	public:
		HashTable()
		{
			_tables.resize(10, nullptr);
		}

		// 哈希桶的销毁
		~HashTable()
		{
			for (auto ch : _tables)
			{
				Node* next = ch,*last;
				while (next)
				{
					last = next;
					next = next->_next;
					delete last;
				}
			}
			_tables.~vector();
		}

		// 插入值为data的元素,如果data存在则不插入
		bool Insert(const T& data)
		{
			Hash hash;
			KeyOfT Key;
			if (Find(Key(data)))
			{
				return false;
			}
			size_t hash0 = hash(Key(data)) % _tables.size();
			if (_tables[hash0] == nullptr)
			{
				_tables[hash0] = new Node(data);
				_n++;
				return true;
			}
			Node* next = _tables[hash0];
			while (next->_next != nullptr)
			{
				next = next->_next;
			}
			next->_next = new Node(data);
			_n++;
			return true;
		}

		// 在哈希桶中查找值为key的元素,存在返回true否则返回false
		bool Find(const K& key)
		{
			Hash hash;
			KeyOfT Key;
			size_t hash0 = hash(key) % _tables.size();
			Node* find = _tables[hash0];
			while (find != nullptr)
			{
				if (Key(find->_data) == key)
				{
					return true;
				}
				else
				{
					find = find->_next;
				}
			}
			return false;
		}

		// 哈希桶中删除key的元素,删除成功返回true,否则返回false
		bool Erase(const K& key)
		{
			Hash hash;
			KeyOfT Key;
			size_t hash0 = hash(key) % _tables.size();
			Node* next = _tables[hash0], * last = nullptr;
			while (next != nullptr)
			{
				if (Key(next->_data) == key && next == _tables[hash0])
				{
					_tables[hash0] = next->_next;
					delete next;
					_n--;
					return true;
				}
				else if (Key(next->_data) == key)
				{
					last->_next = next->_next;
					delete next;
					_n--;
					return true;
				}
				else
				{
					last = next;
					next = next->_next;
				}
			}
			return false;
		}

	private:
		vector<Node*> _tables;  // 指针数组
		size_t _n = 0;			// 表中存储数据个数
	};
}
相关推荐
地平线开发者1 小时前
点云算法整理与常用计算模块:从数据到模型,再到工程与量化
算法·自动驾驶
Severus_black2 小时前
【C++初阶】list使用及相关实现,思路顺滑流畅!
c语言·数据结构·c++·stl·list
武子康3 小时前
H3-World 为何在加载 LoRA 前,先检查导入的代码?
人工智能·深度学习·算法
秋天的怀念_David4 小时前
算法一千零一夜 · 第二十八夜 注册
java·程序人生·算法
敲上瘾4 小时前
Qt Widgets入门:项目模板解析与Hello World
开发语言·数据库·c++·qt·ui·qt6.3
旖旎夜光5 小时前
【C++ 基础入门】C++ 多态(中):虚函数表、多态原理与动静绑定
开发语言·c++·笔记·学习
NEFU_Jyj6 小时前
贝尔数(组合数学)
算法
希和20276 小时前
C语言程序设计题|有序数组插入数字冒泡排序
c语言·数据结构·算法
旖旎夜光6 小时前
【C++ 基础入门】C++多态(下):单/多继承虚表、多态补充与 21 道高频题
开发语言·c++·笔记·学习