C++_哈希表

本篇文章是对C++学习的哈希表部分的学习分享

相信一定会对你有所帮助~

那咱们废话不多说,直接开始吧!


一、基础概念

1. 哈希核心思想:

  • 哈希函数的作用:通过此函数建立一个Key与存储位置之间的映射关系。
  • 理想目标:实现O(1)时间复杂度的查找

2.直接定址法

本质:⽤关键字计算出⼀个绝对位置或者相对位置

适用场景:Key 范围集中(如 [0,99]

二、关键问题与解决方案

1.哈希冲突:

  • 根本原因:不同 Key 映射到同一位置

  • 负载因子(Load Factor):α = N/M(N为已映射存储的值,M为哈希表的大小)

    • α↑ → 冲突概率↑,空间利用率↑

    • α↓ → 冲突概率↓,空间利用率↓

2. 哈希函数设计原则:

  • 目标:均匀分布、减少冲突

  • 除法散列法 / 除留余数法(重点)

    • h(key) = key % M

    • M 的选择:避免 2^n10^n,因为 key%M 会仅保留 key 的最后 n 位(二进制或十进制),导致不同 key 可能映射到同一位置。例如:

M=16(2^4)时,63(00111111)和31(00011111)的后4位均为 1111,哈希值均为15。

M=100(10^2)时,112和12312的后两位均为12,哈希值相同。

  • 理论上建议选择远离 2^n 的质数作为哈希表大小 M,以减少冲突。但实践中可灵活优化,如 Java 的 HashMap 采用 2^16 作为 M,通过位运算((key ^ (key >> 16)) & (M-1))替代取模,既提升效率又让高位参与计算,分散哈希值。核心在于均匀分布,而非机械套用理论。

  • 其他方法(了解):乘法散列法、全域散列法

3. 非整数Key的处理

有些数据类型无法直接用整形的哈希函数,比如string字符串类型,这时我们便可以尝试将字符串转证书(BKDR哈希思路)

cpp 复制代码
size_t hash = 0;
for (char c : str) {
    hash = hash * 131 + c;  // 质数 131 减少冲突
}

三、 冲突解决策略

1. 开放定址法

线性探测

  • 从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛ 到哈希表尾,则回绕到哈希表头的位置
  • 冲突后公式:hashi = (hash0 + i) % M
  • 缺点:易产生聚集(Clustering)

二次探测

  • 从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为 ⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表 尾的位置

  • 公式:hashi = (hash0 ± i²) % M

  • 缓解聚集,但可能错过空位

删除优化
  • 状态标记(EXIST / EMPTY / DELETE
  1. EXIST:当前槽位存储有效数据。

  2. EMPTY:槽位从未使用过,查找时可终止探测。

  3. DELETE:槽位数据已删除,但探测链不能中断(需继续向后查找)。

cpp 复制代码
enum STATE
{
	EMPTY,
	DELETE,
	EXIST
};

扩容机制

1. 负载因子与扩容条件

  • 负载因子(Load Factor)α = 元素数量 / 哈希表大小

  • 扩容阈值 :当 α ≥ 0.7 时扩容,以降低冲突概率。

  • 扩容倍数 :通常扩容为原大小的 2倍

2. 质数大小的必要性
  • 理论要求 :哈希表大小 M 应为质数,使 key % M 分布更均匀(减少聚集)。

  • 问题 :若初始 M 是质数(如 7),2倍扩容后(14)不再是质数,可能引发更多冲突。

3.解决方案

SGI 版本的质数表:

预定义质数表:按近似2倍递增的质数序列扩容

cpp 复制代码
//写出来的28个素数(每一个都差不多为前一个的两倍)
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)
{
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}
  • 扩容步骤

    1. 当前大小为 53(质数),负载因子 ≥0.7 时,从表中取下一个质数 97(≈53×1.8)。

    2. 重新哈希所有元素到新表。

  • 例子:在插入函数中,发现负载因子>=0.7后的操作:

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	//Check(_tables);
	//如果负载因子 >=0.7 时就需要扩容了
	if ((double)_n / (double)_tables.size() >= 0.7)
	{
		HashTable<K, V,Hash> newHT(__stl_next_prime(_tables.size()+1));
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i]._state == EXIST)
			{
				newHT.Insert(_tables[i]._kv);
			}
		}
		_tables.swap(newHT._tables);
		return true;
	}

完整代码实现
*

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

//写出来的28个素数(每一个都差不多为前一个的两倍)
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)
{
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}

enum STATE
{
	EMPTY,
	DELETE,
	EXIST
};

template<class K,class V>
struct HashData
{
	pair<K, V> _kv;
	STATE _state;
};

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

//因为用string类型的数值做key的情况十分常见,但是在unordered_map中却没有在另外写一个仿函数
//是因为直接将HashFunc 特化 了
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t hashi = 0;
		for (auto e : s)
		{
			hashi += e;
		}
		return hashi;
	}
};

template<class K,class V,class Hash = HashFunc<K>>
class HashTable
{
public:
	//构造函数
	HashTable(size_t size = __stl_next_prime(0))
		:_n(0)
		, _tables(size)
	{}

	//将一些非整形的数值强转成整形一次方便映射关系的计算

