20 散列表

散列表的本质就是:hash函数计算后得到的名值对已经当hash函数算到相同的名的时候,解决冲突的方法。最简单的就是字符ascii码当下标访问长度为128的数组,字符的值直接被当作名访问数组的对应项,即:index = hash(ch),这里很明显不存在冲突。


hash函数举例(实际上hash函数是很难设计的):字符串得到hash值代码

package nuaa.hash;

public class Hash {
	public static int hash1(String key,int tableSize){
		int hashValue = 0;
		for(int i=0;i<key.length();i++){
			hashValue = (hashValue*128+key.charAt(i))%tableSize;
		}
		return hashValue;
	}
	
	public static int hash2(String key,int tableSize){
		int hashValue = 0;
		for(int i=0;i<key.length();i++){
			hashValue = hashValue*37+key.charAt(i);
		}
		hashValue = hashValue%tableSize; //%运算允许可以越界的
		if(hashValue<0)              //因此可能hashValue已经溢出变成负值
			hashValue += tableSize;
		return hashValue;
	}
	
	//String内部hashCode生成的方式,区别是String会缓存散列码
	//从hashCode是找不到对应的串的,hash函数其实也叫单向函数
	public static int hash3(String key,int tableSize){
		int hashValue = 0;
		for(int i=0;i<key.length();i++){
			hashValue = hashValue*31+key.charAt(i);
		}
		
		return hashValue;
	}
}

冲突解决:

首先都是%tableSize,tableSize数组的长度,存储项的方式:

1 图的存储方式:邻接表,冲突就邻接式挂链

2 数组式存储:冲突时候可以线性探测,每次加1看是否有元素,到底就循环至头,但这个很明显会造成 初始聚类  ,推导可以得到插入平均分析单元数为(1+1/(1-a)^2)/2,a为负载因子,比如a为0.9时,前次和本次插入不是独立的,因此实际的平均分析单元数为50,而不是独立情况下的10个,性能是不可接受的。因此可以采用二次探测。

每次本次的位置加上i^2开始探测,i从1开始。本质 就是相邻元素每次冲突寻找的位置不一样,因此不会造成 初始聚类 。当然也会造成二次聚类,会造成每次插入要加上额外的小于一半的探测,但这个只在高负载的情况下出现。

 插入:数组元素个数为素数(非素数会造成可供选择的位置偏少,也就是回绕的时候导致重复项),载重因子不超过0.5时,二次探测总能找到位置插入元素。

删除:由于是hash操作,因此每个元素都不能动,也不能设置要删除的元素为null,null表示冲突寻找到结尾了,会影响寻找操作,删除操作只能是每个元素设置一个额外的标识,这种删除叫做迟删除。

package nuaa.ds;

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;

public class HashSet<E> extends AbstractCollection<E> implements Set<E> {
	
	private static final int DEFAULT_TABLE_SIZE = 101;
	private int currentSize = 0; //当前实际元素容量
	private int occupied = 0;    //实际有值不为null的容量
	private int modCount = 0;    //修改次数
	private HashEntry[] array;
	
	public HashSet(){
		this.allocateArray(DEFAULT_TABLE_SIZE);
	}
	public HashSet(Collection<? extends E> other){
		allocateArray(nextPrime(other.size()*2));
		clear();
		for(E e:other)
			add(e);
	}
	
	@Override
	public int size(){
		return this.currentSize;
	}
	
	@Override
	public Iterator<E> iterator() {
		return new HashSetIterator();
	}

	public boolean contains(Object x){
		return isActive(array,findPos(x));
	}
	
	private static boolean isActive(HashEntry[] arr,int pos){
		return arr[pos]!=null && arr[pos].isActive;
	}
	
	public E getMatch(E x){
		int currentPos = findPos(x);
		if(isActive(array,currentPos)) {
			return (E)array[currentPos].element;
		}
		return null;
	}
	
	public boolean remove(Object x){
		int currentPos = findPos(x);  //得到的下标永远不会越界
		if(!isActive(array,currentPos))
			return false;
		
		array[currentPos].isActive = false;
		currentSize--;
		modCount++;
		
		//剩余项只有8分之一的时候rehash
		if(currentSize<array.length/8)
			rehash();
		return true;
	}
	
