哈希原理和解决哈希冲突方法

news2024/12/23 11:06:59

   第一 哈希介绍

   哈希和红黑树是我早期就听过的名词,却一直没见到真面目,实现哈希后才发现原来我早就使用过了哈希。看下面例题。

  用map和set都可以轻松解决,但是在我写这题时我还不会用map和set,我用了另一种方法。看下面代码。先定义一个数组,因为题目说了astr中只会出现小写字母,所以数组只需要开26个空间然后将字母的ASCii码值-'a'做下标例如astr[i]是字符a,-'a'那下标就是0,就在arr[0]处++,表示出现次数+1, 如果arr在这个下标处存的数字不为0,那表示出现次数不为一,返回false,表示字符不唯一出现代码如下这种映射法存字符就是一种哈希函数,非常方便查找,我们用arr[astr[i]-'a']找出现次数的时间复杂度是O(1),如果用map和set则要O(logn)。这就是哈希强大之处。

class Solution {
public:
    bool isUnique(string astr) 
    {
        int arr[26]={0};
        for(int i=0;i<astr.size();i++)
        {
            if(arr[astr[i]-'a']==0)//判断该字符是否已经存在
            {
                arr[astr[i]]++;//不存在该位置数字+1
            }
            else
             return false;
        }
        return true;
    }
};

事实上哈希还和计数排序有点相似,

  如果是这个数组是给哈希用的,当4来了的时候,它的放置下标i=key%14,(这就是另外一个常用的哈希函数,由于值和空间是多对一的,由此产生出哈希冲突等问题),所以下标i就是4,并且把4放到该数组中去,而计数排序是把出现次数放入数组,这就是两者的不同之处。

  如果又来了个数字18,i计算结果也是4,那这时候难道让18去覆盖4吗,当然不行,这时候出现的状况就称为哈希冲突,是不是很好理解,那应该存哪呢?既然4位置处满了,那就往后找下一个空格处存就好了,这会不会找不到,丢了呢?不会,你想想,首先如果4下标位置处不在,那就肯定是存在4的后面,一直往后找就行了,当你遇到空格了都还没找到,那就说明这个数不存在,而如何找下一个空格则有不同的方法,如果是一个个往后找,这就是闭散列的线性探测,如果是key=key+i^2(i为查找次数),则称为二次探测,可以拉开数据间的空隙,不会让数据都堆积在一起,了解即可。下面来看看闭散列线性探测的相关实现。

第二 闭散列实现

1 HashData类

可以认为哈希数组中每个元素是个HashData类型的数据,这个自定义类型内存了个pair类型的 _kv,pair的first就是key,用来计算下标,算到什么值,就把pair放到哪个位置,而不是对pair的second++,不要和map搞混了,写博客才发觉HashData存个pair好像容易混淆。

     enum State
	{
		Empty,
		Exist,
		Delete
	};

    template<class K, class T>
	struct HashData
	{
		pair<K, T> _kv;
		State _state = Empty;  默认为空
	};

  还有个成员是_state,这个是什么呢?这就涉及到前面忽略的问题,如果原来4下标放了一个4,再来个18会冲突,往后存,如果之后我们把4删除了,然后找18,那先用18算key,从4开始找,可以4这个位置元素被删了,那一般也就是置为零,表示该元素空了,这不就是找到空格,算没找到18,直接结束吗。

   诶,好像哪里不对,那你可能会说不对,用零表示被删,-1表示空,这样18来找的时候,发现4上是0,就知道这是被删除了,18可能在后面,这么来看好像说得通,可是用数字表示状态本身就有个大问题,你看看0下标处本来就要存0,难道这表示删除?然后14来了把它覆盖?

  回想红黑树,是用枚举常量表示节点是红还是黑的状态,那我们也可以用枚举常量表示空,删除和存在三种状态,而且十分直观。

2 HashTable类

   在看insert成员函数时,有个概念要提一下,负载因子,由于线性探测是往后找空位置放冲突元素,本质上是占用别人的位置来解决自己的冲突但是如果哈希表上的空格越来越少,那每次插入元素都要往后查找可能接近O(N),就出现退化了,所以用_size记录存放的元素个数,当超过一定值,就要扩容,保持哈希表的空格是比较充裕的,这就会使得空间上会有浪费,这是无法避免的。