	void Check(vector<HashData<K,V>>& table)
	{
		double fuzai = _n / table.size();
		if (fuzai >= 0.7)
		{
			cout << "负载过大" << endl;
		}
		else
		{
			cout << "负载正常" << endl;
		}
	}

	bool Insert(const pair<K, V>& kv)
	{
		//Check(_tables);
		//如果负载因子 >=0.7 时就需要扩容了
		if ((double)_n / (double)_tables.size() >= 0.7)
		{
			HashTable<K, V,Hash> newHT(__stl_next_prime(_tables.size()+1));
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
					newHT.Insert(_tables[i]._kv);
				}
			}
			_tables.swap(newHT._tables);
			return true;
		}

		Hash hs;
		size_t hash0 = hs(kv.first) % _tables.size();
		size_t hashi = hash0;
		size_t i = 1;

		//如果映射的位置已经被占用了
		while (_tables[hashi]._state == EXIST)
		{
			hashi = (hashi + i) % _tables.size();
			++i;
		}

		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;

		return true;
	}

	HashData<K,V>* Find(const K& key)
	{
		Hash hs;
		size_t hash0 = hs(key) % _tables.size();
		size_t hashi = hash0;
		size_t i = 1;

		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._kv.first == key && _tables[hashi]._state != DELETE)
			{
				return &_tables[hashi];
			}
			hashi = (hashi + i) % _tables.size();
			++i;
		}
		cout << " 找不到找不到 " ;
		return nullptr;
	}

	bool Erase(const K& key	)
	{
		HashData<K, V>* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			cout << "成功删除!" << endl;
			return true;
		}
		else
		{
			cout << "删除失败奥" << endl;
			return false;
		}

	}

private:
	vector<HashData<K, V>> _tables;
	size_t _n;
};

2.链地址法

  • 核心思想:冲突位置挂链表(桶)
  • 扩容时机 :负载因子 α ≥ 1(STL 风格)

  • 极端场景优化

    • 链表过长 → 转红黑树(Java 8 HashMap 策略)
  • 扩容技巧

    • 直接移动旧节点(避免重复创建):
cpp 复制代码
// 旧节点重新映射到新表
cur->_next = newTable[hashi];
newTable[hashi] = cur;
  • 例子:在哈希桶版本的Insert函数中发现负载因子过大
cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	if (_n == _tables.size())
	{
		vector<Node*> newTables(__stl_next_prime(0), nullptr);
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				size_t hashi = cur->_kv.first % newTables.size();
				cur->_next = newTables[hashi];
				newTables[hashi] = cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newTables);
	}

	size_t hashi = kv.first % _tables.size();
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi];
	_tables[hashi] = newnode;
	++_n;
	return true;
}

完整代码实现

cpp 复制代码
template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	HashNode* _next;

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

template<class K,class V>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
	HashTable(size_t size = __stl_next_prime(0))
		:_tables(size,nullptr)
	{}

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

	bool Insert(const pair<K, V>& kv)
	{
		if (_n == _tables.size())
		{
			vector<Node*> newTables(__stl_next_prime(0), nullptr);
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					size_t hashi = cur->_kv.first % newTables.size();
					cur->_next = newTables[hashi];
					newTables[hashi] = cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
			_tables.swap(newTables);
		}

		size_t hashi = kv.first % _tables.size();
		Node* newnode = new Node(kv);
		newnode->_next = _table[hashi];
		_tables[hashi] = newnode;
		++_n;
		return true;
	}

	Node* Find(const K& key)
	{
		size_t hashi = 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 = key % _tables.size();
		Node* prev = nullptr;
		Node* cur = _tables[hashi];
		while (cur)
		{
			
			if (cur->_kv.first == key)
			{
				if (prev == nullptr)
				{
					cur->_next = _tables[hashi];

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

			prev = cur;
			cur = cur->_next;
		}
		return false;
	}
private:
	vector<Node*> _tables;
	size_t _n = 0;
};

那么本次关于哈希表的知识分享就此结束了~

非常感谢你能够看到这里~

如果感觉对你有些许的帮助也请给我三连 这会给予我莫大的鼓舞!

之后依旧会继续更新C++学习分享

那么就让我们

下次再见~

相关推荐
无影无踪的青蛙6 分钟前
[C++] list双向链表使用方法
c++·链表·list
tkevinjd1 小时前
C++中线程库的基本操作
开发语言·c++
CodeWithMe1 小时前
【C/C++】不同防止头文件重复包含的措施
c语言·开发语言·c++
Fre丸子_1 小时前
C++定长内存块的实现
c++
子豪-中国机器人2 小时前
C++ 信息学奥赛总复习题答案解析
开发语言·c++·算法
弥彦_2 小时前
牛客round95D
c++·算法
oioihoii2 小时前
C++11列表初始化:从入门到精通
java·开发语言·c++
tomato092 小时前
2025 年中国大学生程序设计竞赛全国邀请赛(郑州)暨第七届CCPC河南省大学生程序设计竞赛(补题)
c++
whoarethenext2 小时前
使用 C++/OpenCV 创建动态流星雨特效 (实时动画)
开发语言·c++·opencv
whoarethenext2 小时前
使用 C/C++的OpenCV 实现模板匹配:从基础到优化
c语言·c++·opencv