哈希表

本文详细介绍了哈希表的概念,包括其工作原理、哈希函数设计、哈希冲突及其解决方案。哈希表通过哈希函数将键映射到特定位置以快速访问数据。哈希冲突的常见解决方法有开放地址法、再哈希法等。此外,还讨论了哈希表的Java接口实现,并展示了使用AVL平衡树作为底层数据结构的哈希表实现,包括扩容和缩容策略。
摘要由CSDN通过智能技术生成

1. 哈希表:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。 给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

因此哈希表最关键的两个问题就是,如何设计哈希函数,如何解决哈希冲突

2. 哈希函数:" 键 "通过哈希函数得到的" 索引 "分布越均匀越好,对于些特殊领域,有特殊领域的哈希函数设计方式,甚至有专门的论文。我们只关注一般的哈希函数的设计。我们对通过hashCode计算出的值进行取正后再对一个素数取余( 可以将hash表容量定义成素数,对hash表容量取余 )来计算hash值

3. 哈希冲突:不同的数据计算出的hash值相同即为哈希冲突

4. 哈希冲突的解决方案:

        1>. 开放地址法( Open Addressing )

                线性探测法,遇到哈希冲突 +1。

                平方探测法,遇到哈希冲突+1 +4 +9 +16。

                二次哈希法 +hash2(key)。

        2>. 再哈希法( Rehashing )

                重新计算hash值。

        3>. Colasced Hashing

                融合了链地址法和开放地址法。

5. 哈希表实现的接口:

public interface Map<K, V> {
	
	//向map中添加键值对
	public void put(K key, V value);
	
	//删除map中指定key的键值对
	public V remove(K key);
	
	//判断二分搜索树中是否包含指定key
	public boolean contains(K key);
	
	//通过key从map中获取key对应的值
	public V get(K key);
	
	//修改指定key对应的值
	public void set(K key, V value);
	
	//获取map中有效元素的个数
	public int size();
	
	//判断map是否为空
	public boolean isEmpty();
	
	//获取map中的键的集合 因为map中键key是唯一的 所以用集合set进行存储
	public Set<K> keySet();
	
	//获取map中的值的列表 因为map中键value不是唯一的 所以用列表list进行存储
	public List<V> values();
	
	//获取map中键值对的集合 因为map中键值对(key:value)是唯一的 所以用集合set进行存储
	public Set<Entry<K, V>> entrySet();
	
	//定义一个获取键值对Entry<K, V>的接口
	public interface Entry<K, V> extends Comparable<Entry<K, V>>{
		
		//获取键值对的键
		public K getKey();
		
		//获取键值对的值
		public V getValue();
				
	}
}

6. 哈希表数据结构的实现:我们底层通过AVL平衡树映射来实现哈希

//定义hash表 底层通过AVL平衡树映射来实现
//AVL平衡树是最早的一种二分搜索树之一 因此AVL平衡树实现的映射中的key应具有比较性
//因此hash表中的映射的key也要具有可比性
public class HashTable<K extends Comparable<K>, V> implements Map<K, V>{
	//预制数组容量 是一个容量逐渐递增的数组
	private static final int[] capacity = {
            53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
            49157, 98317, 196613, 393241, 768433, 1572869, 3145739, 6291469,
            12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741
    };
	
	//每个桶的最大存储数据数量
	private static final int upperTol = 10;
	
	//每个桶的最小存储数据数量
	private static final int lowerTol = 2;
	
	//预制容量数组的索引
	private int capacityIndex = 0;
	
	//表示hash表中桶的数量 即hash表的容量
	private int M;
	
	//表示hash表中键值对的个数
	private int size;
	
	//存放数据的数组即为hash表 AVLTreeMap类型的数组 
	//用来存储AVLTreeMap 即一个AVLTreeMap代表一个桶
	private AVLTreeMap<K, V>[] hashTable;
	
	//构造方法 初始化hash表容量 hash表 hash表中键值对的个数 
	public HashTable() {
		M = capacity[capacityIndex];
		hashTable = new AVLTreeMap[M];
		for(int i = 0; i < M; i++) {
			hashTable[i] = new AVLTreeMap<K, V>();
		}
		size = 0;
	}

	//计算key的hash值 即指定键在hash表中桶的角标
	private int hash(K key) {
		//为防止计算的结果超过整型范围 并且 计算的结果在数组中下标越界 
		//则 对hashCode计算出的hash值 先与0x7fffffff进行位运算 再对M取余
		return key.hashCode() & 0x7ffffff % M;
	}
	
	//向hash表中添加指定键值对 如果key已经存在 则修改原来的值
	@Override
	public void put(K key, V value) {
		//先获取指定key所在的AVLTreeMap
		int index = hash(key);
		AVLTreeMap<K, V> map = hashTable[index];
		
		//判断key是否存在
		if(map.contains(key)) {
			map.put(key, value); //存在 则修改原先key的值
		}else { //不存在 则将键值对添加到treeMap中
			map.put(key, value);
			size++;
			
			//判断是否需要扩容 如果size超过总体的hash表的最大容量 即需要扩容 且容量数组的索引加一在容量数组中
			//即将容量数组的索引后移到容量数组中容量数组的索引的下一个位置
			if(size > upperTol * M && capacityIndex + 1 < capacity.length) {
				capacityIndex++;
				
				//对数组进行扩容 新长度为容量数组的指定索引位置的值
				resize(capacity[capacityIndex]);
			}
		}
	}

