【数据结构】哈希底层结构

这篇具有很好参考价值的文章主要介绍了【数据结构】哈希底层结构。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一、哈希概念

二、哈希实现

1、闭散列

1.1、线性探测

1.2、二次探测

2、开散列

2.1、开散列的概念

2.2、开散列的结构

2.3、开散列的查找

2.4、开散列的插入

2.5、开散列的删除

3、性能分析


一、哈希概念

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

 理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素:
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
  • 搜索元素:
    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

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

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity;  capacity 为存储元素底层空间总的大小。
【数据结构】哈希底层结构

 用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。但是可能会造成哈希冲突。

哈希整体代码结构:

enum State
{
	EMPTY,
	EXIST,
	DELETE
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};

template<class K>
struct HashFunc
{
	//key本身可以进行隐式类型转换
	size_t operator()(const K& key)
	{
		return key;
	}
};

template<>
struct HashFunc<string>
{
	//BKDR哈希
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 31; //通过这种方式减少不同字符串的ascll码值相同造成的冲突问题
						//这个值可以取31、131、1313、131313等等
		}
		return hash;
	}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
	bool Insert(const pair<K, V>& kv)
	{}

private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0; //存储的数据个数
};

 模板参数中,Hash是一个仿函数,用于将key值转换成整型。 如果key是一个字符串类型,则使用特化,通过BKDR的方式转换成整型。

二、哈希实现

 对于两个数据元素的关键字 k_i 和 k_j(i != j),有 k_i != k_j,但有:Hash(k_i) ==Hash(k_j),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

 产生哈希冲突的原因是哈希函数设计不够合理。哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间。
  • 哈希函数计算出来的地址能均匀分布在整个空间中。
  • 哈希函数应该比较简单。

常见哈希函数:

  1. 直接定址法--(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。
    优点:简单、均匀。
    缺点:需要事先知道关键字的分布情况。
    使用场景:适合查找比较小且连续的情况。
  2. 除留余数法--(常用)
     设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

哈希冲突的解决主要有两种方法:闭散列和开散列。

1、闭散列

 闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

1.1、线性探测

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

插入

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

【数据结构】哈希底层结构

插入代码

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

    Hash hash;
	
    size_t hashi = hash(kv.first) % _tables.size(); //这里是size,而不是capacity
                                          //因为 [] 无法访问 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;
}

 因为可能出现 size 0 ,或者容量不够的情况,因此需要扩容操作:

【数据结构】哈希底层结构

//size为0,或者负载因子超过 0.7 就扩容
if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
{
	size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
	vector<HashData> newtables(newsize); //创建一个新的vector对象
	//遍历旧表,重新映射到新表
	for (auto& data : _tables)
	{
		if (data._state == EXIST)
		{
			//重新算在新表中的位置
            size_t hashi = hash(data.kv.first) % newtables.size();
			size_t i = 1;
			size_t index = hashi;
			while (newtables[index]._state == EXIST)
			{
				index = hashi + i;
				index %= newtables.size();
				++i;
			}

			newtables[index]._kv = data.kv;
			newtables[index]._state = EXIST;
		}
	}
	_tables.swap(newtables);
}

 需要注意的是,在扩容时,需要重新开辟一个 vector 对象,所有的数据都要重新插入一遍。而不能在原有的 vector 对象上扩容,因为这样做的话,扩容后,映射位置关系就变了,原来不冲突的值可能冲突了,原来冲突的值可能不冲突了。

 因为以上写法存在代码的冗余,所以可以采用如下写法简化代码:

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

    Hash hash;

	//负载因子超过 0.7 就扩容
	if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
	{
		size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		HashTable<K, V> newht;  //创建一个新的哈希表对象
		newht._tables.resize(newsize); //扩容
		//遍历旧表,重新映射到新表
		for (auto& data : _tables)
		{
			if (data._state == EXIST)
			{
				newht.Insert(data._kv);
			}
		}
		_tables.swap(newht._tables);
	}

	size_t hashi = hash(kv.first) % _tables.size(); //这里是size,而不是capacity

	//线性检测
	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;
}

 删除:

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

