哈希理论基础
哈希表
哈希表是根据关键码的值而直接进行访问的数据结构
哈希表能解决什么问题? 快速判断一个元素是否出现在集合里,只需要时间复杂度
哈希函数
哈希函数(hash function)可以把值直接映射为哈希表上的索引。在查找的时候可以直接通过查询索引快速得知某值是否在表中。
哈希函数会先通过hashCode把其他数据格式转化为数值,从而映射为哈希表上的索引。当转化出的值大于哈希表的tableSize,就对数值取模,从而落到哈希表上。
哈希碰撞
当两个数据被映射到同一个哈希索引上,称之为哈希碰撞。解决哈希碰撞有两种常用方法:拉链法(链表法),线形探测法
拉链法
把发生冲突的数据以链表的形式存储在冲突的索引位置。查找数据时需要在索引处遍历链表
使用拉链法要选择适当的tableSize,防止因为表中空值浪费内存,和链表太长而浪费查找时间
线形探测法
前提:tableSize>dataSize
如果数值计算出的索引已经存放了数据,就向下寻找空位存放。
常见的三种哈希结构
HashMap、TreeMap 和 HashSet 的原理区别
如何选择HashMap、TreeMap 和 HashSet
- HashMap:需要高效的键值对存取,顺序无关。
- TreeMap:需要按键的顺序存储和访问键值对。
- HashSet:需要存储无重复的元素,顺序无关。
HashMap
- HashMap 采用数组+链表/红黑树的存储结构,能够在 O(1)的时间复杂度内实现元素的添加、删除、查找等操作。
- HashMap 是线程不安全的,因此在多线程环境下需要使用ConcurrentHashMap来保证线程安全。
- HashMap 的扩容机制是通过扩大数组容量和重新计算 hash 值来实现的,扩容时需要重新计算所有元素的 hash 值,因此在元素较多时扩容会影响性能。触发扩容后,容量通常变为原来的两倍
- 在 Java 8 中,HashMap 的实现引入了拉链法、树化等机制来优化大量元素存储的情况,进一步提升了性能。
- HashMap 中的 key 是唯一的,如果要存储重复的 key,则后面的值会覆盖前面的值。
- HashMap 的初始容量和加载因子都可以设置,==初始容量表示数组的初始大小,加载因子表示数组的填充因子==。一般情况下,初始容量为 16,加载因子为 0.75。
- HashMap 在遍历时是无序的,因此如果需要有序遍历,可以使用TreeMap。
HashSet
- HashSet基于HashMap,用HashMap的键储存集合元素,值使用一个固定对象
- HashSet也是无序的。查找、插入、删除操作平均时间复杂度为
TreeMap
- TreeMap采用红黑树的存储结构,查找、插入、删除操作的时间复杂度为 。
- TreeMap的键值对有序,按照键的自然顺序(需要实现Comparable接口)或通过自定义的Comparator排序
LeetCode 242 有效的字母异位词
思路
本质上是在看t中出现的字母是否也在s中出现,故使用哈希表。 字母作为键,字母出现的次数作为值。但字母的个数是已知的,最多26种,所以可以用数组的下标0~25代替字母a~z。
- 遍历字符串s中的字母,记录每个字母出现的个数
- 遍历t中的字母,每遇到一个字母就把对应的次数-1.
- 检查数组是否全为0,是则返回true,否则返回false
LeetCode 349 两个数组的交集
思路
题目本质是要看有哪些数字,在nums1存在也在nums2存在。涉及存在的问题,使用哈希表。鉴于题目只考虑唯一的数字,不考虑出现次数,只需要使用键,我们可以使用HashSet
- 遍历nums1,把所有唯一的数字存储在一个HashSet set1中
- 遍历nums2,对于每个存在在set1中的数字,加入另一个HashSet set2中。
- 把set2转为数组返回
难点
把HashSet<Integer>转为int[]过程较为麻烦
// 转换为 Integer[] 数组
Integer[] tempArray = set.toArray(new Integer[0]);
// 转换为 int[] 数组
int[] intArray = new int[tempArray.length];
for (int i = 0; i < tempArray.length; i++) {
intArray[i] = tempArray[i];
}
toArray(T[] a)的工作机制:
- 接受一个类型为 T[] 的数组作为参数,用来确定目标数组的类型。
- 返回一个与集合大小相等且类型为 T[] 的数组。
- 如果传入的数组 a 足够大,集合会直接将元素放入该数组,并返回。
- 如果传入的数组 a 长度不足,toArray 会创建一个新的数组,类型与 a 相同,大小为集合的元素数量。
- 传入 new Integer[0] 是一种惯用法,表示希望生成一个与集合大小匹配的数组,同时确保数组类型正确。因为传入的数组长度为 0,toArray 不会浪费空间,直接创建一个大小适合的数组。
LeetCode 202 快乐数
思路
根据题设我们可以知道对于一个正整数n,它的迭代过程有两种
- 无限循环
- 可以变为1 如果在迭代的过程中出现了曾经出现过的数字,那么n一定会无限循环 因为迭代过程相同,再循环一次也只是按照上一次的路径,而路径中没有1 所以只要出现过已经出现的数,就不是快乐数,返回false。这种“是否出现”的题目可以使用哈希表,且本题只有一类数据,故使用set
从而我们得到如下步骤:
- 创建一个HashSet,加入n
- 把n替换为它每个位置上的数字的平方和n1
- 如果n1存在于set中,返回false
- 如果不存在,把n1加入set,继续循环
LeetCode 1 两数之和
思路
对于每个在nums中的num,都有一个对应的数字target-num。只要看target-num是否存在于nums中,存在即返回两者下标
- 存在👉哈希表
- 通过值找下标👉HashMap,键为值,值为下标
故有如下步骤:
- 创建一个HashMap
- 对于每个nums[i]
- 在map中查找是否存在键nums[i]
- 存在则返回i和键对应的值
- 不存在则向map中加入键值对(target-nums[i], i),进入下个循环
- 在map中查找是否存在键nums[i]
因为题设有且只有一个解,在最外侧的返回空值即可
今日收获总结
今日学习3小时,因为前几次都没有去学习哈希的底层结构,这次主要精力放在了对Java中哈希表的底层原理理解上。题目都能独立完成,因为只要知道用哈希,就比较简单~所以对题目着重剖析了考虑到哈希法的心路历程。