	//删除hash表中添加指定键值对 并返回要删除key的值
	@Override
	public V remove(K key) {
		//先获取指定key所在的AVLTreeMap
		int index = hash(key);
		AVLTreeMap<K, V> map = hashTable[index];
		V ret = null;
		
		//判断key是否存在
		if(map.contains(key)) {
			//存在 则删除key
			ret = map.remove(key);
			size--;
			
			//判断是否需要缩容 如果size小于总体的hash表的最小容量 即需要缩容 且容量数组的索引减一在容量数组中
			//即将容量数组的索引前移到容量数组中容量数组的索引的上一个位置
			if(size < lowerTol * M && capacityIndex - 1 >= 0) {
				capacityIndex--;
				
				//对数组进行缩容 新长度为容量数组的指定索引位置的值
				resize(capacity[capacityIndex]);
			}
		}
		return ret;
	}

	//对数组进行缩容或者扩容
	private void resize(int newM) {
		//创建新数组和数组中的每一个桶 长度为newM
		AVLTreeMap<K, V>[] newHashTable = new AVLTreeMap[newM];
		for(int i = 0; i < newHashTable.length; i++) {
			newHashTable[i] = new AVLTreeMap<K, V>();
		}
		
		//遍历原数组 将原数组的键值对重新添加到新数组中 
		//添加的位置要重新通过hash方法计算 但是hash方法中用的是原数组容量计算的
		//因此在计算前要将hash中的原数组容量改为新数组容量
		M = newM;
		for(int i = 0; i < hashTable.length; i++) {
			AVLTreeMap<K, V> map = hashTable[i];
			for(K key : map.keySet()) {
				newHashTable[hash(key)].put(key, map.get(key));
			}
		}
		hashTable = newHashTable;
	}

	//判断hash表中是否包含指定key
	//先获取指定key所在的AVLTreeMap 再判断指定key是否在其所在的AVLTreeMap中即可
	@Override
	public boolean contains(K key) {
		int index = hash(key);
		AVLTreeMap<K, V> map = hashTable[index];
		return map.contains(key);
	}

	//获取hash表中指定key的值
	//先获取指定key所在的AVLTreeMap 再获取指定key在其所在的AVLTreeMap中的值即可
	@Override
	public V get(K key) {
		int index = hash(key);
		AVLTreeMap<K, V> map = hashTable[index];
		return map.get(key);
	}

	//修改hash表中指定key的值
	//先获取指定key所在的AVLTreeMap 如果指定的key在map中 就将其值进行修改
	@Override
	public void set(K key, V value) {
		int index = hash(key);
		AVLTreeMap<K, V> map = hashTable[index];
		map.set(key, value);
	}

	//获取hash表中键值对的个数
	@Override
	public int size() {
		return size;
	}

	//判断hash表是否为空
	@Override
	public boolean isEmpty() {
		return size == 0;
	}

	//获取hash表中键的集合 并返回
	/*
	 * 先创建一个存放key的容器
	 * 遍历数组 并获取对应位置的桶(AVLTreeMap) 
	 * 再遍历桶的键的集合 将每个桶的键依次添加到新容器中 并返回新容器
	 */
	@Override
	public Set<K> keySet() {
		TreeSet<K> set = new TreeSet<K>();
		for(int i = 0; i < hashTable.length; i++) {
			AVLTreeMap<K, V> map = hashTable[i];
			for(K key : map.keySet()) {
				set.add(key);
			}
		}
		return set;
	}

	//获取hash表中值的列表 并返回
	/*
	 * 先创建一个存放value的容器
	 * 遍历数组 并获取对应位置的桶(AVLTreeMap) 
	 * 再遍历桶的键值集合 将每个桶的值依次添加到新容器中 并返回新容器
	 */
	@Override
	public List<V> values() {
		LinkedList<V> list = new LinkedList<V>();
		for(int i = 0; i < hashTable.length; i++) {
			AVLTreeMap<K, V> map = hashTable[i];
			for(V value : map.values()) {
				list.add(value);
			}
		}
		return list;
	}

	//获取hash表中键值对的集合 并返回
	/*
	 * 先创建一个存放value的容器
	 * 遍历数组 并获取对应位置的桶(AVLTreeMap) 
	 * 再遍历桶的键值对的集合 将每个桶的键值对依次添加到新容器中 并返回新容器
	 */
	@Override
	public Set<Entry<K, V>> entrySet() {
		TreeSet<Entry<K, V>> set = new TreeSet<Entry<K,V>>();
		for(int i = 0; i < hashTable.length; i++) {
			AVLTreeMap<K, V> map = hashTable[i];
			for(Entry entry : map.entrySet()) {
				set.add(entry);
			}
		}
		return set;
	}
}

 本次课程会带着大家学习Hash算法,从源码的角度去学习算法,更加容易理解的方式去学习,能够更高效的吸收学到的内容,也能培养出能够独自看源码,分析源码的能力。Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。  哈希表是根据设定的哈希函数H(key)和处理冲突方法将一组关键字映射到一个有限的地址区间上,并以关键字在地址区间中的象作为记录在表中的存储位置,这种表称为哈希表或散列,所得存储位置称为哈希地址或散列地址。作为线性数据结构与表格和队列等相比,哈希表无疑是查找速度比较快的一种。  通过将单向数学函数(有时称为“哈希算法”)应用到任意数量的数据所得到的固定大小的结果。如果输入数据中有变化,则哈希也会发生变化。哈希可用于许多操作,包括身份验证和数字签名。也称为“消息摘要”。  简单解释:哈希(Hash)算法,即散列函数。它是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出。哈希函数的这种单向特征和输出数据长度固定的特征使得它可以生成消息或者数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值