删除代码:

HashData<K, V>* Find(const K& key)
{
	if (_tables.size() == 0)
	{
		return nullptr;
	}

    Hash hash;

	size_t hashi = hash(key) % _tables.size();

	size_t i = 1;
	size_t index = hashi;
	while (_tables[index]._state != EMPTY)
	{
		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;
	}
}

 需要注意的是,为了防止哈希表中只有存在与删除而造成的死循环问题,在函数中需要增加一次判断,限制查找次数。

 线性探测优点:实现非常简单,
 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

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是表的大小。

 研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

2、开散列

2.1、开散列的概念

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

 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

2.2、开散列的结构

template<class K, class V>
struct HashNode
{
	HashNode<K, V>* _next;
	pair<K, V> _kv;

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

template<class K>
struct HashFunc
{
	//key本身可以进行隐式类型转换
	size_t operator()(const K& key)
	{
		return key;
	}
};

template<>
struct HashFunc<string>
{
	//BKDR哈希
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 31; //通过这种方式减少不同字符串的ascll码值相同造成的冲突问题
						//这个值可以取31、131、1313、131313等等
		}
		return hash;
	}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
    ~HashTable()
	{
		for (auto& cur : _tables)
		{
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			cur = nullptr;
		}
	}
	bool Insert(const pair<K, V>& kv)
	{}

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

2.3、开散列的查找

Node* Find(const K& key)
{
	if (_tables.size() == 0)
		return nullptr;

    Hash hash;

	size_t hashi = hash(key) % _tables.size();
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
			return cur;
		cur = cur->_next;
	}
	return nullptr;
}

2.4、开散列的插入

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

    Hash hash;

    size_t hashi = hash(kv.first) % _tables.size();
	//头插
	Node* newnode = new Node(kv);
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	++_n;

	return true;
}

 因为有可能出现哈希表的 size 为 0 ,或者需要扩容的情况。所以需要给哈希表扩容。负载因子越大,冲突的概率越高,查找的效率就越低,同时空间利用率越高。

 因为原表中的节点都是自定义类型的,所以不会被自动析构。我们只需要把原表中的节点重新计算位置,挪动到新表就可以了。

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

    Hash hash;

	//负载因子为1时,扩容
	if (_n == _tables.size())
	{
		size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		vector<Node*> newtables(newsize, nullptr);
		for (Node*& cur : _tables)
		{
			while (cur)
			{
				Node* next = cur->_next;
				size_t hashi = hash(cur->_kv.first) % newtables.size();
				//头插到新表
				cur->_next = newtables[hashi];
				newtables[hashi] = cur;

				cur = next;
			}
		}
		_tables.swap(newtables);
	}

	size_t hashi = hash(kv.first) % _tables.size();
	//头插
	Node* newnode = new Node(kv);
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	++_n;
	return true;
}

2.5、开散列的删除

bool Erase(const K& key)
{
    Hash hash;

	size_t hashi = hash(key) % _tables.size();
	Node* prev = nullptr;
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			if (prev == nullptr)
			{
				_tables[hashi] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			delete cur;
			return true;
		}
		else
		{
			prev = cur;
			cur = cur->_next;
		}
	}
	return false;
}

 应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

3、性能分析

 对于开散列的哈希来说,增删查改的时间复杂度是 O(1) ,虽然在最坏的情况下(所有的值都挂在同一个下标上,即在同一个桶中),时间复杂度是 O(N),但是因为扩容操作的存在,这种最坏的情况几乎不可能出现。

 如果真的出现了极端情况,导致所有的数据都在一个桶中。则可以采取当单个桶超过一定的长度,就把这个桶改挂成红黑树的方式:把哈希数据类型设置为结构体,结构体中包括链表指针、桶长度以及树的指针,如果桶的长度超出指定数值,就使用树的指针,反之则使用链表指针。


关于哈希底层结构的内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!文章来源地址https://www.toymoban.com/news/detail-455647.html

