算法——查找之散列表

本文介绍了散列表作为快速查找数据结构的原理,包括散列函数的一致性、易计算性和分布均匀性,以及解决冲突的拉链法和线性探测法。拉链法通过链表解决冲突,保持高效查找;线性探测法则利用数组空位解决冲突,但可能需要重新插入键。散列表提供O(1)的查找速度,但不适用于需要排序的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面我们了解二叉树查找,虽然二叉树的查找已经很快了,但是仍然需要O(logn)的时间复杂度。我们能不能更快呢?我们就想到了一个方法,如果我们能够把所有查找的键转化为一个唯一的数字,那么我们就可以采用数组的形式来存放键和值,那么存取的速度就会非常快了,时间复杂度是O(1)。

如果没有内存限制,我们可以做一个超大的数组来存放,那么所有查找都只需要一次访问就可以得到了。

但是显然,我们并没有这么大的内存来存放数组。所以我们只能选择一个折中的办法,就是采用散列表的形式。


散列表的实现需要解决以下两个问题:

1.散列函数

2.冲突解决


散列函数

散列函数需要满足什么条件呢?

1.一致性

2.易于计算

3.分布均匀


一致性

对于同一个键,使用散列函数计算得到的数组下标,每次计算都应该是一致的。

易于计算

使用散列函数计算数组的下标应该是高效的,否则就失去了散列表的特性。

分布均匀

使用散列表计算多个键得到的下标应该分布均匀,否则也会失去散列表的高效性。


因为键可能是各种各样类型的键,考虑到分布均匀的特征,我们应该将整个键映射成一个下标,而不是只使用键的一部分。例如:Date类型的键,应该充分考虑到day,month,year所有的因素。

幸运的是,对于java,它提供了hashCode()函数,对于大部分的数据类型,我们不必操心他们的散列函数。当然,对于我们自定义的数据类型,hashCode方法是需要重写的。


冲突解决

散列表允许冲突。因为我们并没有一个很大很大的数组,所以一个数组是存不下所有的键值的。所以就会出现多个键,映射到同一个数组下标中,这就造成了冲突。冲突的解决是一个重要的问题。

这里讨论两种冲突解决的办法:1.拉链法 2.线性探测法


拉链法

拉链法是将大小为M的数组中的每个元素指向一条链表,链表中每个节点都存储了散列值为当前下标的键值对。

这样,解决了冲突发生的时候,冲突的键值对存放的位置的问题。

因为发生冲突的元素都被存储在链表当中,这个方法的基本思想就是选择足够大的M,使得所有链表都尽可能的短,就能保证高效的查找了。


实现如下:

public class SeparateChainHashST {
	
	private final static int DEFAULT_SIZE = 5;

	private int M; // 数组大小
	private BSTSearch[] array;
	
	public SeparateChainHashST() {
		this(DEFAULT_SIZE);
	}
	
	public SeparateChainHashST(int M) {
		this.M = M;
		array = new BSTSearch[M];
		for (int i = 0; i < M; i++) {
			array[i] = new BSTSearch();
		}
	}
	
	private int hash(Comparable key) {
		return (key.hashCode() & 0x7fffffff) % M;
	}
	
	public String get(Comparable key) {
		return array[hash(key)].search(key);
	}
	
	public void put(Comparable key, String value) {
		array[hash(key)].put(key, value);
	}
	
	public void delete(Comparable key) {
		array[hash(key)].delete(key);
	}

}
这里我们内部的链表使用之前写过的二叉树,这样我们的代码就可以复用了。对于使用二叉树作为链表还是单链表,这个问题其实并不重要,因为数组的大小应该是可变的,这样我们数组中的链表就不应该很长,使用二叉树还是单链表应该是差不多的。这里只是因为可以复用之前的代码比较方便而已。

值得留意的是,散列函数我们不能直接使用hashCode()函数,因为hashCode()返回值可能是负的,取余就是负的了。


线性探测法

使用大小为M的数组来保存N个键值对,其中M>N。我们需要使用数组中的空位来解决冲突,这一类方法成为开放地址散列表。

开放地址散列表中最简单的方法就是线性探测法。当发生冲突时(键的hash值已经被另一个不同的键占用了),我们直接检查散列表中的下一个位置。

这样就可能出现以下三种情况:

1.命中,键和被查找的键相同 2.未命中,该位置没有键 3.该位置的键和查找的键不同,需要继续查找


实现:

public class LinearProbeHashST {
	
	private static final int DEFAULT_SIZE = 16;
	
	private int N; // 当前键数目
	private int M; // 数组大小
	
	private Comparable[] keys;
	private String[] values;
	
	public LinearProbeHashST() {
		this(DEFAULT_SIZE);
	}
	
	public LinearProbeHashST(int M) {
		this.M = M;
		N = 0;
		keys = new Comparable[M];
		values = new String[M];
	}
	
	private int hash(Comparable key) {
		return (key.hashCode() & 0x0fffffff) % M;
	}
	
	public void put(Comparable key, String value) {
		int keyPos = hash(key);
		while (keys[keyPos] != null) {
			if (keys[keyPos].equals(key)) {
				values[keyPos] = value;
				return;
			}
			keyPos = (keyPos + 1)%M;
		}
		keys[keyPos] = key;
		values[keyPos] = value;
		N++;
		if (N > 0 && N >= M/2) resize(M*2);
	}
	
	public String get(Comparable key) {
		int keyPos = hash(key);
		while (keys[keyPos] != null) {
			if (keys[keyPos].equals(key)) {
				return values[keyPos];
			}
			keyPos = (keyPos + 1) % M;
		}
		return null;
	}
	
	public void delete(Comparable key) {
		int keyPos = hash(key);
		while (keys[keyPos] != null) {
			if (keys[keyPos].equals(key)) {
				keys[keyPos] = null;
				values[keyPos] = null;
				N--;
				break;
			}
			keyPos = (keyPos + 1) % M;
		}
		int i = (keyPos + 1) % M;
		while (keys[i] != null) {
			Comparable tempKey = keys[i];
			String tempValue = values[i];
			keys[i] = null;
			values[i] = null;
			N--;
			put(tempKey, tempValue);
			
			i = (i + 1) % M;
		}
		
		if (N > 0 && N <= M/8) resize(M/2);
	}
	
	
	private void resize(int size) {
		LinearProbeHashST t = new LinearProbeHashST(size);
		for (int i = 0; i < M; i++) {
			if (keys[i] != null) {
				t.put(keys[i], values[i]);
			}
		}
		keys = t.keys;
		values = t.values;
		M = t.M;
	}
	
}
对于线性探测法,其中的get和put方法都很容易实现。只需要一个一个找下去就好了。而对于delete操作来说,相对来说就复杂一点了。当我们找到了键的时候,我们并不能简单的把键设为null,值也设为null,就结束了。因为我们是根据是否为null进行判断的,如果设为null了,我们查询到这里就终止了,那这个键之后的键就不会被查找到了。

所以我们需要将被删除键的右侧的所有的键都重新插入到散列表中。

我们插入的时候,只需要简单的找到位置插入就可以了。因为我们会维护数组的大小,不会让插入出现插入失败的情况。



散列表最大的好处就是读取速度非常快,只需要常数级别,时间复杂度为O(1)。

一个不是缺点的缺点,就是散列表的元素不是有序的,对于需要排序的应用来说,散列表就不是一个很好的选择了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值