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

news2024/11/23 17:14:41

目录

一、哈希概念

二、哈希实现

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),但是因为扩容操作的存在,这种最坏的情况几乎不可能出现。

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


关于哈希底层结构的内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/555749.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何用Postman做接口自动化测试?

本文适合已经掌握 Postman 基本用法的读者&#xff0c;即对接口相关概念有一定了解、已经会使用 Postman 进行模拟请求等基本操作。 工作环境与版本&#xff1a; Window 7&#xff08;64位&#xff09;Postman &#xff08;Chrome App v5.5.3&#xff09; P.S. 不同版本页面 U…

JAVA—实验4 继承、接口与多态

一、实验目的 掌握类的继承机制掌握接口的定义方法熟悉成员方法或构造方法多态性 二、实验内容 1&#xff0e;卖车&#xff0d;接口与多态编程 【问题描述】 (1) 汽车接口(Car)&#xff1a;有两个方法&#xff0c; getName()、getPrice()(接口源文件可以自己写&#xff0c;也…

2024总统大选,成为“关乎比特币未来的公投”?背后是怎样的政治抱负?

在今年的迈阿密比特币大会上&#xff0c;Robert F.Kennedy Jr和Vivek Ramaswamy相继发布声明表示&#xff0c;他们将在2024年初选前接受比特币&#xff08;BTC&#xff09;的捐款。 RFK Jr作为美国前总统约翰肯尼迪的侄子&#xff0c;是第一个公开接受Crypto的总统候选人&#…

chatgpt赋能Python-pythons_9_98_987

用Python计算s998987的方法及重要性分析 介绍 Python是一种开源的高级编程语言&#xff0c;它被广泛应用于数据处理、web开发和人工智能等领域。它的简洁、易读易写的语法使得很多程序员喜爱使用它来完成各种工作。本文将介绍如何用Python计算一个简单的数学表达式&#xff1…

微服务基础环境搭建--和创建公用模块

目录 微服务基础环境搭建 创建父工程&#xff0c;用于聚合其它微服务模块 创建父项目, 作为聚合其它微服务模块 项目设置​编辑 ​编辑 删除src, 保留一个纯净环境​编辑 1. 配置父工程pom.xml, 作为聚合其它模块 2、修改e-commerce-center\pom.xml,删除不需要的配置节…

Java.lang.NoClassDefFoundError: org/apache/logging/log4j/util/ReflectionUtil

具体问题描述如下&#xff1a; SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/D:/maven/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.6.2/log4j-slf4j-impl-2.6.2.jar!/org/slf4j/impl/StaticLoggerBinder.class] SL…

【Spring - beans】 BeanDefinition 源码

目录 1. BeanDefinition 1.1 AbstractBeanDefinition 1.2 RootBeanDefinition 1.3 ChildBeanDefinition 1.4 GenericBeanDefinition 2. BeanDefinitionReader 2.1 AbstractBeanDefinitionReader 2.2 XmlBeanDefinitionReader 2.3 GroovyBeanDefinitionReader 2.4 Pro…

(跨模态)AI作画——使用stable-diffusion生成图片

AI作画——使用stable-diffusion生成图片 0. 简介1. 注册并登录huggingface2. 下载模型3. 生成 0. 简介 自从DallE问世以来&#xff0c;AI绘画越来越收到关注&#xff0c;从最初只能画出某些特征&#xff0c;到越来越逼近真实图片&#xff0c;并且可以利用prompt来指导生成图片…

软件测试面试题——数据库知识

1、要查询每个商品的入库数量&#xff0c;可以使用以下SQL语句&#xff1a; SELECT 商品编号, SUM(入库数量) AS 入库数量 FROM Stock GROUP BY 商品编号;这将从Stock表中选择每个商品的入库数量&#xff0c;并使用SUM函数对入库数量进行求和。结果将按照商品编号进行分组&…

数据宝藏与精灵法师:探秘Elf擦除魔法的奇幻故事