template<class K, class T, class Hashfun=DefaultHashfunc<K>>
	class HashTable
	{
	public:
		
		bool insert(const pair<K, T> kv)
		{
			Hashfun hf;
			if (_size * 10 / _table.size() >= 7)  负载因子大于0.7时,扩容
                                         
			{
				size_t newsize = _table.size() * 2;
				HashTable<K, T> newtable;
				newtable._table.resize(newsize);先开好空间,避免频繁扩容
				
                遍历旧表,将数据弄到新表
				for(size_t i = 0; i < _table.size(); i++)
				{
					if(_table[i]._state==Exist)//该数据存在才插入
					newtable.insert(_table[i]._kv);
				}

				新旧表交换,只用交换_table, _size无需变换

				_table.swap(newtable._table);

   原先的哈希表无需写析构函数,是vector会调用自己析构,vector存的元素是HashData
   HashData内存的是一个pair,所以都无资源需要我们释放。

			}
			
             仿函数处理key,下面讲模板参数会提及
            int hashi = hf(kv.first) % (_table.size());
			
            线性探测
			
            while (_table[hashi]._state == Exist)   该位置已存在数字
			{
				if (_table[hashi]._state == Exist && _table[hashi]._kv == kv)
					return false;   已存在该值
				hashi++;
				hashi %= _table.size();  防止越界
			}
			_table[hashi]._kv = kv;
			_table[hashi]._state = Exist;
			_size++;
			return true;
		}
  
	private:
		vector <HashData<K, T>> _table;
		size_t _size = 0; 记录哈希表中存在元素个数
	};

  3 模板参数解析

   先前已经把哈希的基本原理解释完了,但是有个细节点要先补充一下,然后才好把剩下的成员函数介绍完。我们说来了一个4,或者18,就是直接%数组空间大小求下标,如果哈希表要存一个字符串呢,怎么取模?首字符的ASCII码值?这样首字符相同的就都会冲突了,那就把字符串全部的ASCII码值加起来,诶这个貌似极大地减少了冲突,那如果我拿出下面这两个例子呢?"abc"和"bbb"这个也会冲突,阁下又该如何应对呢?前辈们早已经研究出了算法,也就是BKDR算法,本文用仿函数实现的,如下。

   先说个我们写的仿函数巧妙的地方,我们用了模板参数,如果K是int,double这些内置类型,那就调用下面的直接返回,如果是string,那就调用更匹配,就不用我们显示传了,它会直接根据DefaultHashfunc<string>找我们写好的,不然遇到K为string,我们没办法让它调用对应的仿函数。


	template<class K>    
	struct DefaultHashfunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	template<>
	struct DefaultHashfunc<string> 模板特化
	{  
         改进,加上所有字符的ASCii码值,并且用BKDR算法降低冲突

		size_t operator()(const string& s)
		{
			int hash = 1;
			for (auto e : s)
			{
                 hash = hash * 131 + e;  
            
          BKDR,实质是在加上字符的ASCII码时加一个数越来越大的数字
            
                 hash*= 131; 
            } 
			return (size_t)hash;
      最后把求的值之和返回,用于%数组空间大小,
      溢出也无所谓,因为find求下标前也会溢出,也就会用截断后的去找

		}
	};
	

4 其余成员函数

        HashTable()  构造函数
		{
			_table.resize(5); 开空间得用resize,reserve会越界,例如reserse 4个空间,
            我们却不能直接访问这几个空间,因为vector内有效元素为0,[]访问第一个后面的空间
            会报错
		}


	     bool Erase(const K&key)
		{
			pair<K, T> ret = find(key);  先找到该节点
			if (ret)
			{
				ret.second = Delete;  修改状态
				return true;
			}
			return false;
		}

		HashData<const K,T>*  find(const K&key)
		{
			Hashfun hf;
			int hashi = hf(key) % (_table.size());  算下标
			while(_table[hashi]._state!=Empty)  该位置不为空,就继续往后找
			{
				if (_table[hashi]._state == Exist&&_table[hashi]._kv.first==key)
				{
					return (HashData<const K, T>*)&_table[hashi];//防止该编译器不支持隐式类型转换
				}
				hashi++;
			}
			//找到空位置,说明没有该元素,返回空
			return nullptr;
		}

