哈希表的实现

在计算机科学的数据结构体系中,哈希表(Hash Table) 是一种极具代表性且应用极为广泛的存储结构,凭借其卓越的性能优势,成为构建高效系统的核心基础组件。与线性表需要逐一遍历查找、树表需要逐层比较不同,哈希表通过哈希函数 建立关键字与存储地址的直接映射关系,能够在平均 O (1) 常数时间复杂度内完成查找、插入、删除操作,在海量数据处理场景中展现出不可替代的效率优势。

哈希表的核心价值在于突破了传统数据结构的性能瓶颈,以空间换时间的设计思想,实现了数据的快速存取。无论是软件开发中的字典、关联容器 ,互联网架构中的缓存系统、路由表 ,还是数据处理中的高频计数器、去重统计,哈希表都是最常用的底层支撑结构,是现代计算机系统高效运行的关键基石。

本文将围绕哈希表展开系统性讲解,从哈希函数设计、哈希冲突处理两大核心原理入手,详细分析开放寻址法、哈希桶(链地址法)的实现机制,并结合 C++ 语言完成完整代码实现,包括底层结构搭建、迭代器封装、深拷贝管理、扩容优化等关键内容,最终形成一套可落地、可理解、可扩展的哈希表实现方案,帮助全面掌握哈希表的底层逻辑与工程实践方法。

1. 哈希基础概念

1.1 哈希定义

哈希(Hash),又称散列,核心是通过哈希函数,将任意类型的关键字(Key),映射为固定范围的整数(存储地址),实现快速存取。

  1. 核心定义:用哈希函数将关键字(如int、string)转化为存储地址,无需遍历,直接定位,这是哈希表高效的关键。

  2. 核心思想:空间换时间------牺牲部分存储空间(预留哈希桶、空闲位置),换取查找、插入、删除操作均为O(1)的高效性能。

  3. 关键细节:同一关键字经同一哈希函数,必映射到同一地址;不同关键字可能映射到同一地址(哈希冲突),需通过对应方式处理。

核心思想:

  • 给每个 Key 计算一个下标 index = h(key)
  • 将数据存储到这个下标对应位置。
  • 查找时通过哈希函数直接找到位置,极大提升速度。

1.2 直接定地址法

当关键字范围集中时,直接定址法非常高效:

  • 例如关键字在 [0, 99],可用数组下标直接表示关键字。
  • 字符 [a, z] 可以用 ASCII 码减 'a' 得到数组下标。

优点:

  • 简单、快速,无哈希冲突问题。

缺点:

  • 当关键字范围分散时,会浪费大量内存。
  • 不适合稀疏或大范围数据。
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 直接定址法演示:统计 a~z 每个字母出现次数
void directAddressHash(string s)
{
    // a~z 一共26个字母,开辟大小为26的数组
    int cnt[26] = {0};

    // 直接定址核心:ch - 'a' 映射为数组下标 0~25
    for (char ch : s)
    {
        int index = ch - 'a';  // 直接定址哈希函数
        cnt[index]++;
    }

    // 遍历输出每个字母的出现次数
    for (int i = 0; i < 26; i++)
    {
        if (cnt[i] != 0)
        {
            char ch = 'a' + i;
            cout << "字符 " << ch << " 出现了:" << cnt[i] << " 次" << endl;
        }
    }
}

int main()
{
    string str = "abacabxyzabc";
    directAddressHash(str);
    return 0;
}

1.3 哈希冲突

当两个不同的 Key ,经过哈希函数计算后,映射到同一个存储下标位置 时,就会产生哈希冲突

在实际应用中,关键字随机、分布不确定,无论怎么设计哈希函数,都无法从根本上完全杜绝哈希冲突

所以工程上只能做两件事:

  1. 设计分布尽量均匀 的哈希函数,让 Key 尽量散列,减少冲突发生的概率
  2. 配套完善、成熟的哈希冲突解决方案,就算发生冲突,也能正常存入、正常查找,不影响哈希表整体性能。
cpp 复制代码
#include <iostream>
using namespace std;

// 自定义哈希函数:除留余数法
int hashFunc(int key)
{
    // 数组长度为10,对10取模
    return key % 10;
}

