哈希结构(图文详解)【哈希表,哈希桶,位图,布隆过滤器】

2023-11-04

哈希结构

哈希概念

常见的K-V结构,实现了元素关键码与元素值的映射关系,但没有实现元素关键值与元素存储位置的映射关系,在遍历过程中,一般的顺序表或搜索二叉树要进行关键值的多次比较,其中顺序表的时间复杂度为O(n),二叉搜索树的时间复杂度O(lgn)

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

哈希函数

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

哈希操作

  • 插入元素

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

  • 搜索元素

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

  • 删除元素

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

  • 哈希冲突解决

  • 闭散列(线性探测法)

哈希表结构:在闭散列中,将哈希表底层结构设置为vector容器,容器中每一个元素为一个hashNode结构体,包括kv键值对和状态标志位

初始化将其hashNode的状态设置为空

template <class k,class v>
struct HashNode
{
	pair<k, v> kv;
	STATE state = EMPTY;
};

template <class k, class v>
class HashTable
{
public:
	typedef HashNode<k, v> Node;
	HashTable(size_t n = 10)
		:HSTable(n)
		,_size(0)
	{
	}
private:
	vector<Node> HSTable;
	size_t _size;
};

上述插入操作,可能会产生哈希冲突,通过线性探测法,从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止,插入一个新元素

在查找过程值,要想找到发生冲突元素,就必须给标志位

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

enum STATE{
	EXIST  //已存在
	,DELETE  //删除过
	,EMPTY   //空位置
};

通过定义一个枚举类型,包括已存在不可插入状态(EXIST),空位置可插入状态(EMPTY),删除过的位置状态(DELETE)

插入代码如下:


	bool insert(const pair<k, v>& kv)
	{
		checkcapacity();
		//计算hash位置
		int idx = kv.first%HSTable.size();
		//搜索
		while (HSTable[idx].state != EMPTY)//其余两种状态都需要继续查找
		{
			//当前位置已经有数据,且与要插入新数据相同,则插入失败
			if (HSTable[idx].state ==EXIST&& kv.first == HSTable[idx].kv.first)
				return false;
			++idx;
			if (idx == HSTable.size() - 1)//向后查找过程到结尾 从头再开始找空位
				idx = 0;
		}
		//此时找到空位置,插入新元素,状态置为已存在,hash表大小+1
		HSTable[idx].kv = kv;
		HSTable[idx].state = EXIST;
		_size++;
		return true;
		//插满扩容
		
	}

由于插入过程实际上是在vector容器基础上完成,如果需要插入元素足够多,可能导致当前所创造的哈希表插满,此时就需要检查容量,进行扩充操作

扩容判断条件是:负载因子>0.7,此时认为产生冲突的可能性大,需要进行扩容

扩容并非在原表基础上增大size,而是开辟新表,将原表已存在状态位置元素进行逐一存放,代码如下:

void checkcapacity()
	{
		//负载因子控制是否增容
		//负载因子越小,冲突越小,但空间浪费越大
		if (HSTable.size()==0&&_size * 10 / HSTable.size() > 7)
		{
			//开新表
			int newcap = HSTable.size() == 0 ? 10 : 2 * HSTable.size();
			HashTable<k, v> newHST(newcap);
			//直接拷贝会导致hash值计算前后不一致
			//需要重新计算hash位置
			for (int i = 0; i < HSTable.size(); i++)
			{
				if (HSTable[i].state == EXIST)
				{
					newHST.insert(HSTable[i].kv);
				}
			}
			swap(newHST);
		}
	}
	void swap(HashTable<k, v>& HT)
	{
		swap(HSTable,HT.HSTable);
		swap(_size, HT._size);
	}

查找:由于hash冲突,直接通过索引查找难以找到发生hash冲突而通过线性探测法存放的元素,非空位置逐一查找,如果索引超过了表大小,需要循环从头开始

Node* find(const k& key)
	{
		//计算hash位置
		int idx = key%HSTable.size();
		//搜索
		while (HSTable[idx].state != EMPTY)
		{
			if (HSTable[idx].state == EXIST && key == HSTable[idx].first)
				return &HSTable[idx];
			++idx;
			if (idx == HSTable.size() - 1)
				idx = 0;
		}
		return nullptr;
	}

删除:删除并非真删除,释放元素,而是改变对应元素位置状态信息

bool erase(const k& key)
	{
		Node* node = find(key);
		if (node)
		{
			node->state = DELETE;
			_size--;
			return true;
		}
		return false;
	}
  • 开散列(哈希桶)

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