这就是哈希闭散列的全部实现,虽然比红黑树简单,但是细节点也不少。如果看到这里的读者还有冲劲,就向着开散列冲刺吧,我第一次接触感觉挺震撼的。

第三 开散列实现

  闭散列解决冲突的方法终究还是太粗暴了,冲突就占用其它元素的位置,把火都烧到别的地方了,所以大佬们又想到一种方法,哈希桶。我画个来演示大家就知道了。

   这种方式的巧妙在于将冲突元素化为链表链接,形成一个桶,故称哈希桶,不管有多少冲突,都插入到链表中去,查找就在链表里找,找到空都没找到那就说明该元素不存在。接下来看实现

1 HashNode类

  先前的HashData类之所以没有写构造函数,那是数组开好空间了,初始化好了,我们只要往里面放数据就好了,现在是insert的时候要new一个节点出来,我们写好构造函数,方便new初始化,也可以后面自己在手动赋值,反正节点指针你也有,修改节点内的数据还不是简简单单吗。

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

2 HashBucketTable类

有了前面开散列的基础,接下来的介绍就可以省略一点了。

1 先看构造和析构函数以及成员变量


template<class K,class V, class Hashfun = DefaultHashfunc<K>>
class HashBucketTable
{
public:
	typedef HashNode<K, V> Node;
	HashBucketTable()
	{
		_table.resize(5);  直接resize开空间就好,注意别用reserve
	}
	
	~HashBucketTable()
	{
		for (size_t i = 0; i < _table.size(); i++)  遍历哈希表上的链表
		{
			Node* cur = _table[i];
			while (cur) 一个一个delete
			{
				Node* next = cur->_next;
				_table[i] = next;
				delete cur;
				cur = next;
			}
		}
	}
	
private:
	vector<Node*> _table; 存的是一个节点的指针,节点通过_next指针变量链接其它节点
	size_t _size=0;
};

2 其余成员函数

   bool insert(const pair<K,V>& kv)
	{
		//查找该节点是否已经存在
		if(find(kv.first))
			return false;

        Hashfun hf; 将自定义类型key转为整数的仿函数
		 
		if (_size >= _table.size()) 当节点数足够哈希表的每个桶上挂上一个节点就扩容
		{
            开新表
			size_t newsize = _table.size() * 2;
			vector<Node*> newtable;
			newtable.resize(newsize);

			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
                    注意链接后面的节点,免得丢了
					Node* next = cur->_next;
					_table[i] = next; 
					size_t hashi = hf(cur->_kv.first) % newtable.size();
					要重新计算映射下标,size变了,取模结果可能会变
                    
                    将原先节点搬到新表
					
                    cur->_next = newtable[hashi];
					newtable[hashi] = cur;
					cur = next;  搬下个节点
				}
			}
			_table.swap(newtable);
		}
           正常插入数据 
 		size_t hasi = hf(kv.first) % _table.size();
		Node* newnode = new Node(kv);
		newnode->_next = _table[hasi];
		_table[hasi]=newnode;
		_size++;
		return true;
	}

    HashBucketTable(HashBucketTable<K,V>& ht)
	{
         Hashfun hf;

		_table.resize(ht._table.size());
		for (size_t i = 0; i < ht._table.size(); i++)  遍历哈希表ht
		{
			Node* cur = ht._table[i];
			while (cur)    遍历该桶
			{
				Node* newnode = new Node(cur->_kv);
				size_t hashi = hf(cur->_kv.first)% _table.size();
				newnode->_next = _table[hashi];
				_table[hashi] = newnode;
				cur = cur->_next;//去桶的下一个节点
			}
		}
	}
	Node* find(const K& key)
	{
        Hashfun hf;
		size_t hashi = hf(key) % _table.size();
		Node* cur = _table[hashi]; 直接定位到该桶去找
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				return cur;
			}
			cur = cur->_next;
		}
		return nullptr;
	}
	bool Erase(const K& key)  
	{
        Hashfun hf;
        if(!find(const K& key))先用key找节点,找不到返回false
          return false;

		size_t hashi = hf(key) % _table.size();
		Node* pre = nullptr;
		Node* cur = _table[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (pre == nullptr)  处理头删
				{
					_table[hashi] = cur->_next;  链接
				}
				else
				{
					pre->_next= cur->_next;
				}
				delete cur;
				--_size;  负载因子要--
				return true;
			}
			pre = cur;
			cur = cur->_next; 往链表后找删除节点
		}
		return false;
	}

    哈希桶查找数据几乎可以说完美了,只是理论上还会有个缺陷,那就是链表可能还是太长,那怎么办呢?有人就设计将其化为一颗红黑树,红黑树加哈希桶,这就几乎万无一失了,链表转红黑树也就是遍历链表把数据一个个插入到红黑树即可。

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

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