到了这里,关于【数据结构】哈希底层结构的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 【数据结构】HashSet的底层数据结构

    【数据结构】HashSet的底层数据结构

    🐌个人主页: 🐌 叶落闲庭 💨我的专栏:💨 c语言 数据结构 javaEE 操作系统 Redis 石可破也,而不可夺坚;丹可磨也,而不可夺赤。 Set 系列集合 无序:存取顺序不一致 不重复:可以去除重复 无索引:没有带索引的方法,所以不能使用普通fo循环遍历,也不能通过索引来获

    2024年03月16日
    浏览(13)
  • 【数据结构】哈希表——闭散列 | 开散列(哈希桶)

    【数据结构】哈希表——闭散列 | 开散列(哈希桶)

    🐱作者:一只大喵咪1201 🐱专栏:《数据结构与算法》 🔥格言: 你只管努力,剩下的交给时间! 哈希(Hash):是一种方法,将数据的key值和存储位置建立关系。 在之前学习过的顺序结构以及平衡树中,所有数据的key值和存储位置之间都没有对应的关系。所以在查找一个数据

    2023年04月24日
    浏览(18)
  • 数据结构之—哈希表

    数据结构之—哈希表

    目录 一、哈希表的概念 1.前言 2.概念 二、哈希函数:将任意一个key值映射成整数 1.哈希函数最常用的方法:取模 2.哈希函数设计原则 3.比较对象相等时,hashCode与equals关系 4.MD5:一般给字符串进行hash运算 1)MD5的三大特点:定长、分散、不可逆 2)MD5应用 三、哈希冲突 1.概念

    2024年02月08日
    浏览(14)
  • 数据结构:哈希表讲解

    数据结构:哈希表讲解

    哈希: 一种 映射思想 ,也叫散列。即和另一个值建立一个关联关系。注意 这里指的关联关系是多样的 ,比如给你,你可以通过映射关系确定该值在不在或者获得其它信息,不一定要存储另一个值。 哈希表 :也叫散列表,体现了哈希思想。即和存储位置

    2024年02月05日
    浏览(9)
  • 哈希表----数据结构

    哈希表----数据结构

    如果你是一个队伍的队长,现在有 24 个队员,需要将他们分成 6 组,你会怎么分?其实有一种方法是让所有人排成一排,然后从队头开始报数,报的数字就是编号。当所有人都报完数后,这 24 人也被分为了 6 组,看下方。 (你可能会让 1~4 号为第一组,5~8 号为第二组……但

    2024年02月05日
    浏览(11)
  • 数据结构——哈希

    哈希表 是一种使用哈希函数组织数据的数据结构,它支持快速插入和搜索。 哈希表(又称散列表)的原理为:借助 哈希函数,将键映射到存储桶地址。更确切地说, 1.首先开辟一定长度的,具有连续物理地址的桶数组; 2.当我们插入一个新的键时,哈希函数将决定该键应该

    2024年02月09日
    浏览(6)
  • 数据结构之哈希

    数据结构之哈希

    哈希(Hash)是一种将任意长度的二进制明文映射为较短的二进制串的算法。它是一种重要的存储方式,也是一种常见的检索方法。哈希函数通过特定方式(hash函数)处理输入,生成一个值。这个值等同于存放数据的地址,这个地址里面再把输入的数据进行存储。 哈希算法是

    2024年02月11日
    浏览(9)
  • 【数据结构】哈希应用

    【数据结构】哈希应用

    目录 一、位图 1、位图概念 2、位图实现 2.1、位图结构 2.2、比特位置1 2.3、比特位置0 2.4、检测位图中比特位 3、位图例题 3.1、找到只出现一次的整数 3.2、找到两个文件交集 3.3、找到出现次数不超过2次的所有整数 二、布隆过滤器 1、布隆过滤器提出 2、布隆过滤器概念 3、布

    2024年02月08日
    浏览(9)
  • 【数据结构】哈希

    【数据结构】哈希

    unordered 系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。 顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为 O(N),平衡树中为树的高度,即 O( l o g 2

    2024年04月14日
    浏览(7)
  • 数据结构-哈希表(二)

    给你两个整数数组  nums1  和  nums2  ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。 示例 1: 给你两个整数数组  nums1  和  nums2  ,

    2024年04月14日
    浏览(6)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包