哈希表原理详解

1.什么是哈希

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
,搜索的效率取决于搜索过程中元素的比较次数。
如果构造一种存储结构,通过某种函数**(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时可以不经过任何比较,一次直接从表中得到要搜索的元素
该方式即为哈希方法, **哈希方法中使用的转换函数称为哈希
函数,构造出来的结构称为哈希表**(Hash Table)。**
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功。

2.哈希函数

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

2.直接定址法。**取关键字的某个线性函数为散列地址:**Hash(Key)= A*Key + B。需要事先知道关键字的分布情况,适合查找比较小且连续的情况。

3.哈希冲突

不同关键字通过相同哈希函数可能会计算出相同的哈希地址,该种现象称为哈希冲突****或哈希碰撞
解决哈希冲突两种常见的方法是:闭散列和开散列。

3.1闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的**"下一个"**空位置中去。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
4%10=4。 44%10=4。那么地址4这个位置就会冲突,采用线性探测的方法,一直向后找,直到找到空位置进行插入。

那么如何实现查找和删除呢?
我们可以这样做:给哈希表的每个空间进行标记,EMPTY表示此位置空,EXIST表示此位置有元素,DELETE表示元素已删除。
当我们查找元素时,先对关键字取余数找到对应的地址,如果地址标记为EMPTY则表明该元素不存在,如果标记为EXIST或DELETE,则需要将关键字和哈希表中的元素进行比较(如果标记为DELETE直接向后探测即可,不用比较),如果不同就继续向后探测直到标记为EMPTY,如果到标记为EMPTY还没找到,则表明该元素不存在。

3.2开散列

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

插入,查找,删除如何实现?

对于插入,我们可以采用不断头插的方法,如果冲突,就把新的节点头插到该位置。

对于查找,通过哈希函数求得地址后,如果该地址的位置为nullptr,则表明不存在,如果不为空,则沿着这条链进行一一比较。

对于删除,把该节点delete,然后把链接关系搞好即可。

4.关于哈希表扩容的问题

哈希表开始的空间开多大比较好呢,哈希表要不要扩容呢?这都是一个比较值得思考的问题。

哈希表的初始空间不能太大,否则如果插入的数据很少,那么空间浪费就会比较多。所以我们开始可以开一个比较小的空间,假设10个空间。

那么如果插入的数据很多对于闭散列来说,空间肯定是不够的,肯定时需要扩容的,那么我们什么时候扩容,是到哈希表空间全满之后再扩容吗?显然不是的,如果是到哈希表空间全满之后再扩容,那么肯定会存在大量的哈希冲突,这时候再去查找效率就会明显低下,因为你需要去一个一个的去找。所以我们为了效率,需要保证哈希表不能太满,所以我们引入负载因子的概念,当插入的元素占总空间的70%的时候,也就是负载因子大于等于0.7的时候,我们就进行扩容。

对于开散列呢,我们好像不需要扩容,因为可以在一个节点后面不断的链接,能够不断的插入数据,但是也是一样的效率呢?当哈希表中每一条链都很长的时候,查找效率不就非常低下了,那和在数组中查找不就是差不多的了,因此为了保证效率,我们也需要扩容。这是我们的负载因子可以放大一些,当负载因子为1时我们在进行扩容,来保证每一条链都不会太长。

5.关键字为字符串类型如何解决

关键字为字符串类型需要把关键字转换为整型,有一些人专门研究了哈希字符串算法。

cpp 复制代码
size_t operator()(const string& str)
{
	// BKDR算法
	size_t hash = 0;
	for (auto ch : str)
	{
		//这里*131 是为了减少hash冲突 提高效率
		//"bacd"   "abbe"  "abcd" 类似这样的字符串 如果不*131 那么他们的hash值是一样的。就会出现大量的hash重复
		hash *= 131;
		hash += ch;
	}

	return hash;
}

6.代码实现

cpp 复制代码
#pragma once
using namespace std;
#include<string>
#include<vector>
#include<iostream>
enum state
{
	EXITE,
	DELETE,
	EMPTY
};

template<class K>
class DefaultHashFun
{
public:

	//返回size_t 如果关键字为负数 也能解决
	size_t operator()(const K& key)
	{
		return key;
	}
};

//模板的特化
//如果关键字是string类型的则自动调用该模板
template<>
class DefaultHashFun<string>
{
public:

	size_t operator()(const string& str)
	{
		// BKDR算法
		size_t hash = 0;
		for (auto ch : str)
		{
			//这里*131 是为了减少hash冲突 提高效率
			//"bacd"   "abbe"  "abcd" 类似这样的字符串 如果不*131 那么他们的hash值是一样的。就会出现大量的hash重复
			hash *= 131;
			hash += ch;
		}

		return hash;
	}

};
//1.开放地址法
namespace open_address
{
	
	template<class K,class V>
	struct HashNode
	{

		HashNode()
			:_state(EMPTY)
		{}

		pair<K, V> _kv;
		state _state;
	};

	//第三个是仿函数 是为了解决string做关键字无法求模的问题
	template<class K,class V,class KOfV = DefaultHashFun<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_table.resize(10);
		}

		HashNode<const K, V>* find(const K& key)
		{
			KOfV kov;
			int hashi = kov(key) % _table.size();
			while (_table[hashi]._state != EMPTY)
			{
				if (_table[hashi]._state == EXITE && _table[hashi]._kv.first == key)
				{
					return (HashNode<const K, V>*) & _table[hashi]; //这里需要强制类型转换 这两个属于不同的类型
				}
				else
				{
					++hashi;
					hashi %= _table.size();
				}
			}
			return nullptr;
		}

		bool insert(const pair<K, V>& kv)
		{
			KOfV kov;

			//如果存在就不允许插入
			if (find(kv.first))
			{
				return false;
			}
			
			//线性探测hash表不能太满,如果太满则hash冲突的概率就越高,效率就越低 
			// 因此 引入负载因子 取0.7 超过0.7就进行扩容
			
			if ((double)_n / _table.size() >= 0.7)
			{
				int newSize = _table.size() * 2;
				HashTable<K, V>ht;
				ht._table.resize(newSize);
				for (int i = 0; i < _table.size(); i++)
				{
					if (_table[i]._state == EXITE)
					{
						ht.insert(_table[i]._kv);
					}
				}
				_table.swap(ht._table);
			}
			//线性探测
			int hashi = kov(kv.first) % _table.size();
			//注意这里的的条件 如果这个位置存在 则插入到下一个位置
			while (_table[hashi]._state == EXITE)
			{
				++hashi;
				if (hashi == _table.size())
				{
					hashi = 0;
				}
			}
			//这个位置为EMPTY或DELETE
			_table[hashi]._kv = kv;
			_table[hashi]._state = EXITE;
			++_n;

			return true;
		}

		bool erase(const K& key)
		{
			KOfV kov;
			int hashi = kov(key) % _table.size();
			while (_table[hashi]._state != EMPTY)
			{
				if (_table[hashi]._state == EXITE && _table[hashi]._kv.first == key)
				{
					_table[hashi]._state = DELETE;
					--_n;
					return true;
				}
				else
				{
					++hashi;
					hashi %= _table.size();
				}
			}
			return false;
		}

	private:
		vector<Node>_table;
		int _n=0;//记录有效元素的个数

	};

}

