哈希(C++)

一、哈希的概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素

时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即

O(logN),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立

一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

(1)插入元素

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

(2)搜索元素

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

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称

为哈希表(Hash Table)(或者称散列表)

注意:用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

二、哈希冲突

概念:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

三、哈希函数

哈希函数设计原则:

(1)哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值

域必须在0到m - 1之间

(2)哈希函数计算出来的地址能均匀分布在整个空间中

(3)哈希函数应该比较简单

常见哈希函数

1.直接定址法(常用)

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

优点:简单、均匀

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

使用场景:适合查找比较小且连续的情况,值得分补范围比较集中

2.除留余数法(常用)

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

3.平方取中法(了解)

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

4.折叠法(了解)

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

  1. 随机数法(了解)

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

6.数学分析法(了解)

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

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

四、哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列-开放定址法和开散列

4.1闭散列-开放地址法

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有

空位置,那么可以把key存放到冲突位置中的"下一个" 空位置中去,按照规则找下一个位置,

(也就是占用别人得位置)

4.1.1.线性探测

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

插入

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

删除

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

哈希表什么情况下进行扩容?如何扩容?

散列表的载荷因子定义为:α= 填入表中的元素个数 / 散列表的长度

α是散列表装满程度的标志因子。由于表长是定值,α与"填入表中的元素个数"成正比,所以,Q越大,表明填入表中的元素越多,发生冲突的可能性就越大;反之,α越小,表明填入表中的元素越少,发生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数.

对于开放定址法,负载因子是特别重要的因素,应严格限制在0.7 - 0.8以下。超过0.8,查表时的CPU缓存不命中(cache缺失)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将调整大小散列表。

总结:载荷因子越大,冲突的概率越大,空间利用率越高;载荷因子越小,冲突的概率越小,空间利用率越低

所以,哈希表不能满了在扩容,而是负载因子到一定的值就要开始进行扩容

线性探测哈希(开放地址法)实现

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
#include<stdbool.h>
#include<string>
using namespace std;

//开放地址法:不需要写析构函数,vector自己的析构就能实现析构
namespace open_address
{
	//定义状态
	enum STATE
	{
		DELETE,
		EXIST,
		EMPTY
	};

	template<class K, class V>
	class HashData
	{
	public:
		STATE _sta;
		pair<K, V> _kv;
	};



	//使用仿函数对其进行K进行整型的转换
	template<class K>
	struct DefaultHashFunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};


	struct StringHashFunc
	{
		size_t operator()(const string& str)
		{
			return str[0];
		}
	};

	//如果不想传StringHashFunc,就可以使用模板的特化
	template<>
	struct DefaultHashFunc<string>
	{
		//这里的字符串哈希算法使用的是BKDR哈希算法
		size_t operator()(const string& str)
		{
			size_t hash = 0;

			for (auto element : str)
			{
				hash = hash * 131;
				hash += element;
			}

			return hash;
		}
	};

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		//直接使用构造函数开辟空间
		HashTable()
		{
			_table.resize(10);
		}

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

			//扩容(扩容之后映射关系会改变,需要重新映射)
			if ((double)_n / _table.size() >= 0.75)//计算载荷因子一i的那个记得强制类型转换
			{
				//重新定义一个HashTable的对象开辟newSize的空间大小
				size_t newSize = _table.size() * 2;
				HashTable<K, V, HashFunc> newHashTable;
				newHashTable._table.resize(newSize);

				//遍历旧表,将原来在旧表里的数据全部映射到新表
				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._sta == EXIST)
					{
						newHashTable.Insert(_table[i]._kv);
					}
				}
				//交换
				_table.swap(newHashTable._table);
			}

			//线性探测
			HashFunc hf;
			size_t hashi = hf(kv.first) % _table.size();//这里不能模capacity,因为size后面的方括号无法访问
			//判断当前节点的状态是否为空或者删除
			while (_table[hashi]._sta == EXIST)
			{
				++hashi;

				//可能走到结尾要返回去,每次都模一下
				hashi %= _table.size();
			}

			//赋值
			_table[hashi]._kv = kv;
			_table[hashi]._sta = EXIST;
			++_n;

			return true;
		}

		HashData<const K, V>* Find(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			while (_table[hashi]._sta != EMPTY)
			{
				if (_table[hashi]._sta == EXIST && _table[hashi]._kv.first == key)
				{
					return (HashData<const K, V>*) & _table[hashi];
				}
				++hashi;
				hashi %= _table.size();
			}
			return nullptr;
		}

		bool erase(const K& key)
		{
			HashData<const K, V>* ret = Find(key);
			if (ret != nullptr)
			{
				ret->_sta = DELETE;
				--_n;

				return true;
			}
			return false;
		}
	protected:
		vector<HashData<K, V>> _table;
		size_t _n;//表示存储的有效数据个数
	};
}

