算法系列(十二)散列

概述

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代码也在不断完善中,有些地方可能有问题,还请多指教


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


  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
散列算法是一种将任意长度的输入转换为固定长度输出的算法。它的基本原理是通过对输入数据进行一系列的处理和变换,最终生成一个唯一的散列值。这个散列值是根据输入数据计算得出的,并且具有以下几个特点: 1. 唯一性:对于不同的输入数据,散列算法应该生成不同的散列值。 2. 固定长度:无论输入数据的长度是多少,散列算法都会生成固定长度的输出。 3. 不可逆性:根据散列值无法推导出原始输入数据的内容。 4. 散列值的变化:即使输入数据的细微变化,也会导致生成的散列值完全不同。 实现散列算法的方法有很多种,其中常见的一种是MD5(Message Digest Algorithm 5)算法。MD5算法是MD家族中最常用的一种加密算法。它基于一系列的位操作、位移操作和逻辑运算,对输入数据进行分组处理,并通过多次迭代和轮函数的运算来生成最终的散列值。 MD5算法的实现过程可以分为以下几个步骤: 1. 填充信息:将输入数据进行填充,使得数据长度满足算法要求。 2. 初始值设置:设置初始的散列值,即A、B、C、D。 3. 真正的计算:通过多次迭代和轮函数的运算,对输入数据进行处理,最终生成散列值。 散列算法的实现还可以通过其他算法,如SHA-1、SHA-256等。在实际应用中,散列算法常用于校验文件的完整性和存储用户密码等场景。通过对文件进行散列计算,可以判断文件是否被篡改;而存储用户密码时,通常将密码进行散列处理,并将散列值存储在数据库中,以增加密码的安全性。 在Java中,可以使用java.security.MessageDigest类来实现并使用散列算法。使用该类可以对输入数据进行散列计算,并生成散列值。具体的实现和使用方法可以参考Java文档和相关教程。 综上所述,散列算法的基本原理是将任意长度的输入转换为固定长度的输出,通过一系列的处理和变换来生成唯一的散列值。常见的实现方法包括MD5算法和SHA系列算法。使用Java的MessageDigest类可以方便地实现和使用散列算法

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值