相关文章

【Java】微服务——Nacos配置管理(统一配置管理热更新配置共享Nacos集群搭建)

目录 1.统一配置管理1.1.在nacos中添加配置文件1.2.从微服务拉取配置1.3总结 2.配置热更新2.1.方式一2.2.方式二2.3总结 3.配置共享1&#xff09;添加一个环境共享配置2&#xff09;在user-service中读取共享配置3&#xff09;运行两个UserApplication&#xff0c;使用不同的pr…

Office Tool Plus下载与神龙版官网下载

文章目录 一、Office Tool Plus下载二、神龙下载 Office Tool Plus简称OTP&#xff0c;是一款专业的Office官方镜像下载器&#xff0c;可以下载安装Word、Excel、Visio等。神龙用于快速激活使用。 注意&#xff1a;Win7系统必须要SP1或更高版本才能使用&#xff0c;Office Tool…

intel 一些偏门汇编指令总结

intel 汇编手册下载链接&#xff1a;https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html LDS指令&#xff1a; 手册中可以找到 位于 3-588 根据手册内容猜测&#xff1a;lds r16 m16:16 的作用&#xff0c;是把位于 [m16:16] 内存地址的数…

JMeter的详细使用及相关问题

一、中文乱码问题 如果出现乱码&#xff0c;需要修改编码集&#xff0c;&#xff08;版本问题有的不需要修改&#xff0c;就不用管&#xff09; 修改后保存重启就好了。 JMeter5.5版本的按照如下修改&#xff1a; 二、JMeter的启动 ①建议直接用ApacheJMeter.jar双击启动…

Zabbix4自定义脚本监控MySQL数据库

一、MySQL数据库配置 1.1 创建Mysql数据库用户 [rootmysql ~]# mysql -uroot -p create user zabbix127.0.0.1 identified by 123456; flush privileges; 1.2 添加用户密码到mysql client的配置文件中 [rootmysql ~]# vim /etc/my.cnf.d/client.cnf [client] host127.0.0.1 u…

CSDN博主粉丝数突破10万:坚持分享的力量与收获

今天&#xff0c;我在CSDN上看到了一位好友的统计数据&#xff0c;他统计了CSDN上所有粉丝数量排名靠前的博主的排名。虽然这个统计可能存在一些误差&#xff0c;但大体上应该是准确的。我惊讶地发现&#xff0c;截止到2023年10月4日&#xff0c;我的粉丝数量已经达到了101,376…

QScrollArea样式

QScrollBar垂直滚动条分为sub-line、add-line、add-page、sub-page、up-arrow、down-arrow和handle几个部分。 QScrollBar水平滚动条分为sub-line、add-line、add-page、sub-page、left-arrow、right-arrow和handle几个部分。 部件如下图所示&#xff1a; /* 整个滚动…

Pikachu靶场——文件包含漏洞(File Inclusion)

文章目录 1. File Inclusion1.2 File Inclusion(local)1.2.1 源代码分析1.2.2 漏洞防御 1.3 File Inclusion(remote)1.3.1 源代码分析1.3.2 漏洞防御 1.4 文件包含漏洞防御 1. File Inclusion 还可以参考我的另一篇文章&#xff1a;文件包含漏洞及漏洞复现。 File Inclusion(…

商业智能系统的主要功能包括数据仓库、数据ETL、数据统计输出、分析功能

ETL服务内容包含&#xff1a; 数据迁移数据合并数据同步数据交换数据联邦数据仓库

plt 画图不显示label

