C++进阶 —— 哈希

2023-05-16

目录

一,哈希的介绍

哈希的概念

哈希冲突

哈希函数

二,哈希冲突解决

闭散列

开散列

开散列与闭散列比较


        在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,查询效率可达到logN,即最差情况下需要比较红黑树的高度次,当树节点非常多时,查询效率也不理想;最好的查询是,进行很少的比较次数就能够将元素找到,因此C++11,STL又提供了4个unordered系列的关联式容器(unordered_set/unordered_map、unordered_multiset/unordered_multimap),这4个容器与红黑树结构的关联式容器使用方式基本类似,只是结构不同;

一,哈希的介绍

unordered系列关联式容器之所以效率高,是因为其底层使用了哈希结构;

哈希的概念

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

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

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

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

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

该方法进行搜索不必进行多次元素比较,因此搜索速度较快;

哈希冲突

对于两个元素的关键码k_{i}k_{j},当hash(k_{i})==hash(k_{j}),即不同关键码通过相同哈希函数计算出相同的哈希地址,称为哈希冲突或哈希碰撞;

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”;

哈希函数

 引发哈希冲突的原因可能是,哈希函数设计不够合理;

哈希函数设计原则

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

常见哈希函数

  • 直接定制法(常用)
    • 取关键码的某个线性函数作为散列地址,Hash(Key)=A*Key+B;
    • 适合查找比较小且连续的情况;
    • 优点,简单、均匀;
    • 缺点,需要事先知道关键码的分布情况;
  • 除留余数法(常用)
    • 假设散列表中允许的地址数为m,取一个不大于m,但最接近或等于m的质子p作为除数,按照哈希函数Hash(Key)=Key%p(p<m),将关键码转换成哈希地址;
  • 平方取中法(了解)
    • 假设关键码为1234,对其平方就是1522756,抽取中间3位227作为哈希地址;
    • 在如关键码为4321,对其平方就是18671041,抽取中间3为671(或710)作为哈希地址;
    • 比较适合不知道关键码的分布,而位数又不是很大的情况;
  • 折叠法(了解)
    • 是将关键码从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表长,取后几位作为散列表地址;
    • 适合事先不知道关键码的分布,适合关键码位数比较多的情况;
  • 随机数法(了解)
    • 选择一个随机函数,取关键码的随机函数值为它的哈希地址,Hash(Key)=random(Key);
    • 通常应用于关键码长度不等时常用此法;
  • 数学分析法(了解)
    • 设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能 在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址;
    • 适合处理关键码位数比较大的情况,如事先折叠关键码的分布且关键码的若干位分布较均匀的情况;

注:哈希函数设计的越精妙,哈希冲突就越低,但无法避免哈希冲突;

二,哈希冲突解决

解决哈希冲突常见方法,闭散列、开散列;

闭散列

也叫开放定址法,当发生哈希冲突时,如哈希表未被装满,说明在哈希表中必然还有空位置,那么可把key存放到冲突位置中的“下一个”空位置中去,寻找“下一个”空位置方法有,线性探测和二次探测;

线性探测

如上述案例中,在插入元素44,计算的哈希地址为4,但该位置已存放了元素,发生了哈希冲突;线性探测,从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止;

  • 插入,通过哈希函数获取插入哈希表中的位置,如该位置没有元素直接插入,如该位置已有元素使用线性探测找到下一个空位置,插入新元素;
  • 删除,采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有元素,若直接删除会影响其他元素的搜索;因此线性探测采用标记的伪删除法来删除元素;
//哈希表每个空间给个标记
//EMPTY为空,EXISTY已有元素,DELETE元素已删除
enum State{EMPTY, EXIST, DELETE}

优点:实现非常简单;

缺点:一旦发生哈希冲突,所有的冲突连在一起,容易发生数据堆积,即不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致效率低下;

线性探测的实现

template<class K, class V>
class HashTable
{
	struct Elem
	{
		pair<K, V> _val;
		State _state;
	};

public:
	HashTable(size_t capacity = 3)
		:_ht(capacity, _size(0))
	{
		for (size_t i = 0; i < capacity; ++i)
		{
			_ht[i]._state = EMPTY;
		}
	}

