数据结构与算法之哈希表

(一) Hash表

1.哈希(散列)函数

在一般的线性表和树中, 存储的元素在结构中的相对位置是随机的, 即和存储的元素之间不存在确定的关系. 所以,在以上数据结构中查找存储的元素时需进行一系列和关键字(key)的比较.

如: 在数组中, 从0索引处到末尾索引中取出数据与关键字(key)一一比较; 在树中, 从根节点到叶子节点中取出数据与关键字(key)一一比较.

设想, 是否存在一种函数 f(), 直接通过关键字(key) 找到 值(value)所在位置(索引或节点…), 那么函数 f()就称之为 hash(哈希)函数

哈希冲突: 哈希函数并不能保证每一个关键字(key) 找到 对应不同的位置, 即:不同的关键字对应到同一个存储位置的现象

2.哈希表

哈希表是基于哈希函数建立的一种特殊的数据结构, 这种数据结构最复杂的操作在于设计哈希函数解决哈希冲突

在哈希表中, “key” 通过哈希函数得到的 “索引(位置)” 分布的越均匀越好, 则哈希冲突越低,效率越高。为了降低哈希冲突,需要采用大于实际存储数据数量的哈希表. 哈希表的设计充分的提现算法设计领域的经典思想: 空间换时间

3.哈希函数的设计

设计遵循三大原则:

  1. 一致性: 如果 a == b, 则 hash(a) == hash(b);
  2. 均匀性: 哈希值分布均匀
  3. 高效性: hash函数计算高效
  • 整型:

    1. 小范围的正整数直接使用
    2. 小范围的负整数进行偏移: -100 ~ 100 ====> 0 ~ 200
    3. 大整数: 取模 于 一个素数: 25 ~ 26 ====> % 53
  • 浮点型: 浮点数在计算机中都是32位或64位的二进制表示, 只不过是计算机解析成了浮点数, 将二进制转成整型处理

  • 字符串: 转成整型处理 (B: 代表进制; M: 代表素数)

    1. “166” ====> 1 * 102 + 6 * 101 + 6 * 100
    2. “code” ====> c * 263 + o * 262 + d * 261 + e * 260
    3. “code” ====> c * B3 + o * B2 + d * B1 + e * B0
    4. hash(“code”) ====> ( c * B3 + o * B2 + d * B1 + e * B0 ) % M
    5. hash(“code”) ====> ((((c * B) + o) * B + d) * B + e ) % M
    6. hash(“code”) ====> ((((c % M) * B + o) % M * B + d) % M * B + e ) % M
  • 复合类型: 转换成整型处理

    1. Date(year, month, day): hash(“Date”) ====> (((data.year % M) * B + data.month) % M * B + data.day) % M
3.哈希冲突的解决方案
  • 链地址法(Seperate Chaining): 添加元素时根据哈希函数计算得出在数组中的位置索引, 然后进行存储. 当位置索引重复时存储在查找表中, 查找表可以为链表, 平衡树等数据结构.
    在这里插入图片描述

  • 开放地址法: 与链地址法刚好相反, 在链地址法中, 索引地址是封闭的, 只能存储相同哈希值的元素. 而开放地址法中的索引地址可以存储不相同哈希值的元素, 即相同哈希值的元素可以存储在不同的索引地址.
    在这里插入图片描述
    平方探测遇到哈希冲突: +1 +4 +9 +16…

(二) 自定义HashTable

基于Java中的TreeMap(底层采用红黑树)实现, 哈希表实际存储了 TreeMap[] 数组

1.HashTable基础结构
public class HashTable<K, V> {

	/**
	 * 存储元素的数据结构: TreeMap[] 数组
	 */
	private TreeMap<K, V>[] hashTable;
	
	/**
	 * 取模的素数
	 */
	private int M;
	
	/**
	 * 存储元素个数
	 */
	private int size;
	
	@SuppressWarnings("unchecked")
	public HashTable(int M) {
		this.M = M;
		this.size = 0;
		hashTable = new TreeMap[M];
		for (int i = 0; i < M; i++) {
			hashTable[i] = new TreeMap<>();
		}
	}
	
	public HashTable() {
		this(97);
	}
	
	/**
	 * 哈希函数: 先将K转换为大整型, 然后取模于一个素数M
	 * 
	 * @param key
	 * @return
	 */
	private int hash(K key) {
		return (key.hashCode() & 0x7fffffff) % M;
	}
}
2.HashTable的CRUD操作
/**
 * 向hashTable中添加键值对key-value数据
 * 
 * @param key
 * @param value
 */
public void add(K key, V value) {
	// hashTable通过哈希函数获取指定位置索引的TreeMap
	TreeMap<K, V> map = hashTable[hash(key)];
	if (map.containsKey(key)) {
		map.put(key, value);
	} else {
		map.put(key, value);
		size++;
	}
}

/**
 * 从HashTable中删除键为key的数据
 * 
 * @param key
 * @param value
 */
