哈希/散列--哈希表[思想到结构][==修订版==]

文章目录

  • 1.何为哈希?
    • 1.1百度搜索
    • 1.2自身理解
    • 1.3哈希方法/散列方法
    • 1.4哈希冲突/哈希碰撞
    • 1.5如何解决?
      • 哈希函数的设计
  • 2.闭散列和开散列
    • 2.1闭散列/开放定址法
    • 2.2开散列/链地址法/开链法
      • 1.概念
      • 2.容量问题
      • 3.字符串问题
      • 4.开散列性能测试
      • 5.开散列与闭散列比较
  • 3.代码实现[配备详细注释]
    • 3.1闭散列
    • 3.2开散列

1.何为哈希?

1.1百度搜索

在这里插入图片描述
在这里插入图片描述

1.2自身理解

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应关系
在查找一个元素时,必须要经过key的多次比较。
顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN).

有没有这样一种方法 不经过任何比较 直接从表中得到要查找的元素。

大佬神作: 构造一种存储结构,通过某种函数使元素的存储位置与key之间建立一一映射的关系,在查找时通过该函数找到该元素.

1.3哈希方法/散列方法

插入元素:
将待插入元素的key,以某个函数[哈希函数/散列函数]计算出该元素的存储位置并按此位置存放,构造出一个结构[哈希表/散列表]
搜索元素:
对元素的key进行同样的计算,求得的函数值即为元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
在这里插入图片描述

1.4哈希冲突/哈希碰撞

  • 不同关键码通过哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞.
  • 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

在这里插入图片描述

1.5如何解决?

哈希函数的设计

设计一个合理的哈希函数

哈希函数设计原则:

  1. 简单方便
  2. 哈希函数要使得关键码均分分布
  3. 定义域为所有key 值域为[0, n)