	bool Insert(const pair<K, V>& val)
	{
		size_t hashAddr = HashFunc(key);
		while (_ht[hashAddr] != EMPTY)
		{
			if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)
				return false;
			hashAddr++;
			if (hashAddr == _ht.capacty())
				hashAddr = 0;
		}
		_ht[hashAddr]._state = EXIST;
		_ht[hashAddr]._val = val;
		_size++;
		return true;
	}

	int Find(const K& key)
	{
		size_t hashAddr = HashFunc(key);
		while (_ht[hashAddr]._state != EMPTY)
		{
			if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val._first == key)
				return hashAddr;
			hashAddr++;
		}
		return hashAddr;
	}

	bool Erase(const K& key)
	{
		int index = Find(key);
		if (index != -1)
		{
			_ht[index]._state = DELETE;
			_size--;
			return true;
		}
		return false;
	}
private:
	size_t HashFunc(const K& key)
		return key % _ht.capacity();
private:
	vector<Elem> _ht;
	size_t _size;
};

哈希表扩容

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

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

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

void CheckCapacity()
{
	if (_size * 10 / _ht.capacity() >= 7)
	{
		HashTable(K, V, HF) newHt(GetNextPrime(ht.capacity));
		for (size_t i = 0; i < _ht.capacity(); ++i)
		{
			if (_ht[i]._state == EXIST)
				newHt.Insert(_ht[i]._val);
		}
		Swap(newHt);
	}
}

二次探测

线性探测的缺陷时产生冲突的数据堆积在一块,这与其找下一个位置有关系,因为找空位置的方式就是挨个往后逐个查找的;二次探测就是为了避免该问题的,找下一个空位置的方法为,H_{i}=(H_{0}+i^{2})%m 或 H_{i}=(H_{0}-i^{2})%m(i=1,2,3...,H_{0}是通过散列函数Hash(X)对元素的关键码key进行计算得到的位置,m表示表的大小);

研究表明,当表长度为质数且表转载因子\alpha不超过0.5时,新表项一定能够插入,而且任何一个位置都不会被探查两次,因此只要表中有一半的位置,就不会存在表满的问题,在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子\alpha不超过0.5,如超过需考虑增容;

 因此,闭散列最大的缺陷就是空间利润率较低,这也是哈希的缺陷;

开散列

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

在开散列中,每个桶中放的都是发生哈希冲突的元素;

开散列的实现

template<class V>
struct HashBucketNode
{
	HashBucketNode(const V& data)
		:_pNext(nullptr)
		,_data(data)
	{}

	HashBucketNode<V>* _pNext;
	V _data;
};

template<class V>
class HashBucket
{
	typedef HashBucketNode<V> Node;
	typedef Node* pNode;
public:
	HashBucket(size_t capacity = 3)
		:_size(0)
	{
		_ht.resize(GetNextPrime(capacity), nullptr);
	}

	pNode* Insert(const V& data)
	{
		size_t bucketNo = HashFunc(data);
		pNode pCur = _ht[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return pCur;
			pCur = pCur->_pNext;
		}
		pCur = new Node(data);
		pCur->_pNext = _ht[bucketNo];
		_ht[bucketNo] = pCur;
		_size++;
		return pCur;
	}

	pNode* Erase(const V& data)
	{
		size_t bucketNo = HashFunc(data);
		pNode pCur = _ht[bucketNo];
		pNode pPrev = nullptr;
		pNode pRet = nullptr;
		while (pCur)
		{
			if (pCur->_data == data)
			{
				if (pCur == _ht[bucketNo])
					_ht[bucketNo] = pCur->_pNext;
				else
					pPrev->_pNext = pCur->_pNext;
				pRet = pCur->_pNext;
				delete pCur;
				_size--;
				return pRet;
			}
		}
		return nullptr;
	}