int main()
{
    // 哈希表数组,长度10
    int hashTable[10] = {0};

    // 两个完全不同的 key
    int key1 = 12;
    int key2 = 22;

    int idx1 = hashFunc(key1);
    int idx2 = hashFunc(key2);

    cout << "关键字 " << key1 << " 映射下标:" << idx1 << endl;
    cout << "关键字 " << key2 << " 映射下标:" << idx2 << endl;

    if (idx1 == idx2)
    {
        cout << "发生了哈希冲突!" << endl;
    }

    return 0;
}
cpp 复制代码
关键字 12 映射下标:2
关键字 22 映射下标:2
发生了哈希冲突!

这就是典型的哈希冲突。

2. 哈希函数设计

2.1 将关键字转换为整数

哈希表底层只能用整数 计算存储下标,但关键字可以是整型、字符串、pair、自定义结构体 等类型,所以必须先将关键字转换为整数

整型关键字:直接强制类型转换为整数;

cpp 复制代码
template<class K>
struct HashFunc
{
    size_t operator()(const K& key) const
    {
        return (size_t)key;
    }
};
cpp 复制代码
template<>
struct HashFunc<int>
{
    size_t operator()(int key) const
    {
        key ^= (key >> 16);
        return (size_t)key;
    }
};

字符串关键字:遍历字符,利用字符 ASCII 码加权运算,折算为一个整数;

cpp 复制代码
// 字符串特化:把字符串每个字符转ASCII,加权算出一个整数
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key) const
	{
		size_t hash = 0;
		// 遍历每个字符,将字符转为ASCII参与运算
		for (auto ch : key)
		{
			hash += ch;     // 累加字符ASCII值
			hash *= 131;    // 加权扰动,让哈希值分布更均匀
		}
		return hash;
	}
};

复合类型 pair:拆解内部成员,依次加权合并,最终生成一个哈希整数。

cpp 复制代码
// 把 pair<int,int> 这种组合关键字,换算成一个哈希整数
struct pairHash
{
	size_t operator()(const pair<int, int>& kv) const
	{
		size_t hash = 0;
		hash += kv.first;
		hash *= 131;
		hash += kv.second;
		hash *= 131;

		return hash;
	}
};

转换后的整数再通过 hash % 表容量 就能算出存储下标。

2.2 常用哈希函数

2.2.1 除法散列法

除留余数法 是哈希函数中最常用、最简单的一种构造方法。

设哈希表底层数组长度为 m,对任意关键字 key,哈希函数公式:H(key)=keymodm也就是:哈希地址 = 关键字 % 哈希表容量

作用:把任意大的整数关键字,映射到 0 ~ m-1 之间的数组下标

哈希表容量 m 优先选:

质数(素数) __stl_next_prime 就是专门干这事:每次扩容自动找下一个质数当表长,最大限度减少冲突。

cpp 复制代码
inline unsigned long __stl_next_prime(unsigned long n)
{
	// Note: assumes long is at least 32 bits.
	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
	};

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

2.2.2 乘法散列法

公式:h(key)=⌊M×((A×key)mod1.0)⌋

常数 A 取黄金分割 25​−1​≈0.618。优点:对哈希表大小无特殊要求,不用刻意选质数,映射分布均匀。

2.2.3 全域散列法

公式:hab​(key)=((a×key+b)modP)modM

思路:引入随机参数 、,初始化时随机选定并固定使用。给哈希函数增加随机性,避免恶意构造大量哈希冲突,提升整体稳定性。

3. 哈希冲突的处理策略

3.1 开放定址法

核心思想

  • 所有元素都直接存储在哈希表的数组中
  • 如果一个 Key 映射位置已经被占用,就按某种规则探测下一个可用位置

特点

  • 表内所有元素互相影响
  • 负载因子必须 <1(否则找不到空位置)

主要探测策略

1. 线性探测(Linear Probing)

  • 原理:冲突后,从当前位置依次向后查找空槽
  • 公式:
  • 问题:连续冲突会形成"堆积",降低效率

2.二次探测(Quadratic Probing)

  • 原理:按二次方跳跃式探测
  • 公式:
  • 可以缓解线性探测的堆积问题
  • 问题:仍可能未使用表中全部位置

3.1.1开放定址法实现

