哈希表的设计

发布于:2025-05-07 ⋅ 阅读:(7) ⋅ 点赞:(0)

1. 哈希表的基本原理

哈希表是一种通过 哈希函数 将元素的键(Key)映射到存储位置的数据结构。

  • 哈希函数 的作用:通过键值计算存储位置,公式一般为 index = hash(key) % capacity

  • 哈希冲突:不同的键可能被映射到同一个位置(例如 hash("abc") % 10 == hash("xyz") % 10),这种情况称为 哈希冲突 或 哈希碰撞


2. 解决哈希冲突的两种方法

(1) 闭散列(开放定址法,Open Addressing)

核心思想:如果目标位置已被占用,就按照某种探测方法在哈希表内 寻找下一个可用位置。

常用探测方法

  1. 线性探测(Linear Probing)

    • 冲突时,依次检查下一个位置:

      index = (hash(key) + i) % capacity,  i=1, 2, 3...
  2. 二次探测(Quadratic Probing)

    • 冲突时,按平方步长跳跃检查:

      index = (hash(key) + i²) % capacity,  i=1, 2, 3...
  3. 双重哈希(Double Hashing)

    • 第一个哈希函数计算基础地址,使用第二个哈希函数计算步长:

      index = (hash1(key) + i * hash2(key)) % capacity, i=1,2,3...

开放定址法的特点

  • 所有数据都存在哈希表内,没有额外资源

  • 删除元素需特殊处理(标记为“逻辑删除”),否则会破坏探测链。

  • 负载因子(Load Factor)通常需较低(如 ≤0.7),否则性能急剧下降。


(2) 开散列(链地址法,Separate Chaining)

核心思想:将哈希表的每个槽作为 链表头节点,冲突的元素直接追加到链表中。

优化

  • 当链表过长时(如 Java 8 的 HashMap),可能转换为 红黑树,将查找时间从 O(n) 优化到 O(log n)。

链地址法的特点

  • 实现简单,允许负载因子 >1(即元素数量可以超过哈希表容量)。

  • 删除操作直接(直接移除链表节点即可)。

  • 内存开销较大,除了哈希表外,还有额外资源节点

#pragma once
#include<vector>//只包头文件不展开命名空间白搭
#include<utility>//
#include<iostream>

using namespace std;

namespace OpenAdress//开放定址法
{
	enum State
	{
		EMPTY,
		DELETE,
		EXIST
	};

	template<class K,class V>
	struct HashData//为了能直接访问hashdata设为struct
	{
		pair<K, V> _kv;
		State _state=EMPTY;

		//使用默认构造,初始化列表使用缺省值

	};

	template<class K,class V>
	class HashTable
	{
	public:
		bool  Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}

			if (_tables.size() == 0 || (double)_n / _tables.size() >= 0.7)//负载因子设为0.7
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<HashData<K, V>> newtable;
				newtable.resize(newsize);
				for (HashData<K, V>& e : _tables)//已经扩容过了必定可以插入
				{
					if (e._state == EXIST)
					{
						size_t hashi = e._kv.first % newtable.size();
						size_t i = 1;
						size_t index = hashi;
						while (newtable[index]._state == EXIST)
						{
							index = hashi + i;
							index %= newtable.size();
							i++;
						}
						newtable[index]._kv = e._kv;
						newtable[index]._state = EXIST;


					}
				}
				_tables.swap(newtable);

			}



			size_t hashi = kv.first % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state == EXIST)
			{
				index = hashi + i;
				index %= _tables.size();
				i++;
			}
			_tables[index]._kv = kv;
			_tables[index]._state = EXIST;
			_n++;
	
			return true;

		}

		HashData<K,V>* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			
			size_t hashi = key % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state!=EMPTY)//插入是从hashi开始向后找不EXIST的位置,按插入方式找的时候如果遇到空说明没有该值,找了一圈没有找到那也是没有
			{
				if (_tables[index]._state == EXIST && _tables[index]._kv.first == key)
				{
					return &_tables[index];
				}

				index =hashi+ i;
				index %= _tables.size();
				i++;
				if (index == hashi)//找了一圈
				{
					break;//直接返回报警不是所有路径有返回值
				}

			}
			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 = 0;//有效数据个数
	};

}


//在全局作用域定义Hash仿函数,包了头文件就能直接用
template<class K>
struct HashFunc//可作为Hash的缺省值
{
	size_t operator()(const K& key)
	{
		return key;//可隐式类型转换
	}
};

template<>//特化   仿函数
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t i= 0;
		for (auto e : str)
		{
			i += e;
			i *= 31;
		}

		return i;
	}
};


