代码随想录算法训练营Day6|哈希基础,242 有效的字母异位词,349 两个数组的交集,202 快乐数,1 两数之和

85 阅读6分钟

哈希理论基础

参考文档:programmercarl.com/哈希表理论基础.htm…

哈希表

哈希表是根据关键码的值而直接进行访问的数据结构

哈希表能解决什么问题? 快速判断一个元素是否出现在集合里,只需要时间复杂度O(1)O(1)

哈希函数

哈希函数(hash function)可以把值直接映射为哈希表上的索引。在查找的时候可以直接通过查询索引快速得知某值是否在表中。

哈希函数会先通过hashCode把其他数据格式转化为数值,从而映射为哈希表上的索引。当转化出的值大于哈希表的tableSize,就对数值取模,从而落到哈希表上。 Pasted image 20250113103255.png

哈希碰撞

当两个数据被映射到同一个哈希索引上,称之为哈希碰撞。解决哈希碰撞有两种常用方法:拉链法(链表法),线形探测法

拉链法

把发生冲突的数据以链表的形式存储在冲突的索引位置。查找数据时需要在索引处遍历链表

使用拉链法要选择适当的tableSize,防止因为表中空值浪费内存,和链表太长而浪费查找时间

线形探测法

前提:tableSize>dataSize

如果数值计算出的索引已经存放了数据,就向下寻找空位存放。

常见的三种哈希结构

HashMap、TreeMap 和 HashSet 的原理区别 HashMap、TreeMap 和 HashSet 的原理区别.png

如何选择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也是无序的。查找、插入、删除操作平均时间复杂度为O(1)O(1)

TreeMap

  • TreeMap采用红黑树的存储结构,查找、插入、删除操作的时间复杂度为 O(logn)O(log n)
  • TreeMap的键值对有序,按照键的自然顺序(需要实现Comparable接口)或通过自定义的Comparator排序

LeetCode 242 有效的字母异位词

题目链接:leetcode.cn/problems/va…

文档讲解:programmercarl.com/0242.有效的字母异…

视频讲解:www.bilibili.com/video/BV1YG…

思路

本质上是在看t中出现的字母是否也在s中出现,故使用哈希表。 字母作为键,字母出现的次数作为值。但字母的个数是已知的,最多26种,所以可以用数组的下标0~25代替字母a~z。

  1. 遍历字符串s中的字母,记录每个字母出现的个数
  2. 遍历t中的字母,每遇到一个字母就把对应的次数-1.
  3. 检查数组是否全为0,是则返回true,否则返回false

LeetCode 349 两个数组的交集

题目链接:leetcode.cn/problems/in…

文档讲解:programmercarl.com/0349.两个数组的交…

视频讲解:www.bilibili.com/video/BV1ba…

思路

题目本质是要看有哪些数字,在nums1存在也在nums2存在。涉及存在的问题,使用哈希表。鉴于题目只考虑唯一的数字,不考虑出现次数,只需要使用键,我们可以使用HashSet

  1. 遍历nums1,把所有唯一的数字存储在一个HashSet set1中
  2. 遍历nums2,对于每个存在在set1中的数字,加入另一个HashSet set2中。
  3. 把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[] 的数组。
    1. 如果传入的数组 a 足够大,集合会直接将元素放入该数组,并返回。
    2. 如果传入的数组 a 长度不足,toArray 会创建一个新的数组,类型与 a 相同,大小为集合的元素数量。
  • 传入 new Integer[0] 是一种惯用法,表示希望生成一个与集合大小匹配的数组,同时确保数组类型正确。因为传入的数组长度为 0,toArray 不会浪费空间,直接创建一个大小适合的数组。

LeetCode 202 快乐数

题目链接:leetcode.cn/problems/ha…

文档讲解:programmercarl.com/0202.快乐数.ht…

思路

根据题设我们可以知道对于一个正整数n,它的迭代过程有两种

  1. 无限循环
  2. 可以变为1 如果在迭代的过程中出现了曾经出现过的数字,那么n一定会无限循环 因为迭代过程相同,再循环一次也只是按照上一次的路径,而路径中没有1 所以只要出现过已经出现的数,就不是快乐数,返回false。这种“是否出现”的题目可以使用哈希表,且本题只有一类数据,故使用set

从而我们得到如下步骤:

  1. 创建一个HashSet,加入n
  2. 把n替换为它每个位置上的数字的平方和n1
    1. 如果n1存在于set中,返回false
    2. 如果不存在,把n1加入set,继续循环

LeetCode 1 两数之和

题目链接:leetcode.cn/problems/tw…

文档讲解:programmercarl.com/0001.两数之和.h…

视频讲解:www.bilibili.com/video/BV1aT…

思路

对于每个在nums中的num,都有一个对应的数字target-num。只要看target-num是否存在于nums中,存在即返回两者下标

  • 存在👉哈希表
  • 通过值找下标👉HashMap,键为值,值为下标

故有如下步骤:

  1. 创建一个HashMap
  2. 对于每个nums[i]
    1. 在map中查找是否存在键nums[i]
      1. 存在则返回i和键对应的值
      2. 不存在则向map中加入键值对(target-nums[i], i),进入下个循环

因为题设有且只有一个解,在最外侧的返回空值即可

今日收获总结

今日学习3小时,因为前几次都没有去学习哈希的底层结构,这次主要精力放在了对Java中哈希表的底层原理理解上。题目都能独立完成,因为只要知道用哈希,就比较简单~所以对题目着重剖析了考虑到哈希法的心路历程。