线性探测测试函数

cpp 复制代码
int main()
{
	using namespace open_address;
	//哈希测试
	int a[] = { 1,2,3,4,6,11,21,89 };
	HashTable<int, int> hs1;
	for (auto e : a)
	{
		hs1.Insert(make_pair(e, e));
	}

	HashData<const int, int>* ret1 = hs1.Find(21);
	ret1->_kv.second = 21;

	HashTable<string, string> hs2;
	hs2.Insert(make_pair("hello", "你好"));
	hs2.Insert(make_pair("insert", "插入"));
	hs2.Insert(make_pair("love", "X"));

	HashData<const string, string>* ret2 = hs2.Find("love");

	ret2->_kv.second = "爱";

	DefaultHashFunc<string> hf3;
	cout << hf3("xxx") << endl;
	cout << hf3("abcd") << endl;
	cout << hf3("dcba") << endl;

	return 0;
}

线性探测优点:实现非常简单,

线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起(冲突会互相影响),容易产生数据"堆积",即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

4.1.2.二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位

置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法

为:H_i = (H_0 + i^2) % m, 或者:H_i = (H_0 - i ^ 2) % m。其中:i =1, 2, 3..., H_0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

4.2开散列(拉链发/哈希桶)

开散列概念:

开散列法又叫链地址法(开链法/哈希桶),首先对关键码集合用散列函数计算散列地址,具有相同地

址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

注意:开散列在进行插入的时候也要进行扩容,如果不扩容单个桶就会很长,就和链表无疑了,效率得不到保证了,但是载荷(负载)因子可以适当的大一点(把载荷因子控制到1)

开散列/哈希桶实现

cpp 复制代码
//开散列/哈希桶:编译器默认生成的析构函数无法完成彻底析构
namespace Hash_Bucket
{
	template<class K, class T>
	class HashNode
	{
	public:
		HashNode(const pair<K, T>& kv)
			:_kt(kv)
			,next(nullptr)
		{
		}
		HashNode(const HashNode<K,T>& node)
			:_kt(node._kt)
			, next(nullptr)
		{
		}
	public:
		pair<K, T> _kt;
		HashNode<K, T>* next;
	};

	//仿函数支持字符串的比较
	template<class K>
	class DefaultHashFunc
	{
	public:
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	//特化
	template<>
	class DefaultHashFunc<string>
	{
	public:
		size_t operator()(const string& str)
		{
			size_t ret = 0;
			for (auto ch : str)
			{
				ret += ch;
				ret *= 131;
			}
			return ret;
		}
	};

	template<class K, class T, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		typedef HashNode<K, T> node;

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

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

		HashTable(const HashTable<K, T>& ht)
		{
			vector<node*> v;
			v.resize(ht._table.size(), nullptr);
			_n = ht._n;

			 for (size_t i = 0; i < ht._table.size(); i++)
			{
				node* cur = ht._table[i];
				node* tail = nullptr;
				while (cur)
				{
					node* newnode(cur);
					if (tail == nullptr)
					{
						//第一个节点
						_table[i] = newnode;
					}
					else
					{
						//尾插
						tail->next = newnode;
					}
					tail = newnode;
				}
			}
		}
		