namespace HashBucket
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

		HashNode(const T& data)
			:_data(data)
			,_next(nullptr)
		{}
	};


	template<class K, class T, class KeyOfT, class Hash>//前置声明
	class HashTable;


	template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
	struct __HashIterator
	{
		typedef HashNode<T> Node;
		typedef __HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
		typedef __HashIterator< K, T, T&, T*, KeyOfT, Hash> iterator;
		typedef HashTable<K, T, KeyOfT, Hash> HT;//不前置声明的话会报<前缺少;的错误
		Node* _node;
		HT* _ht;

		__HashIterator(Node* node,HT* ht)
			:_node(node)
			,_ht(ht)
		{}

		__HashIterator(const iterator& it)
			:_node(it._node)
			,_ht(it._ht)
		{}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
		//哈希表为单向迭代器
		Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
				break;
			}
			else
			{
				KeyOfT kot;
				Hash hash;
				size_t hashi = hash(kot(_node->_data)) % _ht->GetSize();
                
				hashi++;
				while (hashi < _ht->GetSize())
				{
					if (_ht->_tables[hashi])
					{
						_node = _ht->_tables[hashi];
						break;
					}
					else
					{
						hashi++;
					}
				}

				if (hashi == _ht->GetSize())
				{
					_node = nullptr;
				}
			}
			return *this;
		}
		
	};



	template<class K,class T,class KeyOfT,class Hash>//Hash仿函数将key转换为整形
	class HashTable
	{
		//需要访问_tables
		template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
		friend struct __HashIterator;


		typedef HashNode<T> Node;
	public:
		typedef __HashIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
		typedef __HashIterator<K, T, const T&,const T*, KeyOfT, Hash> const_iterator;

		iterator begin()
		{
			Node* cur = nullptr;
			for (int i = 0;i < _tables.size();i++)
			{
				if (_tables[i])
				{
					cur = _tables[i];
					break;
				}
			}
			return iterator(cur, this);
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}

		const_iterator begin()const
		{
			Node* cur = nullptr;
			for (int i = 0;i < _tables.size();i++)
			{
				if (_tables[i])
				{
					break;
				}
			}
			return const_iterator(cur, this);
		}

		const_iterator end()const
		{
			return const_iterator(nullptr, this);
		}


		~HashTable()
		{
			for (Node* cur : _tables)
			{
				while (cur)
				{
					Node* tmp = cur;
					cur = cur->_next;
					delete tmp;

				}
			}
		}


		size_t GetSize()
		{
			return _tables.size();
		}

		iterator Find(const K& key)
		{
			if (_tables.size() == 0)
				return iterator(nullptr,this);

			KeyOfT kot;
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur,this);
				}

				cur = cur->_next;
			}

			return iterator(nullptr, this);

		}

		bool Erase(const K& key)
		{
			KeyOfT kot;
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (kot(cur->_data) != key)
				{
					prev = cur;
					cur = cur->_next;
				}
				else
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
			}

			return false;
		}

		pair<iterator,bool> Insert(const T& data)
		{
			KeyOfT kot;
			iterator it  = Find(kot(data));
			if ( it!=end())
			{
				return make_pair(it,false);
			}

			Hash hash;
			if (_n == _tables.size())//先判断是否要扩容,且该代码包含为空的情况
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*> newtable(newsize,nullptr);
				for (Node* cur : _tables)
				{
					while (cur)
					{
						Node* tmp = cur;
						cur = cur->_next;
						size_t hashi = hash(kot(tmp->_data)) % newtable.size();
						tmp->_next = newtable[hashi];
						newtable[hashi] = tmp;
					}
				}
				_tables.swap(newtable);
			}

			size_t hashi = hash(kot(data)) % _tables.size();
			Node* tmp = new Node(data);
			tmp->_next = _tables[hashi];
			_tables[hashi] = tmp;
			return make_pair(iterator(tmp,this),true);


		}
	private:
		vector<Node*> _tables;
		size_t _n;
	};

}

1.闭散列也就是发放定址法(线性探测)实现哈希表

a.哈希表节点设计

两个模板参数一个键类型K,一个值类型V,有一个数据,然后数据类型是pair<K,V>,一个状态值是枚举类型,不去设置泛型的数据类型T,而是设计成这种数据格式,这个设计和AVL树节点设计很像

b.哈希表设计

哈希表的成员有一个vector,vector的元素就是哈希表节点,还有一个size_t,是节点个数

哈希表模板参数本应由三个,第一个是键的类型K,第二个是值类型V,第三个是将K转换成size_t的仿函数类,但这里偷懒了没有加,测试直接用的K是int,而且因为节点数据类型不是T,所以也不用加把T变成K的仿函数类

c.哈希表节点为什么有三种状态而不是只有empty和exist两种

插入一个元素,如果键映射的位置已经是exist那就往后探测,如果是delete或empty,那直接存就是了。

查找一个元素,如果是exist那就看看键是不是我要的,如果不是那就下一个,如果是delete,那说明这个已经被删了,但后面可能还有,所以继续往后探测,如果是empty那真到头了,直接结束吧,没有对应键的节点

也就是说delete这种状态是因为我们要用empty来表示查找结束,所以删除一个元素不能用empty来表示,所以加一个delete状态

d.哈希表中存储节点,没有额外资源

2.开散列也就是链地址法实现哈希表

a.哈希表节点设计

一个模板参数T表示数据类型,然后节点里有一个T类型数据,一个指针,因为节点是用单链表串起来的

b.哈希表设计

模板参数是,键的类型K,哈希表节点数据类型T,把数据类型T变成K的仿函数类keyOfT,把键类型K变成size_t的仿函数类类型Hash

成员是,一个vector,vector的元素是节点指针,存的是一个哈希桶单链表的头结点指针,还有一个size_t成员,表示节点个数

c.哈希表的插入

插入就是头插节点

d.哈希表的迭代器设计

哈希表的迭代器就是封装了结点指针的类

成员有结点指针,这个自然。节点是以单链表的形式存在的,我们为了支持迭代器++能够找到下一个节点,要在迭代器对象里加一个哈希表对象的指针,不然下一个节点不在这个单链表我们就没法弄

迭代器的模板参数K,T,keyOfT,Hash都是为了有结点类型和哈希表类型,剩下俩ref和ptr是为了实现迭代器和const迭代器

ps:迭代器的解引用*返回的是节点里数据的引用,迭代器的成员访问->,返回的是节点里数据的指针。编译器在编 迭代器->成员 时,先是 迭代器.operator->(),返回节点里数据的指针,然后 编译器自动 指针->成员,最终实现访问节点里数据的成员的效果


网站公告

今日签到

点亮在社区的每一天
去签到