C++实现哈希表原理 + 解决冲突

 更新时间:2026年06月19日 07:47:50   作者:Byte不洛  
这段文章详细介绍了哈希表的工作原理及其在数据结构中的应用,探讨了哈希函数的设计原则和常见的冲突解决策略,包括闭散列和开散列法,文章还讨论了不同类型的数据(如整型和字符串)的如何适应哈希表,并强调了哈希表在提升查找效率方面的核心价值

哈希概念

在所有的数据结构中无论是顺序结构还是平衡树(平衡二叉树),元素关键码与其存储位置之间是没有对应的关系的,因此在查询一个元素的时候,必须要经过关键码的多次比较。顺序查询元素的时间复杂度为O(N);平衡树中是树的高度,它的时间复杂度是O(log2​n),搜索的效率取决于搜索过程中元素的比较次数。

在传统数据结构中:

  • 顺序表:查找需要遍历 → 时间复杂度 O(N)
  • 平衡树:通过比较查找 → 时间复杂度 O(logN)

本质问题:

元素位置和关键码没有直接关系 → 必须“比较”才能找到

那么在理想条件下,是否存在一种结构,不经过多余的比较,一次直接从存储结构中搜索到对应的元素,只需要通过某种方式(函数)将元素的存储位置与它的关键码之间建立起一一映射的关系,那么在查找的就可以通过这种方式(函数)很快的找到这个元素。

  • 插入元素

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

  • 搜索元素

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

这种方式就是哈希(散列)方法,在哈希方法中使用的转化函数就是哈希(散列)函数,构造出来的结构就是哈希表(散列表)。

例如:        { 4, 14, 24, 34, 5, 7, 1 }

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

我们其实可以发现,通过这样的方式,我们不需要像数组那样从头开始遍历,只需要通过哈希函数就可以快速定位到对应的位置,但是我们可以看到上面还有一个问题就是会有多个值被映射到了同一位置,这种情况就是哈希冲突。

哈希冲突

不同的关键字通过相同哈希函数计算之后得到相同的哈希地址,这种情况就是哈希冲突(也叫哈希碰撞)。

把具有不同关键码而具有相同哈希地址的数据元素称为同义词。

那么应该如何解决哈希冲突呢?

哈希函数

能够引起哈希冲突的一个原因就是哈希函数设计的不够合理。

所以哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值 域必须在0到m-1之间。
  • 哈希函数计算出来的地址能均匀分布在整个空间中。
  • 哈希函数应该比较简单。

常见的哈希函数

1. 直接定址法

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

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况

2. 除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数, 按照哈希函数:Hash(key) = key% p(p将关键码转换成哈希地址)。

3. 平方取中法1

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。

4. 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这 几部分叠加求和,并按散列表表长,取后几位作为散列地址。 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

5. 随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。 通常应用于关键字长度不等时采用此法。

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

哈希冲突的解决

两种常见的方法是:闭散列和开散列。

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的下一个空位置中去。

线性探测

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

插入

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

删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素14,如果直接删除掉,24查找起来就会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除。

enum Status
	{
		EXIST,
		EMPTY,
		DELETE
	};

线性探测的实现    

enum Status
	{
		EXIST,
		EMPTY,
		DELETE
	};
	template<class K, class V>
	struct HashNode
	{
		std::pair<K, V> _kv;
		Status _s = EMPTY;
	};
	template<class K, class V, class Hash = HashFun<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_tables.resize(10);
		}
		bool Insert(const std::pair<K, V>& kv)
		{
			Hash hf;
			if (Find(kv.first))
			{
				return false;
			}
			if ((double)_n / (double)_tables.size() == 0.7)
			{
				HashTable<K, V> newHTable;
				newHTable._tables.resize(_tables.size() * 2);
				for (int i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._s == EXIST)
					{
						newHTable.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newHTable._tables);
			}
			size_t hashi = hf(kv.first) % _tables.size();
			while(_tables[hashi]._s == EXIST)
			{
				hashi++;
				hashi = hashi % _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._s = EXIST;
			_n++;
			return true;
		}
		Node* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			while (_tables[hashi]._s != EMPTY)
			{
				if (_tables[hashi]._s == EXIST && _tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}
				else
				{
					hashi++;
					hashi %= _tables.size();
				}
			}
			return nullptr;
		}
		bool Erase(const K& key)
		{
			Node* data= Find(key);
			if (data)
			{
				data->_s = DELETE;
				_n--;
				return true;
			}
			else
			{
				return false;
			}
		}
	private:
		std::vector<Node> _tables;
		size_t _n = 0;
	};