//链地址法
namespace hash_bucket
{
	template<class K, class V>
	struct HashNode
	{
		HashNode(const pair<const K,V>& kv)
			:_kv(kv)
			,_next(nullptr)
		{}

		pair<const K, V> _kv;
		HashNode<const K, V>* _next;
	};

	template<class K,class V, class KOfV = DefaultHashFun<K> >
	class HashTable
	{
		typedef HashNode<const K, V> Node;
	public:

		HashTable()
		{
			_table.resize(10, nullptr);
		}

		~HashTable()
		{
			for (int i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		}

		Node* find(const K& key)
		{
			KOfV kov;
			int hashi = kov(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				else
				{
					cur = cur->_next;

				}

			}
			return nullptr;
		}

		bool insert(const pair<K, V>& kv)
		{
			//扩容 如果不进行扩容 当有很多数据插入时
			//链上会有很多节点 这样查找的效率非常低 所有需要扩容 来提高效率 这里的负载因子可以放大一些
			KOfV kov;
			if (_n == _table.size())
			{
				int newSize = _table.size() * 2;
				vector<Node*>newTable;
				newTable.resize(newSize, nullptr);

				// 遍历旧表,顺手牵羊,把节点牵下来挂到新表
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 头插到新表
						size_t hashi = kov(cur->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}

					_table[i] = nullptr;
				}
				_table.swap(newTable);
			}
			int hashi = kov(kv.first) % _table.size();
			Node* cur = new Node(kv);
			//头插
			if (_table[hashi])
			{
				
				Node* prev = _table[hashi];
				_table[hashi] = cur;
				cur->_next = prev;
			}
			_table[hashi] = cur;
			++_n;
			return true;
		}

		bool erase(const K& key)
		{
			KOfV kov;
			size_t hashi = kov(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					--_n;
					return true;
				}

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

			return false;

		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_kv.first << ":" << cur->_kv.second << "->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
			cout << endl;
		}

	private:
		vector<Node*>_table;
		int _n = 0;
	};
}
相关推荐
chao_7893 小时前
Union 和 Optional 区别
开发语言·数据结构·python·fastapi
hope_wisdom5 小时前
C/C++数据结构之用数组实现栈
c语言·数据结构·c++·数组·
范特西_5 小时前
数组的最大美丽值
数据结构·算法
蒙奇D索大6 小时前
【数据结构】图论核心应用:关键路径算法详解——从AOE网到项目管理实战
数据结构·笔记·学习·考研·算法·图论·改行学it
学c语言的枫子6 小时前
数据结构——Dijkstra算法
数据结构·算法
Pluchon15 小时前
硅基计划4.0 算法 字符串
java·数据结构·学习·算法
麦格芬23016 小时前
LeetCode 416 分割等和子集
数据结构·算法
2401_8414956420 小时前
【数据结构】顺序表的基本操作
数据结构·c++·算法·顺序表·线性表·线性结构·顺序表的基本操作
自信的小螺丝钉20 小时前
Leetcode 138. 随机链表的复制 哈希 / 拼接+拆分
leetcode·链表·哈希算法