没写 plt.legend() 这个 ! # 效果模拟-------------- import matplotlib.pyplot as plt import matplotlib as mpl # matplotlib其实是不支持显示中文的 显示中文需要一行代码设置字体 mpl.rcParams[font.family] = STKAITI # STKAITI——字体 plt.rcParams[axes.unicode_m…

亲,您的假期余额已经严重不足了......

引言 大家好&#xff0c;我是亿元程序员&#xff0c;一位有着8年游戏行业经验的主程。 转眼八天长假已经接近尾声了&#xff0c;今天来总结一下大家的假期&#xff0c;聊一聊假期关于学习的看法&#xff0c;并预估一下大家节后大家上班时的样子。 1.放假前一天 即将迎来八天…

侯捷 C++ STL标准库和泛型编程 —— 9 STL周围

最后一篇&#xff0c;完结辽&#xff01;&#x1f60b; 9 STL周围 9.1 万用Hash Function Hash Function的常规写法&#xff1a;其中 hash_val 就是万用Hash Function class CustumerHash { public:size_t operator()(const Customer& c) const{ return hash_val(c.fna…

x64内核实验2-段机制的变化

x64内核实验2-段机制的变化 ia-32e模式简介 x86下的段描述符结构图如下 在x86环境下段描述符主要分为3个部分的内容&#xff1a;base、limit、attribute&#xff0c;而到了64位环境下段的限制越来越少&#xff0c;主要体现在base和limit已经不再使用而是直接置空&#xff0…

U盘里文件损坏无法打开怎么恢复?

U盘&#xff0c;全称为USB闪存盘&#xff0c;是一种体积小巧、传输数据速度快的便携式存储设备。由于其出色的便捷性和高效性&#xff0c;U盘在各个工作领域和日常生活中得到了广泛应用&#xff0c;赢得了消费者的普遍好评。然而&#xff0c;使用U盘的过程中也可能会面临数据损…

Zabbix配置监控文件系统可用空间小于30GB自动告警

一、创建监控项 二、配置监控项 #输入名称–>键值点击选择 #找到磁盘容量点击 注&#xff1a; 1、vfs 该键值用于检测磁盘剩余空间&#xff0c;zabbix 内置了非常多的键值可以选着使用 2、单位B不需要修改&#xff0c;后期图表中单位和G拼接起来就是GB 3、更新时间 10S…

Qt扫盲-QSqlTableModel理论总结

QSqlTableModel理论总结 一、概述二、使用1. 与 view 视图绑定2. 做中间层&#xff0c;不显示 三、常用函数 一、概述 QSqlTableModel是用于从单个表读写数据库记录的高级接口。它构建在较低级的QSqlQuery之上&#xff0c;可用于向QTableView 等视图类提供数据。这个主要是对单…

基于三平面映射的地形纹理化【Triplanar Mapping】

你可能遇到过这样的地形&#xff1a;悬崖陡峭的一侧的纹理拉伸得如此之大&#xff0c;以至于看起来不切实际。 也许你有一个程序化生成的世界&#xff0c;你无法对其进行 UV 展开和纹理处理。 推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 三平面映射&#xff08;Trip…

【C++】String -- 详解

⚪C语言中的字符串 C 语言中&#xff0c;字符串是以 \0 结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C 标准库中提供了一些 str 系列的库函数&#xff0c;但是这些库函数与字符串是分离开的&#xff0c;不太符合 OOP 的思想&#xff0c;而且底层空间需要用户自己…

Java实现整数互转罗马数字基本算法

目录 一、罗马数字的起源&#xff1f; 二、算法代码 &#xff08;1&#xff09;整数转罗马数字算法代码 &#xff08;2&#xff09;罗马数字转整数算法代码 三、测试结果 &#xff08;1&#xff09;整数转罗马数字测试结果 &#xff08;2&#xff09;罗马数字转整数测试…

GD32F103 硬件 IIC

1. 硬件IIC 1. 硬件IIC的框图 如果MCU做为主机SCL就做为输出&#xff0c;做从机SCL就做为输入。 主机&#xff1a; 当MCU作为主机发送数据流程从数据缓冲寄存器里拿到移位寄存器。在从移位寄存器一位一位发送。 当MCU作为主机接收数据流程先放到移位寄存器。在从移位寄存…