算法笔记12:散列表

散列表和散列函数hashcode()

关于hash算法可以阅读以下文章
https://www.cnblogs.com/xiohao/p/4389672.html
https://blog.csdn.net/u014209205/article/details/80820263

在数组中查找只需要1次查询就可以得到所需数据,散列表就是希望使用数组提高查询速度。当有一个大小为M的数组时,我们根据键值分配其在数组中的位置,一般情况下我们使用java实现的hashcode()方法得到一个int值,根据hashcode()%M的值来决定在数组中的位置。
很显然当键的数量大于M时,会出现hash冲突,也就是两个不同的键值的散列值相同,我们可以增大数组M来减少冲突,但是没法消除冲突,因为键值是由调用散列表的程序的输入决定的,而且数组的维护需要内存,此时使用拉链法和开放地址法解决冲突。

拉链法

当hash冲突的时候对散列值相等的部分采用链表:首先根据散列值找到链表,然后在链表中顺序查找,拉链法的实现主要有两种:
1 数组中存储链表的结点类型Node,根据散列值找到相应的Node,这个Node就是链表的首节点,然后根据键值key判断首节点键值是否就是目标值,如果是则返回首节点的value,如果不是则根据Node的next属性找到下一个节点进行判断,直到找到对应键值的结点或者链表查找完毕返回null。插入时根据散列值找到对应的Node,如果为空则形成新的结点和引用,如果不为空则判断键值是否相等,如果相等则改变对应Node的value,如果不等则查找下一位,直到遇到null或者键值相等的结点。java的HashMap的源码中就是使用了这种方法,数组tab[]中保存的是链表的Node或者红黑树的TreeNode
2 数组中存储的是链表,而不是链表的结点。根据散列值直接找到链表,然后使用链表的方法查找、插入和删除。下面的代码就是采用这种方式,代码中LinkedListST访问超链接LinkedListST
可以看到,拉链法在查找和插入时比较的次数和链表的长度有关,无论键如何分布,链表的平均长度肯定是键的个数和数组的长度的比值:N/M。所以平均情况下的查找和插入最多需要N/M次比较和1次散列值的计算。

/**
 * 基于顺序链表查找表实现的拉链法的散列表
 */
import second.LinkedListST;
@SuppressWarnings("unchecked")
public class LinkedChainHashST<Key,Value> {
	private LinkedListST<Key, Value>[] tab;//使用链表的数组
	private final int M=97;
	
	public LinkedChainHashST(){
		tab=(LinkedListST<Key, Value>[])new LinkedListST[M];
		for (int i = 0; i < tab.length; i++) {
			tab[i]=new LinkedListST<Key, Value>();
		}
	}
	public Value get(Key key){
		return (Value)tab[hash(key)].get(key);		
	}
	public void put(Key key,Value value){
		tab[hash(key)].put(key,value);	
	}
	public void delete(Key key){
		tab[hash(key)].delete(key);
	}
	public int hash(Key key){
		return (key.hashCode() & 0x7fffffff)%M;		
	}
	public void show(){
		for (int i = 0; i < tab.length; i++) {
			tab[i].show();
		}
	}
	public static void main(String[] args) {
		LinkedChainHashST<String, Integer> st=new LinkedChainHashST();
		st.put("lm",3);
		st.put("sgt",4);
		st.put("zx",2);
		st.put("sq",1);
		st.delete("zx");
		st.show();
	}
}

查询的速度和链表的长度有关,在链表的长度很长时可以扩大M来减小长度,通常情况下链表的长度保持在2-8之间。N>8*M时M可以增加2倍。

开发地址法

