算法系列(十二)散列

概述

Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

HASH主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做HASH值. 也可以说,hash就是找到一种数据内容和数据存放地址之间的映射关系。

散列表是一种用于以常数平均时间执行插入、删除、查找的算法。

散列函数

散列表每个关键字被映射到0到TableSize-1这个范围中的某个值,这个映射叫做散列函数。因为单元个数是有限的,两个关键字可能映射到同一个值,这个时候就需要通过一些方式来处理冲突。

以关键字为字符串为例,设计简单的散列函数。

通过将字符串的ASCII码值的和来计算hash值。

public static int hash1(String key, int tablesize) {
		int hashVal = 0;
		for (int i = 0; i < key.length(); i++) {
			hashVal += key.charAt(i);
		}
		return hashVal % tablesize;
	}

这种方法有个明显缺陷,就是hash值分配不均匀。

较好的散列方法

public static int hash2(String key, int tablesize) {
		int hashVal = 0;
		for (int i = 0; i < key.length(); i++) {
			hashVal = 37 * hashVal + key.charAt(i);
		}
		hashVal = hashVal % tablesize;
		if (hashVal < 0) {
			hashVal += tablesize;
		}
		return hashVal;
	}

分离链法解冲突

将散列到一个值的所有元素保留到一个表中。

思想:将散列到同一个值的元素保留到一个链表中。

缺点:给新链表单元分配地址空间需要花费时间,会降低算法的速度。

分析:一次不成功的查找需要访问的节点数平均为;成功的查找需要遍历大约1+/2个链。

简单实现

public class SeparateChainHashTable<T> {
	private static final int DEFAULT_SIZE = 100;
	protected LinkedList[] lists;

	public SeparateChainHashTable() {
		this(DEFAULT_SIZE);
	}

	public SeparateChainHashTable(int size) {
		lists = new LinkedList[PrimeNumber.nextPrime(size)];
		for (int i = 0; i < lists.length; i++) {
			lists[i] = new LinkedList<T>();
		}
	}

	public boolean contains(T x) {
		return lists[hashCode(x)].contains(x);
	}

	public void insert(T x) {
		LinkedList<T> linkedList = lists[hashCode(x)];
		if (!linkedList.contains(x)) {
			linkedList.add(x);
		}
	}

	public void remove(T x) {
		LinkedList<T> linkedList = lists[hashCode(x)];
		if (linkedList.contains(x)) {
			linkedList.remove(x);
		}
	}

	/**
	 * 计算hash值
	 *
	 * @param x
	 * @return
	 */
	private int hashCode(T x) {
		int hashVal = x.hashCode();
		hashVal = hashVal % lists.length;
		if (hashVal > 0) {
			hashVal += lists.length;
		}
		return hashVal;
	}
}

开放定址法

通常要求装填因子 < 0.5(为了1.保证线性时间界;2.保证能成功插入),否则需要rehash()

思想:解决冲突的另一个办法是当冲突发生时选择另一个单元进行判断,直到出现空单元。即执行,直到找到空单元,其中,f()是解决冲突的函数。这样的表也叫探测散列表(Probing hash tables)。

特点: 此种方法使用的tableSize比分离链接法要大(因为将冲突的元素分配到其他cell中);通常来说,此类方法的装填因子要低于0.5。

实现:

1)线性探测:采取的方式,容易产生一次聚集(primary clustering),产生一些滚雪球的堆块,导致散列到区块中的任何键值都需要多次试选单元才能解决冲突。一种解决方法是采用随机冲突解决方法(怎么查找?),即使得每次探测都与前次探测无关。可以看到,如果装填因子大于0.5时,线性探测不是一个好办法。

2)平方探测:采用的方式,问题在于,一旦装填因子大于0.5,就不能保证总会找到空的单元(如果表大小不是素数,那小于0.5时也可能找不到空单元)。平方探测会引起二次聚集(secondary clustering)效应,但其影响远小于一次聚集,可使用双散列的方法消除二次聚集,但同时也带来了额外的计算开销。

3)双散列(double hashing):一种流行的方式是,第二个散列函数的选择至关重要,一种选择是,R是小于TableSize的素数。实验证明,双散列预期探测次数几乎和随机冲突探测的相同,具有较好的表现。但由于需要两个散列函数,计算很耗时(尤其在string类型的key下)。所以实际中通常采用更加简便快捷的二次探测。

分析:在探测散列表中不能执行标准的删除操作(否则依赖当前cell的后续冲突元素将无法删除),需要采用懒惰删除,即在元素上附加数据成员表征其状态(ACTIVE/EMPTY/DELETED)。

再散列(reHash)

使用平方探测的开放定址法时,如果表中元素太多,那么操作的时间会增加,而且插入操作有可能失败,此时需要对哈希表的大小进行扩张(两倍),使用一个新散列函数扫描原来的散列表,计算每个元素的新散列值。

再散列通常有多种实现方式:

1)当表满到一半时;

2)遇到插入失败时;

3)当表到达一个装填因子时(途中策略)。

通常选取一个好的截止点来采用第三种方案。

可扩散列

用于内存不可一次载入的大数据量操作。类比B-树的实现:根D存在主存中,对磁盘内容进行索引。关键问题在于如何降低分支系数和如何进行树结构的分裂和扩展。具体分析不再详述。

需要注意一点是,如果M(每片树叶最多能存储的元素数目)过小的话,可能会导致目录过大,此时需要做一个二级索引(变相增加了M的大小),但存在潜在的无法避免的二次磁盘访问(如果主存不足以装下二级索引)。

标准库中的三列表