bool insert(const V& val)
	{
		checkcapacity();
		KeyofValue kov;
		int idx = kov(val) % _hst.size();

		//查找
		Node* cur = _hst[idx];
		while (cur)
		{
			if (kov(cur->_val) == kov(val))
			{
				//key重复
				return false;
			}
			cur = cur->_next;
		}
		//插入  头插
		cur = new Node(val);
		cur->_next = _hst[idx];
		_hst[idx] = cur;
		_size++;
		return true;
	}

哈希桶通过建立新连接进行扩容操作:

void checkcapacity()
	{
		if (_size == _hst.size())
		{
			int newcap = _size == 0 ? 10 : 2 * _size;
			vector<Node*> newhst(newcap);
			KeyofValue kov;
			for (int i = 0; i < _hst.size(); i++)
			{
				Node* cur = _hst[i];
				while (cur)
				{
					Node* next = cur->_next;
					int idx = kov(cur->_val) % newhst.size();
					//新表头插
					cur->_next = newhst[idx];
					newhst[idx] = cur;

					cur = next;
				}
				//原表断开 置空
				_hst[i] = nullptr;
			}
			swap(_hst,newhst);
		}
	}
  • 迭代器操作:

迭代器的++步骤:

#include<iostream>
#include<vector>
using namespace std;

template <class V>
struct HashNode
{
	V _val;
	HashNode<V>* _next;

	HashNode(const V& val)
		:_val(val)
		,_next(nullptr)
	{

	}
};

//hash表的前置声明
template<class K, class V, class KeyofValue>
class HSTable;


//hash表迭代器  封装单链表节点
template<class K, class V, class KeyofValue>
struct HashIterator{
	typedef HashNode<V> Node;
	typedef HSTable<K, V, KeyofValue> ht;
	typedef HashIterator<K, V, KeyofValue> Self;
	//成员:节点指针,哈希表指针
	Node* _node;
	ht* _hptr;

	HashIterator(Node* node,ht* hptr)
		:_node(node)
		,_hptr(hptr)
	{

	}

	Self& operator++()
	{
		if (_node->_next)
		{
			_node = _node->_next;
		}
		//找下一个非空链表头结点
		else
		{
			//计算当前节点在hash表中位置
			KeyofValue kov;
			int idx = kov(_node->_val) % _hptr->_hst.size();
			//从下一位置开始查找
			++idx;
			for (int; idx < _hptr->_hst.size(); idx++)
			{
				if (_hptr->_hst[idx])
				{
					_node = _hptr->_hst[idx];//找到非空链表头结点给node记录
					break;
				}
			}
			if (idx = _hptr->_hst.size())//此时走到end位置  将node节点置为空
			{
				_node = nullptr;
			}
		}
		return *this;
	}
};



template<class K,class V,class KeyofValue>
class HSTable
{
public:
	typedef HashNode<V> Node;
	typedef HashIterator<K, V, KeyofValue> iterator;

	//由于迭代器需要访问其私有成员_hst  声明为友元类
	friend HashIterator<K, V, KeyofValue>;
	HSTable(int n=10)
		:_hst(n)
		,_size(0)
	{

	}

	iterator begin()
	{
		for (int i = 0; i < _hst.size(); i++)
		{
			if (_hst[i])
			{
				return iterator(_hst[i], this);
			}
			else
				return iterator(nullptr,this);

		}
	}
	iterator end()
	{
		return iterator(nullptr, this);
	}


private:
	vector<Node*> _hst;
	int _size;
};

哈希表的应用

  • 位图

位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。

比如Tencent的一道面试题:

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中

此题解法较多,一般容易想到的就是暴力求解,直接遍历查询,但海量数据导致时间复杂度过大,无法实际中应用;稍微进阶一些的会想到采用二分查找,虽然相比较于直接遍历,把时间复杂度从O(n)提升到O(lgn),但对于40亿数据还是难以实现

而通过位图可以实现:数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:

代码实现如下:

#include<iostream>
#include<vector>
using namespace std;

/*
位图应用:
(1)存放不重复数据简单信息,不需要存放数据本身   优点:节省空间,查找效率高

*/
class bitset
{
public:
	//位图内存大小与数据范围有关
	bitset(size_t range)
		:_bit(range/32+1)
	{

	}
	//存储信息
	void set(size_t num)
	{
		//计算整数位置
		int idx = num / 32;
		//计算比特位置
		int bitidx = num % 32;
		//对应比特位置1  按位或运算
		_bit[idx] |= (1 << bitidx);  //把1向左移动bixidx位置  或运算之后  将其置为1
	}
	//查找信息
	bool test(size_t num)
	{
		//计算整数位置
		int idx = num / 32;
		//计算比特位置
		int bitidx = num % 32;
		//对应比特位右移bitidx后 与1 与运算  如果为1 则为1  否则为0
		return (_bit[idx] >> bitidx)&1;		
	}