常见哈希函数

  • 直接定址法–(常用)
    取关键字的某个线性函数为散列地址:Hashi(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况 否则导致—数据量小 但是需要的空间极大 例如 :数据-1 2 3 9999下标: 1 2 3 9999
    使用场景:数值小且分布集中
  • 除留余数法–(常用)
    设散列表中允许地址数为n,除数p的取值规则: 小于等于n 接近/等于n的质数
    哈希函数:Hashi(key) = key % p(p <= n)
  • 平方取中法–(了解)
    假设关键码为6392,它的平方为40857664,抽取中间的3位857或576作为hashi;
    使用场景:不知道关键码的分布 位数不是很大
  • 折叠法–(了解)
    将关键码从左到右分割成位数近似相等的几部分 将这几部分叠加求和
    按散列表表长 取后几位作为hashi
    使用场景:无所谓关键码的分布 位数较大
  • 随机数法–(了解)
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即Hashi(key) = random(key)
    使用场景:关键字长度不等
  • 数学分析法–(了解)
    假设关键字为某一地区的手机号,大部分前几位都相同的 取后面的四位作为hashi
    还出现冲突–对抽取数字进行反转(如1234改成4321)、右环移位(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等.
    使用场景:关键字位数比较大 事先知道关键字的分布 关键字的若干位分布较均匀
    == 注意:哈希冲突只可缓解 不可避免 ==

2.闭散列和开散列

2.1闭散列/开放定址法

当发生哈希冲突时,如果哈希表未被装满,把key存放到冲突位置的==“下一个” ==空位置
寻找空位置:

  1. 线性探测
    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置
    在这里插入图片描述

插入:

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 该位置没有元素直接插入新元素
  • 该位置中有元素 使用线性探测找到下一个空位置 插入新元素
    在这里插入图片描述

删除:

  • 线性探测采用标记的伪删除法来删除一个元素
  • 若直接删除 会影响其他元素的搜索
    例如上图: 删除对象是33 直接删除 hash6 这个位置应该怎么办? 需要搞一个东西让哈希表的使用者知道这里原来有元素 现在被删除了 置成null? 如果置成空 当想要查找 1002/43即33后面的元素时 遇到hash6 使用者被告知这里是空 停止查找 此时使用者得到的信息是 哈希表中并不存在此元素 这与现实违背 那怎么办? 答案是置成"删除"状态 使得使用者知道何为空何为删除 这就是伪标记删除法

负载因子

哈希表的负载因子定义为: α = 表中的元素个数[ _n ] / 哈希表长[ _tables.size() ]
负载因子a: 哈希表装满程度的标志因子
表长是定值,α与_n成正比
α越大 填入表中的元素越多 哈希冲突可能性越大
α越小 填入表中的元素越少 哈希冲突可能性越小
哈希表的平均查找长度是负载因子α的函数 处理冲突方法不同函数不同
负载因子越大,冲突的概率越高,查找效率越低,空间利用率越高
负载因子越小,冲突的概率越低,查找效率越高,空间利用率越低

容量问题: 1.size是实际能够访问数据的范围 2.capacity是存储数据的空间大小

在这里插入图片描述
在这里插入图片描述

优点:

实现简单

缺点;

x占据y的位置 y就得放到y+1的位置
冲突累计 产生数据堆积
本意是要减缓哈希冲突
虽然使得有相同hashi的不同数据有位置存放
但是数据堆积时 会使得寻找某关键码的位置需要许多次比较
导致搜索效率降低。

  1. 二次探测
    线性探测寻找空位置的方法[逐个后移]导致线性探测的缺陷[产生冲突的数据堆积]
    修改探测的方法:
    在这里插入图片描述

2.2开散列/链地址法/开链法

1.概念

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

2.容量问题

桶的个数有限[即哈希表的表长有限]
随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容
某种情况下 每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,我们假定在元素个数刚好等于桶的个数时,进行扩容 即在这里插入图片描述
需要了解到是 我们最然控制α为1时进行扩容 并不代表此时哈希桶都挂了一个结点 更为普遍的情况是 一些桶为空 一些桶有许多结点 只不过结点总个数为哈希表长度大小

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.字符串问题

在一些函数中 我们遇到了一个问题:

//哈希函数计算的下标
size_t hashi = key % _tables.size();

当key是字符串类型时 无法进行取模操作

改进

哈希/散列--哈希表[思想到结构][==修订版==]_第1张图片
哈希/散列--哈希表[思想到结构][==修订版==]_第2张图片
哈希/散列--哈希表[思想到结构][==修订版==]_第3张图片

不传显示函数 默认调用缺省函数

在这里插入图片描述

调用显示传参函数

哈希/散列--哈希表[思想到结构][==修订版==]_第4张图片哈希/散列--哈希表[思想到结构][==修订版==]_第5张图片

在解决字符串取模问题时 我们首先想到了搞一个仿函数 由key传参 获得整型 但是这仍然无法解决数据是字符串类型 于是我们想到了 缺省值这一语法 Fun类模板默认接收ConvertFunc 当用户想要使用string类型时 显示传StringToInt 在实现StringToInt时 我们右遇到了另一个问题 当数据对象不同但是ASCII值加和相同 此时又造成了不同数据会哈希冲突的局面 怎么办呢? 这个问题实际上是一个很经典的问题 对于这个问题 业界很多大佬都对其发表了自己的见解 我们采取一种即可 有想对其进行更多了解的 点击链接查阅即可 这里不再赘述
字符串Hash函数对比

4.开散列性能测试

哈希/散列--哈希表[思想到结构][==修订版==]_第6张图片
哈希/散列--哈希表[思想到结构][==修订版==]_第7张图片

有人说当在哈希表中大量数据都插入到了一个桶中 此时最坏的T(N)=O(N)
但实际上 由于存在负载因子α 每进行一次扩容这样的情况就会消失 所以
哈希表的T(N) = O(1) 由下面的性能测试代码可以证明

5.开散列与闭散列比较

闭散列需要开大量空间以确保搜索效率 在这些空间中 有许多空闲空间
开散列虽然增设链表结点和指针 但是与闭散列相比 更节省空间

3.代码实现[配备详细注释]

3.1闭散列

//闭散列/开放地址法
namespace OpenAddressing
{
	//状态枚举
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	//哈希数据元素
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _pair;
		State _state = EMPTY;
	};

	//哈希表
	template<class K, class V>
	class HashTable
	{
	public:
		//插入函数
		bool Insert(const pair<K, V>& pair)
		{
			//值已存在 插入错误
			if (Find(pair.first))
				return false;

			//负载因子/荷载系数 -- Load_Factor = _n / _tables.size();

			//(double)_n / (double)_tables.size() >= 0.7
			//_n * 10 / _tables.size() >= 7

			//使得扩容发生的条件: size为0 负载因子达到阈值
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				/  低级代码  /
				/*
				//先更新size 由size作为参数扩容 解决只改容量 不更新访问范围的问题
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;

				//调用vector的有参构造[有参构造里调用reserve] 创建一个新表
				vector> newtables(newsize);

				//遍历旧表  由哈希函数更新数据位置
				for (auto& e :  _tables)
				{
					if (e._state == EXIST)
					{
						//哈希函数计算出的下标
						size_t hashi = pair.first % newtables.size();

						//重新线性探测
						size_t index = hashi;//index代替hashi进行操作 保留原始hashi的值不变
						for (size_t i = 1; newtables[index]._state == EXIST; ++i)
						{
							index = hashi + i;        //从原始下标不断 +1 +2 +3 ...
							index %= newtables.size();//防止越界 只在表内定位index
						}

						//将数据放入合适位置
						newtables[index]._pair = e._pair;
						newtables[index]._state = EXIST;
					}
				}

				//新表的数据才是我们想要的数据 交换后 newtables中存放的变为旧数据
				//newtables是个局部变量 让其"自生自灭"
				_tables.swap(newtables);
				*/

				//  高级代码 //
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> other;
				other._tables.resize(newsize);

				for (auto& e : _tables)
				{
					if (e._state == EXIST)
						other.Insert(e._pair);
				}

				_tables.swap(other._tables);
				/

				//以上高级代码实际是对下面的线性探测进行了复用
			}

			//哈希函数计算出的下标
			size_t hashi = pair.first % _tables.size();

			// 线性探测
			size_t index = hashi;//index代替hashi进行操作 保留原始hashi的值不变
			for (size_t i = 1; _tables[index]._state == EXIST; ++i)
			{
				index = hashi + i;      //从原始下标不断 +1 +2 +3 ...
				index %= _tables.size();//防止越界 只在表内定位index
			}
			//将数据放入合适位置
			_tables[index]._pair = pair;
			_tables[index]._state = EXIST;
			_n++; //数据个数++

			return true;
		}

		//查找函数
		HashData<K, V>* Find(const K& key)
		{
			//哈希表为空 返回nullptr
			if (_tables.size() == 0)
				return nullptr;

			//哈希函数计算出的下标
			size_t hashi = key % _tables.size();

			// 线性探测
			size_t index = hashi;
			for (size_t i = 1; _tables[index]._state != EMPTY; ++i)
			{
				//obj是key的前提是obj存在
				if (_tables[index]._state == EXIST
					&& _tables[index]._pair.first == key)
				{
					return &_tables[index];
				}

				index = hashi + i;
				index %= _tables.size();

				//当表中元素状态非exist即delete时 
				//for循环判断条件一直为真 死循环
				//解决: 当找了一圈还未找到
				//表中无此key 返回false
				if (index == hashi)
					break;
			}
			return nullptr;
		}

		//删除函数
		bool Erase(const K& key)
		{
			HashData<K, V>* pos = Find(key);
			if (pos)
			{
				pos->_state = DELETE;
				--_n; //虽然已标记删除 仍然要使数据个数减减 防止有用数据未达到阈值就执行扩容
				return true;
			}
			else
				return false;
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;//存储的数据个数
	};
	//  测试函数  ///
	void TestHashTable()
	{
		int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
		HashTable<int, int> ht;
		//插入
		for (auto& e : a)
			ht.Insert(make_pair(e, e));
		//插入第8个数据 达到阈值  测试扩容
		ht.Insert(make_pair(15, 15));

		//查找 + 删除
		int tmp = 12;
		if (ht.Find(tmp))
			cout << tmp << "在" << endl;
		else
			cout << tmp << "不在" << endl;

		ht.Erase(tmp);

		if (ht.Find(tmp))
			cout << tmp << "在" << endl;
		else
			cout << tmp << "不在" << endl;
	}
}

3.2开散列

//开散列/链地址法
namespace ChainAddressing
{
	/*STL库中unordered_map/unordered_set的定义
	   template
	   <
		class Key,
		class T,
		class Hash = hash,
		class Pred = equal_to, 
		class Alloc = allocator< pair >  
	   > 
		class unordered_map;
	    class unordered_set;
	*/
	//结点类
	template<class K, class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		pair<K, V> _pair;

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

	//转换函数==类模板特化应用场景==[int->int char->int + 特化:string->int]
	template<class K>
	struct hash
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};
	template<>
	struct hash<string>
	{
		size_t operator()(const string& s)
		{
			size_t value = 0;
			for (auto ch : s)
			{
				value = value * 131 + ch;
			}
			return value;
		}
	};
	

	//字符串转换函数
	/*
    struct StringToInt
	{
		size_t operator()(const string& s)
		{
			//问题代码:
			//return s[0];
			//1.如果传空串 违背使用者意愿
			//2.不同单词相同首字母均会哈希冲突 不太合适 

			//代码改进
			size_t value = 0;
			for (auto& ch : s)
			{
				value = value * 131 + ch;
			}
			return value;
		}
	};
	*/

	//哈希表类
	template< class K, class V,class Hash = hash<K> >
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		//析构函数
		~HashTable()
		{
			for (auto& ptr : _tables)
			{
				while (ptr)
				{
					//记录下一个结点
					Node* next = ptr->_next;
					//释放当前结点
					delete ptr;
					//更新ptr
					ptr = next;
				}

				ptr = nullptr;
			}
		}

		//查找函数
		Node* Find(const K& key)
		{
			//为空不查找 返回nullptr
			if (_tables.size() == 0)
				return nullptr;

			Hash hash;
			//哈希函数计算的下标
			size_t hashi = hash(key) % _tables.size();
			//首先得到表里的指针 即相当于每一个桶的头指针
			//[实际上 每一个桶就是一个链表 表中的ptr是每一个链表的哨兵指针]
			Node* ptr = _tables[hashi];

			while (ptr)
			{
				if (ptr->_pair.first == key)
					return ptr;
				ptr = ptr->_next;
			}

			return nullptr;
		}

		//删除函数
		bool Erase(const K& key)
		{
			Hash hash;
			//哈希函数计算的下标
			size_t hashi = hash(key) % _tables.size();
			//首先得到表里的指针 即相当于每一个桶的头指针
			//[实际上 每一个桶就是一个链表 表中的ptr是每一个链表的哨兵指针]
			Node* ptr = _tables[hashi];
			Node* prv = nullptr;

			while (ptr)
			{
				//当前值为目标值 执行删除操作
				if (ptr->_pair.first == key)
				{
					if (prv == nullptr)
						_tables[hashi] = ptr->_next;
					else
						prv->_next = ptr->_next;

					delete ptr;
					return true;
				}
				//当前值不为目标值 继续向下遍历
				else
				{
					prv = ptr;
					ptr = ptr->_next;
				}
			}
			return false;
		}

		//C++SGI版本STL库:获得下一个素数
		//在SGI下 设定哈希表的容量为素数
		//[可能效率更高 有兴趣的可以自行查阅]
		/*
		size_t GetNextPrime(size_t index)
		{
			static const int num = 28;
			static const unsigned long prime[num] =
			{
				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
			};

			size_t i = 0;
			for (i = 0; i < num; ++i)
			{
				if (prime[i] > index)
					return prime[i];
			}

			return prime[i];
		}
		*/

		//插入函数
		bool Insert(const pair<K, V>& pair)
		{
			//表中已有 返回false
			if (Find(pair.first))
				return false;

			Hash hash;

			//负载因子/荷载系数 -- Load_Factor = _n / _tables.size();
			//负载因子 == 1时扩容
			if (_n == _tables.size())
			{
				///  高级代码1.0  /
				/*
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable newht;
				newht.resize(newsize);
				for (auto& ptr : _tables)
				{
					while (ptr) 
					{
						newht.Insert(ptr->_pair);
						ptr = ptr->_next;
					}
				}

				_tables.swap(newht._tables);
				*/
				

				//初代扩容
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;

				//引进stl库素数思想
				//size_t newsize = GetNextPrime(_tables.size());
				
				vector<Node*> newtables(newsize, nullptr);
				//遍历旧表 取出旧表里每一个指针
				for (auto& ptr : _tables)
				{
					//ptr是旧表里存储的每一个指针
					//它指向当前哈希桶的首结点 即他存储的是首结点的地址
					while (ptr)
					{
						//算出 当前结点根据新哈希函数计算的新下标
						size_t hashi = hash(ptr->_pair.first) % newtables.size();

						//ptr是首结点的地址 ptr->_next为第二个结点的地址
						Node* next = ptr->_next;
						
						// 头插到新表
						ptr->_next = newtables[hashi];
						newtables[hashi] = ptr;
						
						//更新ptr 即向下遍历
						ptr = next;
					}
				}

				_tables.swap(newtables);
			}

			//哈希函数计算出的下标
			size_t hashi = hash(pair.first) % _tables.size();
			//链表头插
			Node* newnode = new Node(pair);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;        
			++_n;

			return true;
		}
		//最大桶数据个数
		size_t MaxBucketSize()
		{
			size_t max = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				auto ptr = _tables[i];
				size_t size = 0;
				while (ptr)
				{
					++size;
					ptr = ptr->_next;
				}

				//每个桶数据个数
				//printf("[%d] -> %d\n", i, size);

				if (size > max)
					max = size;
			}
			return max;
		}
	private:
		vector<Node*> _tables; // 指针数组
		size_t _n = 0;         // 存储有效数据个数
	};
  测试函数  //

	//插入测试
	void TestHashTable1()
	{
		int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
		HashTable<int, int> ht;
		for (auto e : a)
		{ 
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(15, 15));
		ht.Insert(make_pair(25, 25));
		ht.Insert(make_pair(35, 35));
		ht.Insert(make_pair(45, 45));
	}

	//插入和删除
	void TestHashTable2()
	{
		int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Erase(12);
		ht.Erase(3);
		ht.Erase(33);
	}

	//字符串问题
	void TestHashTable3()
	{
		//不实现类模板特化 显示传StringToInt
		//HashTable ht;
		//使用类模板特化 不需要显示传 更符合大佬设计的底层哈希
		HashTable<string, string> ht;
		ht.Insert(make_pair("Kevin", "凯文"));
		ht.Insert(make_pair("Eddie", "彭于晏"));
		ht.Insert(make_pair("Tom", "汤姆"));
		ht.Insert(make_pair("Jerry", "杰瑞"));
		ht.Insert(make_pair("", "null_string"));
	}

	//性能测试
	void TestHashTable4()
	{
		size_t N = 900000;
		HashTable<int, int> ht;
		srand(time(0));
		for (size_t i = 0; i < N; ++i)
		{
			size_t x = rand() + i;
			ht.Insert(make_pair(x, x));
		}

		cout << "最大桶数据个数" << ht.MaxBucketSize() << endl;
	}
}

你可能感兴趣的:(遣返回家的C家家,哈希算法,散列表,算法,开发语言,c++,数据结构,C语言)