• 哈希/散列--哈希表[思想到结构]



    在这里插入图片描述

    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.代码实现[配备详细注释]

    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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187

    3.2开散列

    //开散列/链地址法
    namespace ChainAddressing
    {
    	//结点类
    	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)
    		{
    		
    		}
    	};
    
    	//哈希表类
    	template<class K, class V>
    	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;
    
    			//哈希函数计算的下标
    			size_t hashi = 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)
    		{
    			//哈希函数计算的下标
    			size_t hashi = 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;
    		}
    
    		//插入函数
    		bool Insert(const pair<K, V>& pair)
    		{
    			//表中已有 返回false
    			if (Find(pair.first))
    				return false;
    
    			//负载因子/荷载系数 -- 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;
    				vector<Node*> newtables(newsize, nullptr);
    				//遍历旧表 取出旧表里每一个指针
    				for (auto& ptr : _tables)
    				{
    					//ptr是旧表里存储的每一个指针
    					//它指向当前哈希桶的首结点 即他存储的是首结点的地址
    					while (ptr)
    					{
    						//算出 当前结点根据新哈希函数计算的新下标
    						size_t hashi = 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 = pair.first % _tables.size();
    			//链表头插
    			Node* newnode = new Node(pair);
    			newnode->_next = _tables[hashi];
    			_tables[hashi] = newnode;        
    			++_n;
    
    			return true;
    		}
    
    	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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
  • 相关阅读:
    『 C++类与对象』继承
    MySQL Sharding + 读写分离配置说明
    嵌入式Linux 调试常用工具与知识
    深度优先搜索(java)
    Ubuntu安装SVN服务并结合内网穿透实现公网访问本地存储文件
    【MLOps】优化超参数
    微服务之 consul
    依据象限搜索及混合预计耗费的A*改进算法,包含8邻域及24邻域的改进
    WPS论文编写问题集(参考文献制作、公式居中及编号、公式影响行间距...)_长期更新中ing...
    链表问题 — — 高频面试题【LeetCode - 138】
  • 原文地址:https://blog.csdn.net/LHRan_ran_/article/details/133500622