标准库中包括Set和Map的散列实现,即HashSet类和HashMap类。HashSet的实现直接借助了HashMap。JDK中是使用分离链接散列实现的。

代码实现可以看github,地址https://github.com/robertjc/simplealgorithm

github代码也在不断完善中,有些地方可能有问题,还请多指教

欢迎扫描二维码,关注公众号

时间: 2024-10-07 05:56:04

算法系列(十二)散列的相关文章

[算法系列之二十四]后缀树(Suffix Tree)

之前有篇文章([算法系列之二十]字典树(Trie))我们详细的介绍了字典树.有了这些基础我们就能更好的理解后缀树了. 一 引言 模式匹配问题 给定一个文本text[0-n-1], 和一个模式串 pattern[0-m-1],写一个函数 search(char pattern[], char text[]), 打印出pattern在text中出现的所有位置(n > m). 这个问题已经有两个经典的算法:KMP算法 ,有限自动机,前者是对模式串pattern做预处理,后者是对待查证文本text做预处

Silverlight &amp; Blend动画设计系列十二:三角函数(Trigonometry)动画之自由旋转(Free-form rotation)

原文:Silverlight & Blend动画设计系列十二:三角函数(Trigonometry)动画之自由旋转(Free-form rotation) 说到对象的旋转,或许就会联想到对象角度的概念.对象的旋转实现实际上就是利用对象的角度改变来实现的位置变换,在<Silverlight & Blend动画设计系列二:旋转动画(RotateTransform)>一文中有对对象的不同角度变换的实现介绍,本篇要介绍的自由旋转(Free-form rotation)将借助<Fun

算法系列之二十三:离散傅立叶变换之音频播放与频谱显示

算法系列之二十三:离散傅立叶变换之音频播放与频谱显示 算法系列之二十三离散傅立叶变换之音频播放与频谱显示 导语 什么是频谱 1 频谱的原理 2 频谱的选择 3 频谱的计算 显示动态频谱 1 实现方法 2 杂项说明 结果展示 导语 频谱和均衡器,几乎是媒体播放程序的必备物件,没有这两个功能的媒体播放程序会被认为不够专业,现在主流的播放器都具备这两个功能,foobar 2000的十八段均衡器就曾经让很多人着迷.在上一篇对离散傅立叶变换介绍的基础上,本篇就进一步介绍一下频谱是怎么回事儿,下一篇继续介绍

每日算法之四十二:Permutation Sequence (顺序排列第k个序列)

The set [1,2,3,-,n] contains a total of n! unique permutations. By listing and labeling all of the permutations in order, We get the following sequence (ie, for n = 3): "123" "132" "213" "231" "312" "

struts2官方 中文教程 系列十二:控制标签

介绍 struts2有一些控制语句的标签,本教程中我们将讨论如何使用 if 和iterator 标签.更多的控制标签可以参见 tags reference. 到此我们新建一个struts2 web 项目:struts_basic2 本帖地址:struts2官方 中文教程 系列十二:控制标签 即 http://www.cnblogs.com/linghaoxinpian/p/6941683.html 下载本章节代码 struts2 if标签 我们在thankyou.jsp中添加如下代码: <s:i

SQL Server 2008空间数据应用系列十二:Bing Maps中呈现GeoRSS订阅的空间数据

原文:SQL Server 2008空间数据应用系列十二:Bing Maps中呈现GeoRSS订阅的空间数据 友情提示,您阅读本篇博文的先决条件如下: 1.本文示例基于Microsoft SQL Server 2008 R2调测. 2.具备 Transact-SQL 编程经验和使用 SQL Server Management Studio 的经验. 3.熟悉或了解Microsoft SQL Server 2008中的空间数据类型. 4.具备相应(比如OGC规范.KML规范)的GIS专业理论知识.

Exchange Server 2013系列十二:邮箱的基本管理

杜飞 邮箱是 Exchange 组织中信息工作人员最常用的收件人类型.每个邮箱都与一个 Active Directory 用户帐户关联.用户可以使用邮箱发送和接收邮件,并可以存储邮件.约会.任务.便笺和文档.邮箱是 Exchange 组织中用户的主要邮件传递和协作工具.每个邮箱由 Active Directory 用户以及存储在 Exchange 邮箱数据库中的邮箱数据组成(如下图所示).邮箱的所有配置数据都存储在 Exchange 用户对象的 Active Directory 属性中.邮箱数据

[算法系列之二十]字典树(Trie)

一 概述 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计. 二 优点 利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高. 三 性质 (1)根节点不包含字符,除根节点外每一个节点都只包含一个字符: (2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串: (3)每个节点的所有子节点包含的字符都不相同. 单词列表为"apps&

[算法系列之二十六]字符串匹配之KMP算法

一 简介 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特-莫里斯-普拉特操作(简称KMP算法).KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的. 二 基于部分匹配表的KMP算法 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含搜索串"ABCDABD"? 步骤1:字符串"BBC ABC

算法系列之二十四:离散傅立叶变换之音频播放与均衡器

导语 在算法系列的第二十二篇,我们介绍了离散傅立叶变换算法的实现,将时域的音频信号转换到频域进行分析,获取拨号音频的频率特征.这一篇我们将介绍一种频域均衡器的实现方法,所谓的频域均衡器,就是在频域信号的基础上对音频数据进行调整,然后再将频域信号转换成时域信号在回放设备上播放,从而达到音色调节的目的.将频域信号转换成时域信号的算法,就是离散傅立叶逆变换算法. 1 离散傅立叶逆变换 有从时域转换到频域的方法,就必然有从频域转换到时域的方法,相对于离散傅里叶变换,这个反向转换就是离散傅里叶逆变换(ID