	pNode* Find(const V& data);
	size_t Size()const;
	bool Empty()const;
	void Clear();
	bool BucketCount()const;
	void Swap(HashBucket<V, HF>& ht);
	~HashBucket();
private:
	size_t HashFunc(const V& data)
	{
		return data % _ht.capacity();
	}
private:
	vector<pNode*> _ht;
	size_t _size;
};

开散列增容

桶的个数是一定的,随着元素的不断插入,每个桶中的元素个数会不断增加,可能会导致一个桶中链表节点非常多,进而影响哈希表的性能,因此在一定条件下需要对哈希表进行增容;开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此在元素个数刚好等于桶个数时,可给哈希表增容;

void _CheckCapacity()
{
	size_t bucketCount = BucketCount();
	if (_size == bucketCount)
	{
		HashBucket<V, HF> newHt(bucketCount);
		for (size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx)
		{
			pNode pCur = _ht[bucketIdx];
			while (pCur)
			{
				_ht[bucketIdx] = pCur->_pNext;
				size_t bucketNo = newHt.HashFunc(pCur->_data);
				pCur->_pNext = newHt._ht[bucketNo];
				newHt._ht[bucketNo] = pCur;
				pCur = _ht[bucketIdx];
			}
		}
		newHt._size = _size;
		this->Swap(newHt);
	}
}

开散列的思考

只能存储key为整数的元素,其他类型怎么解决?

除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?

开散列与闭散列比较

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

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

C++进阶 —— 哈希 的相关文章

  • Springboot+Netty搭建TCP客户端-多客户端

    之前搭建了一个Springboot 43 Netty服务端的应用 xff0c 既然有服务端 xff0c 自然也有客户端的应用 xff0c 现在搭建一个Springboot 43 Netty客户端的应用Demo程序 xff0c 多客户端方式
  • 机器学习中的凸和非凸优化问题

    题目 xff08 145 xff09 xff1a 机器学习中的优化问题 xff0c 哪些是凸优化问题 xff0c 哪些是非凸优化问题 xff1f 请各举一个例子 凸优化定义 凸优化问题 非凸优化问题 凸优化定义 xff1a 公式 geome
  • VMware workstation中rhel安装VMware tools失败

    切换登录用户为root即可 转载于 https www cnblogs com dazzleC p 10555809 html
  • Uniform convergence may be unable to explain generalization in deep learning

    本文价值 xff1a understand the limitations of u c based bounds cast doubt on the power of u c bounds to fully explain general
  • 调参之learning rate

    The learning rate is perhaps the most important hyperparameter If you have time to tune only one hyperparameter tune the
  • 调超参(lr,regularization parameter)经验整理

    Learning rate 最优值从1e 4到1e 1的数量级都碰到过 xff0c 原则大概是越简单的模型的learning rate可以越大一些 https blog csdn net weixin 44070747 article de
  • Dropout network, DropConnect network

    Notations input v v v output r r r weight parameter
  • Curriculum adversarial training

    Weakness of adversarial training overfit to the attack in use and hence does not generalize to test data Curriculum adve
  • Python处理中文语言——读取中文

    本文解决问题 xff1a 1 导入中文txt文本 xff0c 并转换为unicode 2 导入包含中文的py file 解决问题一 xff1a 导入中文txt文本 xff0c 并转换为unicode 基础概念 xff1a 1 unicode
  • C# WPF开源控件库HandyControl用法举例

    目录 概述 MessageBox用法举例 Button用法举例 Lable用法举例 Slider用法举例 TextBox用法举例 组合框ComboBox用法举例 源码下载 概述 HandyControl是一款免费开源的WPF控件库 xff0
  • python 等差数列生成器

    典型的迭代器模式作用很简单 遍历数据结构 不过 xff0c 即便不是从集合中获取元素 xff0c 而 是获取序列中即时生成的下一个值时 xff0c 也用得到这种基于方法的标准接口 例如 xff0c 内置的 range 函数用于生成有穷整数等
  • python 终止协程和异常处理

    协程中未处理的异常会向上冒泡 xff0c 传给 next 函数或 send 方法的调用方 xff08 即触发协程的对 象 xff09 下面示例举例说明如何使用之前博客示例中由装饰器定义的 averager 协程 未处理的异常会导致协程终止
  • centos7 下安装 nodejs

    源码包安装 下载安装包到 xff1a usr local 目录下 1 命令下载 wget https span class token punctuation span span class token operator span node
  • Ubuntu配置apt软件源

    清华大学开源镜像网站 xff08 帮助页面 xff09 https mirrors tuna tsinghua edu cn help AOSP 阿里云开源镜像网站 https opsx alibaba com mirror 网易开源镜像网
  • python3 fnmatch和fnmatchcase

    你想使用 Unix Shell 中常用的通配符 比如 py Dat 0 9 csv 等 去匹配文本字符串 xff0c fnmatch 模块提供了两个函数 fnmatch 和 fnmatchcase xff0c 可以用来实现这样的匹配 用法如
  • python unicodedata 处理Unicode 字符串

    你正在处理 Unicode 字符串 xff0c 需要确保所有字符串在底层有相同的表示 span class token comment coding utf 8 span span class token comment 你正在处理 Uni
  • python 插入排序

    问题 xff1a 数组排序 插入排序 xff0c 向已经有序一组序列中 xff0c 插入一个新的元素 默认第一个列表元素为已经排序好的元素 xff0c 从第二个元素进行比较 xff0c 已经排序好的元素 xff0c 重大到小 xff0c 依
  • 分治策略-归并排序

    问题 xff1a 数组排序 分治策略 归并排序 xff1a 1 是合并这些子问题的解 2 分解原问题 xff0c 递归求解 span class token comment coding utf 8 span span class toke
  • 求股票最大收益问题

    问题 xff1a 求股票最大收益 xff0c 股票每天的价格 xff1a 100 113 110 85 105 102 86 63 81 101 94 106 101 79 94 90 97 买进和卖出都在当天结束后进行 xff0c 在某一
  • Python pip 包的安装和卸载 使用。

    Python pip 包的安装和卸载 使用 xff08 一 xff09 pip 安装 一般 来说 Python 需要什么包 直接 pip install 包 即可 但是 这种方法太慢 因为他通过美国的服务器下载 提高 pip 速度 这里提供