cpp 复制代码
enum State { EXIST, EMPTY, 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 {
private:
    vector<HashData<K,V>> _tables;
    size_t _n = 0; // 元素个数
public:
    bool Insert(const pair<K,V>& kv) {
        if (_n * 10 / _tables.size() >= 7) resize();
        size_t hash0 = Hash()(kv.first) % _tables.size();
        size_t i = 0, hashi;
        do {
            hashi = (hash0 + i) % _tables.size(); // 线性探测
            if (_tables[hashi]._state != EXIST) break;
            ++i;
        } while (i < _tables.size());
        _tables[hashi]._kv = kv;
        _tables[hashi]._state = EXIST;
        ++_n;
        return true;
    }
};

3.2 链地址法

核心思想

  • 哈希表只存储指针,不直接存数据
  • 冲突的数据挂在对应桶下形成链表
  • 当桶中数据过多,可以升级为红黑树(如 Java 8 HashMap)

特点

  • 每个桶可以存多个元素
  • 负载因子可 >1,扩容灵活
  • 极端情况下查找效率下降,但一般不会严重
  • 冲突时元素通过链表挂到同一个桶
  • 查找时遍历链表即可

3.2.1 链地址法实现

cpp 复制代码
template<class K, class V>
struct HashNode {
    pair<K,V> _kv;
    HashNode* _next;
    HashNode(const pair<K,V>& kv) : _kv(kv), _next(nullptr) {}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
private:
    vector<HashNode<K,V>*> _tables;
    size_t _n = 0;
public:
    bool Insert(const pair<K,V>& kv) {
        size_t hashi = Hash()(kv.first) % _tables.size();
        auto newnode = new HashNode<K,V>(kv);
        newnode->_next = _tables[hashi]; // 头插法
        _tables[hashi] = newnode;
        ++_n;
        return true;
    }
};

头插法:

cpp 复制代码
auto newnode = new HashNode<K,V>(kv);
newnode->_next = _tables[hashi]; // 头插法
_tables[hashi] = newnode;
  • _tables[hashi] 是哈希桶数组的一个元素,指向该桶链表的头节点;
  • 新节点的 next 先指向原来的头节点;
  • 再让桶指针指向新节点,新节点就变成了链表的新头。

4. 扩容

4.1 开放定址法 扩容规则

负载因子 < 1

  • 开放寻址法底层是数组
  • 每个位置只能存一个数据
  • 装满了就不能再存
  • 所以负载因子必须小于 1
  • 一般达到 0.7 就必须扩容

4.2 链地址法 扩容规则

负载因子可以 > 1

  • 每个位置是链表 / 红黑树
  • 一个桶可以挂很多数据
  • 数据再多也能存
  • 所以负载因子可以超过 1
  • 一般达到 1.0 扩容

4.3 扩容方法

  1. 创建新表 容量 = 旧容量 × 2(取最近质数
  2. 重新哈希把旧表所有数据用新容量重新计算下标全部插入新表
  3. 替换旧表新表替代旧表,旧表释放
cpp 复制代码
// 扩容条件:负载因子 >= 0.7
if ((double)_n / (double)_tables.size() >= 0.7)
{
    // 1. 创建新表:容量取 下一个质数
    HashTable<K, V, Hash> newht(__stl_next_prime(_tables.size()+1));

    // 2. 遍历旧表,重新哈希映射到新表
    for (size_t i = 0; i < _tables.size(); i++)
    {
        if (_tables[i]._state == EXIST)
        {
            newht.Insert(_tables[i]._kv);
        }
    }

    // 3. 新表替换旧表
    _tables.swap(newht._tables);
}

5. 实现

哈希表设计核心:

  1. 选择合适哈希函数
  2. 冲突处理策略(开放定址 / 链地址)
  3. 负载因子与扩容机制
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once

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

inline unsigned long __stl_next_prime(unsigned long n)
{
	// Note: assumes long is at least 32 bits.
	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
	};

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

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

// 特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key) const
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash += ch;
			hash *= 131;
		}

		return hash;
	}
};


namespace open_adrress
{

	enum State
	{
		EXIST,
		EMPTY,
		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(size_t n = __stl_next_prime(0))
			:_tables(n)
			, _n(0)
		{}

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

			// 扩容,负载因子==0.7就扩容
			if ((double)_n / (double)_tables.size() >= 0.7)
			{

				//HashTable<K, V> newht(_tables.size() * 2);
				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);
			}

			Hash hs;
			size_t hash0 = hs(kv.first) % _tables.size();
			size_t hashi = hash0;
			size_t i = 1;
			// 线性探测
			while (_tables[hashi]._state == EXIST)
			{
				hashi = hash0 + i;
				i++;

				hashi %= _tables.size();
			}