	public void clear(){
		currentSize = occupied = 0;
		modCount++;
		for(int i=0;i<array.length;i++)
			array[i] = null;
	}
	
	//不能增加重复项
	public boolean add(E x){
		int currentPos = findPos(x);
		if(isActive(array,currentPos))
			return false;
		if(array[currentPos]==null)
			occupied++;
		array[currentPos] = new HashEntry(x,true);
		currentSize++;
		modCount++;
		
		//实际占据超过数组长度的一半就rehash
		//不用真正有用的数据量size是下面的findPos函数的缘故
		//findPos根据null来查找是否有插入位置
		if(occupied>array.length/2) 
			rehash();
		return true;
	}
	
	private void rehash(){
		HashEntry[] oldArray = array;
		allocateArray(nextPrime(4*size()));//loadFactor变成0.25
		currentSize = 0;
		occupied = 0;
		
		for(int i=0;i<oldArray.length;i++)
			if(isActive(oldArray,i))
				add((E)oldArray[i].element);
	}
	
	/**
	 * 
	//程序控制数组的元素个数为素数
	//loadFactor不超过0.5
	//因此肯定能找到位置
	 * 要么是已经存在的,要么就是新的null位置
	 */
	private int findPos(Object x){
		int offset = 1;
		int currentPos =   //null的话hashCode为0
					(x==null) ? 0:Math.abs(x.hashCode()%array.length);
		while(array[currentPos]!=null){
			if(x==null){ //null可以重复插入,返回已经存在的位置
				if(array[currentPos].element==null)
					break;
			}else if(x.equals(array[currentPos].element))
				break;             //已经存在就返回存在的位置
			
			currentPos += offset;
			offset += 2;           //二次探测,每次增加2i-1
			if(currentPos>=array.length)//超过数组容量就从头开始
				currentPos -= array.length;
		}
		return currentPos;					
	}
	
	private void allocateArray(int arraySize){
		array = new HashEntry[arraySize];
	}
	
	private static int nextPrime(int n){
		if(n%2==0)
			n += 1;
		for(;isPrime(n);n+=2)
			;
		return n;
	}
	
	//一般情况下2特殊判断,然后3开始奇数判断是否能整除
	//n肯定不能被sqrt(n)到n的数整除
	//这里由nextPrime调用,传入的就是大于2的奇数
	private static boolean isPrime(int n){
		for(int i=3;i<=(int)Math.sqrt(n);i+=2)
			if(n%i==0)
				return false;
		return true;
	}
	
	private class HashSetIterator implements Iterator<E>{
		
		private int expectedModCount = modCount;
		private int currentPos = -1;
		private int visited = 0;
		
		@Override
		public boolean hasNext() {
			if(expectedModCount!=modCount)
				throw new ConcurrentModificationException();
			return visited!=size();
		}
        
		@Override
		public E next() {
			if(!hasNext())
				throw new NoSuchElementException();
			do{
				currentPos++;
			}while(currentPos<array.length&&!isActive(array,currentPos));
			visited++;
			return (E)array[currentPos].element;
		}

		@Override
		public void remove(){
			if(expectedModCount!=modCount)
				throw new ConcurrentModificationException();
			if(currentPos!=-1||isActive(array,currentPos))
				throw new IllegalStateException();
			array[currentPos].isActive = false;
			currentSize--;
			visited--;
			modCount++;
			expectedModCount++;
		}
	}
	
	//计算式,删除时候所有元素都不能动,不能像arrayList那样移动后面的覆盖掉
	//也不能设置为null,因为当前元素会影响到后面插入的元素的查找和删除
	//null是表示冲突检查到底了
	private static class HashEntry implements java.io.Serializable{
		public Object element;
		public boolean isActive;// 标识是否删除
		
		public HashEntry(Object e){
			this(e,true);
		}
		public HashEntry(Object e,boolean i){
			element = e;
			isActive = i;
		}
	}
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值