	//删除信息
	void reset(size_t num)
	{
		//计算整数位置
		int idx = num / 32;
		//计算比特位置
		int bitidx = num % 32;
		//对应比特位置10
		_bit[idx] &= ~(1 << bitidx);
	}
private:
	//数组
	vector<int> _bit;
	//默认bit位个数为 32 位
};
  • 布隆过滤器

布隆过滤器是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

布隆过滤器的实现依然需要借助位图的实现来完成,依然依靠0-1进行标记数据是否存在

注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。

  • 查找:

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。

  • 删除:

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

代码如下:

//布隆过滤器
// 假设布隆过滤器中元素类型为K,每个元素对应3个哈希函数
template<class T, class KToInt1, class KToInt2,class KToInt3>
class BloomFilter
{
public:
	BloomFilter(size_t size) // 布隆过滤器中元素个数
		: _bmp(3 * size), _bitcount(3 * size)
	{}

	//存储信息:使用多个bit位存储
	void set(const T& val)
	{
		KToInt1 k1;
		KToInt2 k2;
		KToInt3 k3;
		int idx1 = k1(val) % _bitcount;
		int idx2 = k2(val) % _bitcount;
		int idx3 = k3(val) % _bitcount;
		
		_bmp.set(idx1);
		_bmp.set(idx2);
		_bmp.set(idx3);
	}

	bool test(const T& val)
	{
		KToInt1 k1;
		KToInt2 k2;
		KToInt3 k3;
		int idx1 = k1(val) % _bitcount;
		int idx2 = k2(val) % _bitcount;
		int idx3 = k3(val) % _bitcount;

		if (!_bmp.test(idx1))
			return false;//三个有一个为0 肯定不存在  三个均为1  可能存在(不能说一定存在)
		if (!_bmp.test(idx2))
			return false;
		if (!_bmp.test(idx3))
			return false;
		if (_bmp.test(idx1) && _bmp.test(idx2) && _bmp.test(idx3))
			return true;
	}
private:
	bitset _bmp;
	size_t _bitcount; // bit位的个数
};

struct KToInt1
{
	//hash函数计算方式1  
};

struct KToInt2
{
	//hash函数计算方式2 
};

struct KToInt3
{
	//hash函数计算方式3 
};
void test()
{
	BloomFilter<string, KToInt1, KToInt2, KToInt3> bf(10);
}

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

哈希结构(图文详解)【哈希表,哈希桶,位图,布隆过滤器】 的相关文章