在数字领域的奇幻王国中&#xff0c;大家视数据为宝藏。作为奇幻王国的国王&#xff0c;在他的宝库中&#xff0c;自然是有着无数的数据宝藏。这么多的数据宝藏&#xff0c;却让国王发难了。因为宝库有限&#xff0c;放不下这么多数据宝藏。因此&#xff0c;国王广招天下的精灵…

【备战秋招】每日一题:3月18日美团春招第三题:题面+题目思路 + C++/python/js/Go/java带注释

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&…

简易someip服务发现SD报文演示

环境 $ cat /etc/os-release PRETTY_NAME"Ubuntu 22.04.1 LTS" NAME"Ubuntu" VERSION_ID"22.04" VERSION"22.04.1 LTS (Jammy Jellyfish)" VERSION_CODENAMEjammy IDubuntu ID_LIKEdebian HOME_URL"https://www.ubuntu.com/"…

chatgpt赋能Python-pythonsum

Pythonsum&#xff1a;优秀的Python算法包介绍 Pythonsum是Python语言的一个优秀的算法包&#xff0c;具有很高的可重用性和性能&#xff0c;支持大规模数据处理和复杂算法实现。本文将为大家介绍Pythonsum的基本功能和优势。 Pythonsum的基本功能 Pythonsum提供了一系列丰富…

华为OD机试真题 Java 实现【对称字符串】【2023Q2 200分】

一、题目描述 对称就是最大的美学&#xff0c;现有一道关于对称字符串的美学。 已知&#xff1a; 第 1 个字符串&#xff1a;R 第 2 个字符串&#xff1a;BR 第 3 个字符串&#xff1a;RBBR 第 4 个字符串&#xff1a;BRRBRBBR 第 5 个字符串&#xff1a;RBBRBRRBBRRBRBBR …

扑克牌大小OJ题

题目链接 扑克牌大小_牛客题霸_牛客网 题目完整代码 #include <iostream> #include<string> #include<algorithm> using namespace std;// left_str 左边牌 // right_str 右边牌// left_count 左边牌数 // right_count 右边牌数// left_first 左边第一个牌…

chatgpt赋能Python-pythonsep怎么用

Python在SEO中的应用 Python一直是广受欢迎的编程语言之一&#xff0c;它拥有强大的功能和易于使用的特性&#xff0c;使得它成为了许多开发人员们的首选。“Pythonsep”是Python在SEO中的应用&#xff0c;它可以帮助用户更好地优化自己的网站&#xff0c;让网站更容易被用户发…

搭建python web环境----Django

第一步&#xff1a;安装Django 1.进入cmd&#xff1a;pip install django -i https://pypi.tuna.tsinghua.edu.cn/simple 2.检测版本&#xff1a; 第二步&#xff1a;配置环境变量 1.查找python安装位置: 2.打开django文件夹中bin文件夹&#xff1a; 查看django的安装位置&am…

火爆CV圈的SAM是什么?

SAM是什么 前言 最近几周&#xff0c;人工智能的圈子里都在讨论SAM&#xff08;Segment Anything Model&#xff09;&#xff0c;一个号称&#xff08;零样本&#xff09;分割一切的图像分割模型。 图&#xff1a;Segment Anything Demo 2023年4月6号&#xff0c;Meta AI发布…

npm install(报错)

1、npm install 报错&#xff08;如图&#xff09; WARN ERESOLVE overriding peer dependency npm WARN While resolving: intervolga/optimize-cssnano-plugin1.0.6 npm WARN Found: webpack3.12.0 npm WARN node_modules/webpack npm WARN peer webpack"^2.0.0 || ^3…

spring源码学习

1.xmlBeanFactory对defaultListableBeanFactory类进行扩展&#xff0c;主要用于从XML文档中获取BeanDefinition&#xff0c;对于注册及获取bean都是使用从父类DefaultListableBeanFactory继承的方法去实现。 xmlBeanFactory 主要是使用reader属性对资源文件进行读取和注册。 2.…