哈希表是需要扩容的,那么我们应该如何进行扩容呢?

bool Insert(const std::pair<K, V>& kv)
{
	Hash hf;
	if (Find(kv.first))
	{
		return false;
	}
	if ((double)_n / (double)_tables.size() == 0.7)
	{
		HashTable<K, V> newHTable;
		newHTable._tables.resize(_tables.size() * 2);
		for (int i = 0; i < _tables.size(); i++)
		{
			if (_tables[i]._s == EXIST)
			{
				newHTable.Insert(_tables[i]._kv);
			}
		}
		_tables.swap(newHTable._tables);
	}
	size_t hashi = hf(kv.first) % _tables.size();
	while(_tables[hashi]._s == EXIST)
	{
		hashi++;
		hashi = hashi % _tables.size();
	}
	_tables[hashi]._kv = kv;
	_tables[hashi]._s = EXIST;
	_n++;
	return true;
}

通过简单的代码测试一下。

开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

开散列的实现

template<class K,class V>
struct HashNode
{
	HashNode<K, V>* _next;
	std::pair<K, V> _kv;
	HashNode(const std::pair<K, V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{
	}
};
template<class K, class V, class Hash = HashFun<K>>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
	HashTable()
	{
		_tables.resize(10, nullptr);
	}
	~HashTable()
	{
		for (int 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 std::pair<K, V>& kv)
	{
		Hash hf;
		if (Find(kv.first))
		{
			return false;
		}
		if (_n == _tables.size())
		{
			std::vector<Node*> newTable;
			newTable.resize(_tables.size() * 2, nullptr);
			for (int i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					size_t hashi = hf(cur->_kv.first) % newTable.size();
					cur->_next = newTable[hashi];
					newTable[hashi] = cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
			_tables.swap(newTable);
		}
		size_t hashi = hf(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 hf;
		size_t hashi = hf(key) % _tables.size();
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (hf(cur->_kv.first) == hf(key))
			{
				return cur;
			}
			cur = cur->_next;
		}
		return nullptr;
	}
	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)
				{
					prev->_next = cur->_next;
				}
				else
				{
					_tables[hashi] = cur->_next;
				}
				delete cur;
				_n--;
				return true;
			}
			prev = cur;
			cur = cur->_next;
		}
		return false;
	}
private:
	std::vector<Node*> _tables;
	size_t _n;
};

开散列扩容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可 能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容。开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表扩容。

bool Insert(const std::pair<K, V>& kv)
{
	Hash hf;
	if (Find(kv.first))
	{
		return false;
	}
	if (_n == _tables.size())
	{
		std::vector<Node*> newTable;
		newTable.resize(_tables.size() * 2, nullptr);
		for (int i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				size_t hashi = hf(cur->_kv.first) % newTable.size();
				cur->_next = newTable[hashi];
				newTable[hashi] = cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newTable);
	}
	size_t hashi = hf(kv.first) % _tables.size();
	Node* newNode = new Node(kv);
	newNode->_next = _tables[hashi];
	_tables[hashi] = newNode;
	_n++;
	return true;
}

这里还有一个细节问题就是无论是开散列还是闭散列都只能存储key为整形的元素,其他类型怎么解决?