		bool Insert(const pair<K, T>& kv)
		{

			HashFunc hf;

			if (Find(kv.first))
			{
				return false;
			}

			//扩容,将载荷因子控制到1即可
			if (_n / _table.size() >= 1)
			{

				size_t newSize = _table.size() * 2;
				vector<node*> newtable;
				newtable.resize(newSize, nullptr);

				//遍历_table,将其全部挂到新表上
				for (size_t i = 0; i < _table.size(); i++)
				{
					node* cur = _table[i];
					while (cur != nullptr)
					{
						node* next = cur->next;
						size_t hashi = hf(cur->_kt.first) % newSize;

						cur->next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newtable);
			}

			
			size_t hashi = hf(kv.first) % _table.size();
			//头插
			node* newnode = new node(kv);
			newnode->next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return true;
		}

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

		}

		node* Find(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			node* cur = _table[hashi];
			while (cur)
			{
				if (cur->_kt.first == key)
				{
					return cur;
				}
				cur = cur->next;
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			node* prev = nullptr;
			node* cur = _table[hashi];
			while (cur)
			{
				if (cur->_kt.first == key)
				{
					//进行删除分两种情况,有可能cur就是哈希桶的第一个节点和不是第一个
					if (prev == nullptr)
					{
						//头删
						_table[hashi] = cur->next;
					}
					else
					{
						prev->next = cur->next;
					}
					delete cur;
					cur = nullptr;
					return true;
				}
				prev = cur;
				cur = cur->next;
			}
			return false;
		}
	protected:
		vector<node*> _table;//指针数组
		size_t _n;//记录存储的数据个数
	};
}

开散列/哈希桶测试函数

cpp 复制代码
#define N 100
int main()
{
	using namespace Hash_Bucket;

	HashTable<int, int> ht1;
	ht1.Insert(make_pair(1, 2));
	ht1.Insert(make_pair(2, 3));

	ht1.Print();

	cout << endl;

	//扩容测试

	int a[] = { 12,4,29,6,8,4,23,12,45,78,89 };
	for (auto element : a)
	{
		ht1.Insert(make_pair(element, element));
	}
	ht1.Print();
	cout << endl;

	ht1.Erase(12);
	ht1.Erase(29);

	ht1.Print();

	cout << endl;

	//随机数测试
	HashTable<int, int> ht2;
	srand((size_t)time(0));
	for (size_t i = 0; i < N; i++)
	{
		int ret = rand();
		ht2.Insert(make_pair(ret, ret));
	}
	ht2.Print();

	HashTable<string, string> hf3;
	hf3.Insert(make_pair("like", "喜欢"));
	hf3.Insert(make_pair("miss", "想念、错过"));

	hf3.Print();

	return 0;
}
相关推荐
十五年专注C++开发2 小时前
Xapian: 一款C++全文检索解决方案
c++·全文检索
2501_930707782 小时前
使用C#代码向 Word 文档添加文档属性
开发语言·c#·word
skywalk81632 小时前
为什么Linux系统里用户id和组id不一样?怎么改成一样呢?
linux·服务器
加成BUFF2 小时前
Qt开发核心工具:CMake与qmake全面解析
开发语言·qt·cmake·qmake
野生风长2 小时前
从零开始的C语言:文件操作与数据管理(下)(fseek,ftell,rewind,文件的编译和链接)
android·java·c语言·开发语言·visual studio
阿蒙Amon2 小时前
C#每日面试题-属性和字段的区别
开发语言·c#
百锦再2 小时前
UniApp与UniApp X:跨平台开发的范式革命与全面技术解析
服务器·ai·uni-app·k8s·core·net
资深web全栈开发2 小时前
LeetCode 2054:两个最好的不重叠活动 —— 从暴力到优化的完整思路
算法·leetcode
IT方大同2 小时前
数组的初始化与使用
c语言·数据结构·算法