			_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]._state == EXIST
					&& _tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}

				// 线性探测
				hashi = hash0 + i;
				i++;

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

	private:
		vector<HashData<K, V>> _tables;
		size_t _n;   // 实际存储的数据个数
	};

	void TestHashTable1()
	{
		int a[] = { 19, 30, 5, 36, 13, 20, 21, 12 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert({ e, e });
		}

		cout << ht.Find(20) << endl;
		cout << ht.Find(30) << endl;
		ht.Erase(30);
		cout << ht.Find(20) << endl;
		cout << ht.Find(30) << endl;

		ht.Insert({ -3,3 });
		ht.Insert({ 13,3 });
		ht.Insert({ 33,3 });
		ht.Insert({ 323,3 });
		ht.Insert({ 23,3 });

		for (size_t i = 0; i < 100; i++)
		{
			ht.Insert({ rand(),i });
		}
	}

	struct pairHash
	{
		size_t operator()(const pair<int, int>& kv) const
		{
			size_t hash = 0;
			hash += kv.first;
			hash *= 131;
			hash += kv.second;
			hash *= 131;

			cout << hash << endl;
			return hash;
		}
	};

	// 21:07
	void TestHashTable2()
	{
		//HashTable<string, string, StringHashFunc> dict;
		HashTable<string, string> dict;
		dict.Insert({ "sort", "排序" });
		dict.Insert({ "left", "左边" });
		dict.Insert({ "sort", "xxx" });

		unordered_map<pair<int, int>, int, pairHash> um;
		um.insert({ {1,3},3 });
		um.insert({ {3,1},3 });

	}
}

namespace hash_bucket
{
	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(size_t n = __stl_next_prime(0))
			:_tables(n, nullptr)
			, _n(0)
		{}

		~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 (Find(kv.first))
				return false;

			Hash hs;

			// 负载因子到1扩容
			if (_n == _tables.size())
			{


				// 扩容
				vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
				// 遍历旧表,将旧表的数据全部重新映射到新表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						// cur头插到新表
						size_t hashi = hs(cur->_kv.first) % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}

				_tables.swap(newtables);
			}

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

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

			return true;
		}

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

		bool Erase(const K& key)
		{
			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;
					}

					--_n;
					delete cur;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}
	private:
		vector<Node*> _tables;
		size_t _n;				// 实际存储有效数据个数
	};

	void TestHashTable1()
	{
		int a[] = { 19,30,5,36,13,20,21,12,24,96 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert({ e, e });
		}

		for (size_t i = 0; i < 100; i++)
		{
			ht.Insert({ rand(),i });
		}
	}

	void TestHashTable2()
	{
		int a[] = { 19,30,5,36,13,20,21,12,24,96 };
		HashTable<int, int> ht(11);
		for (auto e : a)
		{
			ht.Insert({ e, e });
		}

		ht.Erase(30);
		ht.Erase(24);

		for (auto e : a)
		{
			ht.Erase(e);
		}
	}

	void TestHashTable3()
	{
		HashTable<string, string> dict;
		dict.Insert({ "sort", "排序" });
		dict.Insert({ "left", "左边" });
		dict.Insert({ "sort", "xxx" });
	}
}
相关推荐
故事和你919 小时前
洛谷-【动态规划1】动态规划的引入4
开发语言·数据结构·c++·算法·动态规划·图论
qq_2965532710 小时前
[特殊字符] 旋转排序数组中的高效搜索:从线性到二分查找的进阶之路
数据结构·算法·搜索引擎·分类·柔性数组
纪念 22910 小时前
顺序表(数据结构入门的开端)
数据结构
汉字萌萌哒10 小时前
2025 CSP-S提高级(第一轮)C++真题以及答案
数据结构·算法
小张成长计划..10 小时前
【C++】35:位图,布隆过滤器和海量数据处理(哈希扩展)
算法·哈希算法
春栀怡铃声10 小时前
【C++修仙录02】筑基篇:list 使用
数据结构·list
夏日听雨眠11 小时前
数据结构(BF算法 )
数据结构·算法·排序算法
夏日听雨眠11 小时前
数据结构(KMP算法)
数据结构·算法
并不喜欢吃鱼11 小时前
从零开始 C++----十【C++ 数据结构】AVL 树详解:从原理到实现
开发语言·数据结构·c++