随机推荐

  • jdk1.8安装和环境变量配置

    一 安装JDK 选择安装目录 安装过程中会出现两次 安装提示 第一次是安装 jdk xff0c 第二次是安装 jre 建议两个都安装在同一个java文件夹中的不同文件夹中 xff08 不能都安装在java文件夹的根目录下 xff0c jdk
  • python 读取PDF(tabula和pdfminer和pdfplumber的简单操作)

    一 pdfminer 读取PDF 官方文档 xff1a http www unixuser org euske python pdfminer 这里针对python3 1 模块安装 xff1a pip install i https pyp
  • 一区即将要洗的DVD片子

    101 Dalmatians Animated 2009 SE 101斑点狗 预计2009年发行特别版 12 Monkeys 05 10 2005 COM DOC 12只猴子 预计2005年5月10日发行扩展版 加评论和记录片等 2001
  • UML — 五大关系

    在UML教学视频中 xff0c 关系有四种 xff0c 而课本中有五种 xff0c 其实就是多加了一种 xff0c 那么下面我一并总结出来 1 关联关系 通俗点说就是关联关系就是两个对象他们之间的联系和关系 关联分两种 xff1a xff0
  • rhel6.5救援模式修复系统

    如果系统中很多重要的部分被删除了例如 boot下的所有东西 xff0c 则可以通过救援模式 root 64 dazzle1 桌面 mkdir backup root 64 dazzle1 桌面 cp etc fstab backup fst
  • 利用nvm安装npm失败的解决办法

    最近发现在安装nodejs后 xff0c 想使用npm发现自己的电脑上没有安装npm xff0c 可是网上都说安装了nodejs后会自动安装npm xff0c 找了很久解决办法发现没有合适的解决办法 xff0c 于是自己尝试了很久发现了问题
  • chrome 浏览器的缩略图怎么没有了?就是浏览过网页的缩略图,一点击就能打开网站。

    这个问题 xff0c 突然今天解决了 哈哈 分享 首先新标签页 点击左下角 最常访问的网站 点击 最常访问的网站 紧接着再点击被置顶端的 最常访问的网站 Ok xff0c 大功告成了 烦恼了几天的这个小功能 xff0c 有缩略图还是看着舒服
  • 史上最详细的PID教程——理解PID原理及优化算法

    Matlab动态PID仿真及PID知识梳理 云社区 华为云 huaweicloud com 位置式PID与增量式PID区别浅析 Z小旋 CSDN博客 增量式pid https zhuanlan zhihu com p 38337248 期望
  • ubuntu 20.04搭建samba文件共享服务器,实现基于Linux和Windows的共享文件服务

    ubuntu 20 04搭建samba文件共享服务器 xff0c 实现基于Linux和Windows的共享文件服务 超详细 一 xff0c samba的基本概念二 xff0c samba的安装三 xff0c samba的基本配置创建文件夹更
  • ERROR: Could not find a version that satisfies the requirement torchvision

    打docker时出错 xff0c ERROR Could not find a version that satisfies the requirement torchvision from versions 0 1 6 0 1 7 0 1
  • openstack 常用命令回顾及总结

    1 概述 命令实际执行基于OpenStack Queens版本 xff0c 更高版本亦可 xff0c 长时间未使用openstack有些遗忘 xff0c 整理后方便自己回顾学习 xff0c 仅供各位参考 xff0c 详细命令及参数可以参考o
  • TPMS方案 传感器 infineon篇 (SP35 SP37)

    TPMS方案 xff08 SP35 SP37 xff09 传感器 infineon篇 关于sp37无压力芯片目前已有方案 关于sp35传感器已经稳定出货 xff0c 欢迎咨询 硬件原理图 软件说明 xff1a 协议 调制方式 FSK 频率
  • sudo rosdep init 出现 ERROR: cannot download default sources list from:

    sudo rosdep init 出现 ERROR cannot download default sources list from 针对目前安装ROS出现一下指令的错误 span class token function sudo sp
  • 新装linux主机可以ping通,但是SSH无法登陆

    0 xff0c 新装一台linux主机 xff0c 可是ssh连接不上 xff0c 能ping通 怎么办呢 xff1f 1 xff0c 先查看一下防火墙状态 sudo ufw status 2 xff0c 关闭防火墙 sudo ufw di
  • tcp头以及ip头

    转自http www cnblogs com zzq919101 p 7866550 html 在网上找了很多有关tcp ip头部解析的资料 xff0c 都是类似于下面的结构 抽象出图文是这种结构 xff0c 但是在底层中数据到底是怎么传输
  • C++初阶 —— 入门/概念

    目录 一 xff0c 关键字 xff08 C 43 43 98 xff09 二 xff0c 命名空间 命名空间定义 命名空间使用 三 xff0c C 43 43 输入 输出 四 xff0c 缺省参数 五 xff0c 函数重载 六 xff0c
  • C++初阶 —— list类

    目录 一 xff0c list介绍 二 xff0c list的使用 构造函数 list iterator的使用 list capacity list element access list modifiers list迭代器失效 三 xff
  • C++初阶 —— stack/queue

    目录 一 xff0c 容器适配器 deque双端队列 二 xff0c stack栈 stack接口 stack模拟实现 三 xff0c queue队列 queue接口 queue模拟实现 四 xff0c priority queue优先级队
  • C++初阶 —— 模板进阶

    目录 一 xff0c 非类型模板参数 模板参数分类 二 xff0c 模板特化 函数模板特化 类模板特化 三 xff0c 模板分离编译 分离编译 链接失败原因 解决方法 附 模板优点 模板缺点 一 xff0c 非类型模板参数 模板参数分类 类
  • C++进阶 —— 哈希

    目录 一 xff0c 哈希的介绍 哈希的概念 哈希冲突 哈希函数 二 xff0c 哈希冲突解决 闭散列 开散列 开散列与闭散列比较 在C 43 43 98中 xff0c STL提供了底层为红黑树结构的一系列关联式容器 xff0c 查询效率可