public V remove(K key) {
	TreeMap<K, V> map = hashTable[hash(key)];
	V res = null;
	if (map.containsKey(key)) {
		res = map.remove(key);
		size--;
	}
	return res;
}

/**
 * 从HashTable中修改Key的值为Value
 * 
 * @param key
 * @param value
 */
public void set(K key, V value) {
	TreeMap<K, V> map = hashTable[hash(key)];
	if (!map.containsKey(key)) {
		throw new IllegalArgumentException(key + " donen't exist!");
	}
	map.put(key, value);
}

/**
 * 判断HashTable是否存在Key
 * 
 * @param key
 * @return
 */
public boolean contains(K key) {
	return hashTable[hash(key)].containsKey(key);
}

/**
 * 从HashTable中取出Key对应的值
 * 
 * @param key
 * @return
 */
public V get(K key) {
	return hashTable[hash(key)].get(key);
}

(三) 自定义HashTable时间复杂度分析

1.HashTable静态空间的时间复杂度分析

HashTable时间复杂度分析分为两步:

  • 根据哈希函数计算得出的索引找到对应的查找表: 由于HashTable是个数组, 支持随机索引访问, 时间复杂度为O(1)
  • 在查找表中根据key寻找value: 假设哈希表中的数组有M个地址,且放入哈希表的元素为N. 则哈希表的每个地址平均存储 N/M 个元素
    1. 查找表底层实现为链表, 时间复杂度为: O(N/M)
    2. 查找表底层实现为平衡树, 时间复杂度为: O(log(N/M))

M 为数组长度且不变, 与静态数组一样, 固定的地址空间是不合理的

2.HashTable动态空间处理
  • 扩容: 平均每个地址承载的元素多过一定程度, 如 N / M >= upperTol
  • 缩容: 平均每个地址承载的元素少过一定程度, 如 N / M < lowerTol

重置哈希表的数组容量, 必须保证数组容量(M)一定为素数, 优化add(K, V) 和 remove(K) 方法

/**
 * 每个地址承载的上限元素个数
 */
private static final int UPPER_TOTAL = 10;

/**
 * 每个地址承载的下限元素个数
 */
private static final int LOWER_TOTAL = 2;

/**
 * 素数(容量)数组常量
 */
private final int[] CAPACITY = { 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 
	786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319,201326611, 402653189, 805306457, 1610612741 };

/**
 * 初始容量数组索引
 */
private int capacityIndex = 0;

@SuppressWarnings("unchecked")
public HashTable() {
	this.M = CAPACITY[capacityIndex];
	this.size = 0;
	hashTable = new TreeMap[M];
	for (int i = 0; i < M; i++) {
		hashTable[i] = new TreeMap<>();
	}
}

@SuppressWarnings("unchecked")
private void resize(int newM) {
	TreeMap<K, V>[] newHashTable = new TreeMap[newM];
	for (int i = 0; i < newM; i++) {
		newHashTable[i] = new TreeMap<>();
	}
	
	// 原哈希表的数组长度
	int oldM = this.M;
	// 哈希函数取模于 newM
	this.M = newM;
	
	for (int i = 0; i < oldM; i++) {
		TreeMap<K,V> map = hashTable[i];
		for(K key : map.keySet()) {
			newHashTable[hash(key)].put(key, map.get(key));
		}
	}
	this.hashTable = newHashTable;
}

/**
* 向hashTable中添加键值对key-value数据
 * 
 * @param key
 * @param value
 */
public void add(K key, V value) {
	// hashTable通过哈希函数获取指定位置索引的TreeMap
	TreeMap<K, V> map = hashTable[hash(key)];
	if (map.containsKey(key)) {
		map.put(key, value);
	} else {
		map.put(key, value);
		size++;
		
		// 平均每个地址承载的元素个数 >= 上限元素个数
		if(size >= UPPER_TOTAL * M && capacityIndex + 1 < CAPACITY.length) {
			capacityIndex++;
			resize(CAPACITY[capacityIndex]);
		}
	}
}

/**
 * 从HashTable中删除键为key的数据
 * 
 * @param key
 * @param value
 */
public V remove(K key) {
	TreeMap<K, V> map = hashTable[hash(key)];
	V res = null;
	if (map.containsKey(key)) {
		res = map.remove(key);
		size--;
		
		// 平均每个地址承载的元素个数 < 下限元素个数
		if(size < LOWER_TOTAL * M && capacityIndex - 1 >= 0) {
			capacityIndex--;
			resize(CAPACITY[capacityIndex]);
		}
	}
	return res;
}

3.HashTable动态空间的时间复杂度分析

对于动态空间哈希表来说, 添加元素的时间复杂度为O(1), 只有元素数从N 增加到 upperTol * N, 地址空间翻倍O(n), 但均摊到每次添加操作中 平均复杂度为 O(1).

或者可以理解为添加操作在 O(lowerTol) ~ O(upperTol) 中, lowerTol 和 upperTol 为自定义常数, 则平均时间复杂为 O(1), 缩容同理. (哈希表牺牲了元素的顺序性).

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值