拉链法存储的键值数量N小于数组长度M,在散列值重复的地方形成链表,开放地址法中是M>N,利用数组中的空位解决hash冲突。开放地址散列表最简单的方法为线性探测法:使用两个数组keys[]和values[]分别保存键值key和相应的value。插入时,根据hash值访问数组中的元素,如果该位置为空,那么目前没有和该键冲突的键,直接改变keys[]和values[]中的值,如果该位置不为空则存在冲突,那么判断key是否相等,如果相等则是重复,只改变values[]中的值,如果不相等则判断下一位,直到遇到null或者键值相等,当到达数组末尾时转向从0开始。查找时计算散列值找到元素如果键值相等则返回values[]中的值,如果不相等则查询下一位,遇到相等的键值或者null时结束。
线性探测法可以形象的理解为将拉链法中的链表放在数组中,将这种在数组中的“链表”称为键簇,即键值的散列值相等的键会相邻,将其称为键簇。线性探测法的性能和 α = M / N \alpha =M/N α=M/N有关,我们将其称为使用率,很显然使用率越大,冲突的机会则会变小,查找时键簇变短,查找更快,Kunth在1962年证明了命中所需的比较次数近似为 1 2 ( 1 + 1 1 − α ) \frac{1}{2}\left ( 1+\frac{1}{1-\alpha } \right ) 21(1+1α1)未命中所需的比较次数近似为 1 2 ( 1 + 1 ( 1 − α ) 2 ) \frac{1}{2}\left ( 1+\frac{1}{(1-\alpha) ^{2} } \right ) 21(1+(1α)21)
为了保证查找的速度一般保证$$之间,当N<M/8时checksize()缩短一半,N>M/2checksize()增加一倍。线性探测中删除操作需要处理数组中的后序元素,因为将该键值内容设为null后原来和改键冲突的键值将因为遇到null而不会被访问到。
线性探测法的实现如下:

/**
 * 基于线性探测法的散列表
 * @author XY
 *
 */
@SuppressWarnings("unchecked")
public class LinearProbingHashST<Key,Value> {
	private Key[] keys;
	private Value[] values;//两个数组保存键和对应的数据
	private int M=97;
	private int N;
	public LinearProbingHashST(){
		keys=(Key[])new Object[M];
		values=(Value[])new Object[M];
	}
	public void put(Key key,Value value){
		int x=hash(key);
		while (keys[x]!=null) {
			if(key.equals(keys[x])){//已经存在
				values[x]=value;
				return;
				}else x=(x+1)%M;
			}
		keys[x]=key;
		values[x]=value;
		N++;
		checksize();
	}
	public Value get(Key key){
		int x=hash(key);
		while(keys[x]!=null){
			if(key.equals(keys[x])) return values[x];//存在
			else {x=(x+1)%M;}
		}
		return null;
	}
	public void delete(Key key){
		for (int i = hash(key);keys[i]!=null; i=(i+1)%M) {
			if(keys[i].equals(key)){
				keys[i]=null;
				values[i]=null;//该键值存在,删除
				rest((i+1)%M);//为了避免此位置的null对后面的造成影响
				N--;
				checksize();
			}
		}
	}
	private void rest(int x){
		while(keys[x]!=null){//非空值向前移位,直到遇到null
			Key tempk=keys[x];
			Value tempv=values[x];
			keys[x]=null;
			values[x]=null;
			N--;
			put(tempk, tempv);
			x=(x+1)%M;
		}
	}
	private void checksize(){//检查大小,使得使用率在1/2~1/8之间
		if(N<M/8){
			Key[] skeys=keys;
			Value[] svalues=values;
			this.M=M/2;
			keys=(Key[])new Object[M/2];
			values=(Value[])new Object[M/2];
			for (int i = 0; i < M; i++) {
				if(skeys[i]!=null) put(skeys[i],svalues[i]);
			}
			
		}else if (N>M/2) {
			Key[] skeys=keys;
			Value[] svalues=values;
			this.M=M*2;
			keys=(Key[])new Object[M*2];
			values=(Value[])new Object[M*2];
			for (int i = 0; i < M; i++) {
				if(skeys[i]!=null) put(skeys[i],svalues[i]);
			}
		}
	}
	public int hash(Key key){
		return (key.hashCode() & 0x7fffffff)%M;
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值