随机推荐

  • DALL·E 2 解读

    目录 一 导读 论文信息 CLIP 打通文本 图像模型 相关讲解 扩散模型Diffusion Model相关讲解 二 DALL E 2 模型解读 DALL E 2 模型总览 DALL E 2 训练过程 DALL E 2 推理过程 由文本生成
  • project 2007项目管理软件

    Microsoft Office Project 2007 项目管理软件 Microsoft Project 2003 2007是国际上最为盛行的基于网络的项目管理软件 在各类IT集成及开发项目 新产品研发 房地产项目 设计项目 工程建设项
  • Java性能调优笔记

    Java性能调优笔记 调优步骤 衡量系统现状 设定调优目标 寻找性能瓶颈 性能调优 衡量是否到达目标 如果未到达目标 需重新寻找性能瓶颈 性能调优结束 寻找性能瓶颈 性能瓶颈的表象 资源消耗过多 外部处理系统的性能不足 资源消耗不多但程序的
  • JSON中的key下划线与驼峰互转

    JSON中的key下划线与驼峰互转工具类 1 JSON中的key 下划线转驼峰 public final static Object underlineToHump String json Object obj JSON parse jso
  • WebGL射击游戏的优化

    myshmup com 允许在浏览器中创建 shmup 射击 游戏 我们可以使用具有创意通用许可证的资源或上传自己的艺术作品和声音 创建的游戏可以在网站上发布 该平台不需要编码 游戏对象的配置是在用户界面的帮助下执行的 后端是使用Djang
  • Spring MVC结果转换

    一 返回视图 ModelAndView 1 视图路径 默认在当前Control的路径下 表示项目部署的根目录 例如 new ModelAndView home jsp 返回的路径是 user home jsp new ModelAndVie
  • 实现一个最小的操作系统

    实现一个最小的操作系统 本实验在Vmware虚拟机的Linux环境下完成 准备工作 硬件 VMware下Linux虚拟机 Ubuntu 18 04 5 LTS 软件 汇编编译器NASM 软盘绝对扇区读写工具 dd命令 VMware的安装以及
  • 机器人基础原理1_2——机器人分类与常见坐标系

    机器人分类与常见坐标系 1 机器人的分类 1 按辈分 2 对应人的不同器官 3 按其构成机构 3 按驱动方式不同 4 按用途分类 2 常见的坐标系及对应的机器人结构 2 1 笛卡尔坐标系 2 2 圆柱坐标系 2 3 球坐标系 1 机器人的分
  • ThoughtWorks(中国)程序员读书雷达

    软件业的特点是变化 若要提高软件开发的技能 就必须跟上技术发展的步伐 埋首醉心于项目开发与实战 固然能够锤炼自己的开发技巧 却难免受限于经验与学识 世界上并不存在速成的终南捷径 但阅读好的技术书籍 尤其是阅读大师们的经典著作 总能收到事半功
  • Zabbix学习笔记(一)---Zabbix的安装

    目录 前言 一 Zabbix简介 二 下载与安装 1 CentOS 9安装 2 安装zabbix A 安装Zabbix包 B 安装Zabbix server 前端 Agent C 设置httpd D 安装数据库 总结 前言 近期学习网络运维
  • vue中使用高德地图实现历史轨迹回放并能控制播放轨迹的倍速

    如何在vue中引入高德地图在这里就不过多赘述 大家可以看这篇参考在vue中引入高德地图 说正事 使用高德地图实现轨迹回放 并能实现倍速控制 具体效果如图 核心代码 绘制小车 this marker new AMap Marker posit
  • ElasticSearch入门

    ElasticSearch概述 ElasticSearch 简称es es是一个开源的高扩展式全文检索引擎 它可以近乎实时的存储 检索数据 本身扩展性很好 可以扩展到上百台服务器 处理PB级别的数据 ElasticSearch安装 声明 j
  • Qt 学习之旅 ----可移动的无边框圆角窗口

    Qt 默认的窗口会有系统自带的边框 如图 但是在大多数情况下 系统自带的边框是不需要的 去掉边框很简单 在建立窗口时 加入如下一个函数 w setWindowFlags Qt FramelessWindowHint 这样 边框就被去掉了 但
  • win7安装计算机的更新,解决win7系统更新升级教程

    操作系统是一个复杂的程序 在使用过程中也需要不断的更新 修复漏洞 但是很多朋友都会将win7系统的自动更新关闭 我给大家带来了win7系统更新升级的小方法 大家可以参考一下 win7系统可以说是目前最易用的操作系统 它增加了一些小功能 如快
  • DevOps B站学习版(一)

    学习地址 01 DevOps的诞生 哔哩哔哩 bilibilihttps www bilibili com video BV1Pt4y1H7Zq p 1 vd source 1f09c23f556b3d6a9b7706f8db12fa54
  • 人类的行为与程序计算

    胡言乱语 引子 人类从出生伊始都在面临着生活中的种种问题 人类无时无刻不在进行着问题的解决过程 程序从设计之初也是用来解决生活中特定问题的 那么人类行为与程序计算理论之间又有什么相似性呢 人类 人类所面临的问题 人类解决问题的过程 人类解决
  • vue 按钮 路由

    APP vue 在已有的按钮上加上路由功能 这里的按钮和布局容器使用了 elementui 的但无关原理 按下按钮即可跳转页面
  • 雷达测高知识点总结

    1 激光和雷达的区别 雷达 radar radio detection and ranging 无线电探测和测距 雷达波段 雷达发射电波的频率范围 大多数雷达工作在超短波及微波波段 其频率范围在30 300000兆赫 相应波长为1mm 10
  • 利用PyTorch自己动手从零实现YOLOv3(详细注释)

    学习一个算法最好的方式就是自己尝试着去实现它 因此 在这片博文里面 我会为大家讲解如何用PyTorch从零开始实现一个YOLOv3目标检测模型 参考源码请在这里下载 模型实现总共会分为以下六部分 一 配置文件以及解析 二 搭建YOLO模型框
  • 哈希结构(图文详解)【哈希表,哈希桶,位图,布隆过滤器】

    哈希结构 哈希概念 常见的K V结构 实现了元素关键码与元素值的映射关系 但没有实现元素关键值与元素存储位置的映射关系 在遍历过程中 一般的顺序表或搜索二叉树要进行关键值的多次比较 其中顺序表的时间复杂度为O n 二叉搜索树的时间复杂度O