我们的哈希函数采用处理余数法,被模的key必须要为整形才可以处理,假如是string类型应该如何处理呢?我们可以通过仿函数+模板特化的方式来对其进行解决,这样就可以解决string类型无法取模的问题了。

template<class K>
struct HashFun
{
	size_t operator()(const K& data)
	{
		return data;
	}
};
template<>
struct HashFun<std::string>
{
	size_t operator()(const std::string& data)
	{
		size_t hash = 0;
		for (auto& s : data)
		{
			hash *= 31;
			hash += s;
		}
		return hash;
	}
};

        哈希表的核心价值在于用空间换时间,通过建立关键码与存储位置的映射关系,将查找效率提升到接近 O(1)。但同时也引入了哈希冲突这一不可避免的问题,因此如何设计合理的哈希函数以及选择合适的冲突解决策略,就成为了哈希表性能的关键。

到此这篇关于C++实现哈希表原理 + 解决冲突的文章就介绍到这了,更多相关C++哈希表冲突内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言不用链表完成学生管理系统(完整代码)

    C语言不用链表完成学生管理系统(完整代码)

    这篇文章主要介绍了C语言不用链表完成学生管理系统(完整代码),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  • C++的cout.tellp()和cout.seekp()语法介绍

    C++的cout.tellp()和cout.seekp()语法介绍

    无论是使用 cout 输出普通数据,用 cout.put() 输出指定字符,还是用 cout.write() 输出指定字符串,数据都会先放到输出流缓冲区,待缓冲区刷新,数据才会输出到指定位置,本文给大家介绍一下C++的cout.tellp()和cout.seekp()语法,需要的朋友可以参考下
    2023-09-09
  • C语言堆结构处理TopK问题详解

    C语言堆结构处理TopK问题详解

    TopK问题即在N个数中找出最大的前K个,这篇文章将详细讲解如何利用小根堆的方法解决TopK问题,文中代码具有一定参考价值,快跟随小编一起学习一下吧
    2022-06-06
  • C++ 基本算法 冒泡法、交换法、选择法、实现代码集合

    C++ 基本算法 冒泡法、交换法、选择法、实现代码集合

    大家在学习C语言的时候,老师可能都会讲的几个算法,这里简单整理下,方便需要的朋友
    2013-04-04
  • C++实现动态绑定代码分享

    C++实现动态绑定代码分享

    对于C++动态绑定的理解,就是编译器用静态分析的方法加上虚拟函数的设计实现在程序运行时动态智能执行正确虚拟函数的技术。要彻底理解动态绑定,只需要掌握两点,一是编译器的静态编译过程,二是虚拟函数的基本知识。只要有了这两点理解,任何动态绑定的分析都是很容易的
    2015-11-11
  • C++多线程实现电子词典

    C++多线程实现电子词典

    这篇文章主要为大家详细介绍了C++多线程实现电子词典,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-03-03
  • C++分析构造函数与析造函数的特点梳理

    C++分析构造函数与析造函数的特点梳理

    本文对类的构造函数和析构函数进行总结,主要包括了构造函数的初始化、重载、使用参数和默认参数,拷贝构造函数和析构函数,希望能帮助读者在程序开发中更好的理解类,属于C/C++基础
    2022-05-05
  • vs code 配置python虚拟环境的方法

    vs code 配置python虚拟环境的方法

    这篇文章主要介绍了vs code 配置python虚拟环境的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • C/C++中时间库函数的使用详解

    C/C++中时间库函数的使用详解

    这篇文章主要为大家详细介绍了C/C++中的时间相关知识总结,例如时间库函数的使用以及获取本地时间的不同方法,文中的示例代码讲解详细,需要的可以参考一下
    2022-11-11
  • clion最新激活码+汉化的步骤详解(亲测可用激活到2089)

    clion最新激活码+汉化的步骤详解(亲测可用激活到2089)

    这篇文章主要介绍了clion最新版下载安装+破解+汉化的步骤详解,本文分步骤给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11

最新评论