今日任务
● 哈希表理论基础
● 242.有效的字母异位词
● 349. 两个数组的交集
● 202. 快乐数
● 1. 两数之和
详细布置
哈希表理论基础
建议:大家要了解哈希表的内部实现原理,哈希函数,哈希碰撞,以及常见哈希表的区别,数组,set 和map。
什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。 这句话很重要,大家在做哈希表题目都要思考这句话。
文章讲解:代码随想录
总结:
数据结构:
- Java 中 HashMap 主要由数组和链表(在 Java 8 后还有红黑树)组成。数组被称为哈希桶(hashTable),用于存储键值对的链表或红黑树的头节点引用
哈希函数:
- 通过哈希函数计算键的哈希值,然后对数组长度取模,得到在数组中的索引位置,以此来决定键值对应该存储在哪个哈希桶中
扩容机制:
- 当 HashMap 中的元素数量超过负载因子(默认 0.75)与当前容量的乘积时,会触发扩容操作。扩容时会创建一个新的、更大的数组,并将原有数据重新哈希到新数组中
键值存储与查找:
- 存储键值对时,将其放入对应的哈希桶中,如果发生哈希冲突(多个键映射到同一个索引位置),则在链表中依次添加节点(在 Java 8 后,如果链表长度超过一定阈值会转换为红黑树)
- 查找键值对时,通过相同的哈希函数计算键的索引位置,然后在对应的链表或红黑树中进行查找
线程不安全:
- HashMap 在并发环境下可能会出现数据丢失、数据覆盖等问题,不适合多线程环境下直接使用,如果需要在多线程环境中使用,可以使用 ConcurrentHashMap 等线程安全的类。
线程安全问题的本质原因:
1>结构的并发修改问题
同时进行扩容操作:
HashMap 在进行扩容时,需要重新计算已有元素的哈希值,并将它们迁移到新的数组位置。如果多个线程同时进行扩容操作,可能会导致数据丢失或者迁移错误。
节点插入和链接的混乱:
在正常情况下,当向 HashMap 中插入一个新的键值对时,需要根据哈希值找到对应的链表位置,并将新节点插入到链表中。如果多个线程同时进行插入操作,可能会导致节点链接的顺序错误,或者出现循环链表的情况。
2>数据不一致问题
读写冲突:
一个线程正在读取 HashMap 的数据,而另一个线程正在对其进行写入操作(例如添加、删除或修改键值对)。在没有适当的同步机制的情况下,读取线程可能会获取到不一致的数据状态。
哈希表状态的不一致:
HashMap 的内部状态包括哈希桶数组的大小、每个链表的结构以及元素的分布等。在多线程环境下,这些状态可能会被多个线程同时修改,导致哈希表的状态变得不可预测和不一致。
3>没有同步机制来保证原子性和可见性
原子性操作的缺失:
许多对 HashMap 的操作,如 put ()、remove () 等,实际上是由多个步骤组成的复合操作。在多线程环境下,这些操作需要保证原子性,即要么全部完成,要么完全不做,以避免中间状态被其他线程看到。
可见性问题:
由于每个线程都有自己的本地缓存(工作内存),线程对 HashMap 的修改可能不会立即被其他线程看到。这就可能导致其他线程读取到过期的数据。
242.有效的字母异位词
建议: 这道题目,大家可以感受到 数组 用来做哈希表 给我们带来的遍历之处。
题目链接:. - 力扣(LeetCode)
题目链接/文章讲解/视频讲解: 代码随想录
暴力解:
public boolean isAnagram(String s, String t) {
boolean res = false;
char[] charArr_s = s.toCharArray();
//仅包含小写字符
Map<Character,Integer> map = new HashMap<>(26);
for(Character c:charArr_s){
map.put(c,map.getOrDefault(c,0)+1);
}
char[] charArr_t = t.toCharArray();
for(Character c:charArr_t){
if(map.containsKey(c)){
int cnt = map.get(c)-1;
if(cnt <= 0){
map.remove(c);
}else{
map.put(c,cnt);
}
}else{
return res;
}
}
return map.size() == 0;
}
字典解
public boolean isAnagram(String s, String t) {
//为什么可以用数组来做字典:
// "s 和 t 仅包含小写字母" 和 "进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?" 已经提示了
int[] arr = new int[26];
for(int i=0;i<s.length();i++){
arr[s.charAt(i)-'a']++;
}
for(int i=0;i<t.length();i++){
arr[t.charAt(i)-'a']--;
}
for(int a:arr){
if(a!=0){
return false;
}
}
return true;
}
349. 两个数组的交集
建议:本题就开始考虑 什么时候用set 什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和 测试用例,添加了 0 <= nums1[i], nums2[i] <= 1000 条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用set。
题目链接/文章讲解/视频讲解:代码随想录
暴力解:
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set = new HashSet(nums1.length);
for(Integer n : nums1){
set.add(n);
}
Set<Integer> res = new HashSet();
for(Integer n : nums2){
if(set.contains(n)){
res.add(n);
}
}
//将结果集合转为数组
return res.stream().mapToInt(x -> x).toArray();
}
利用限制求解:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
这里限制 length 也限制了 值的大小, 都 <=1000 ,那么就提供了, 数组与值的转化土壤,即可以利用这个特性来快速标记数组某元素是否存在的状态
public int[] intersection(int[] nums1, int[] nums2) {
int[] flag = new int[1001];
for(int i = 0; i < nums1.length; i++)
if(flag[nums1[i]] == 0)
flag[nums1[i]] = 1;
int[] ans = new int[1001];
int idx = 0;
for(int i = 0; i < nums2.length; i++)
if(flag[nums2[i]] == 1){
ans[idx++] = nums2[i];
flag[nums2[i]] = 2;
}
return Arrays.copyOf(ans, idx);
}
202. 快乐数
建议:这道题目也是set的应用,其实和上一题差不多,就是 套在快乐数一个壳子
题目链接:. - 力扣(LeetCode)
题目链接/文章讲解:代码随想录
利用字符串求解
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<>();
//如果n之前已经出现了,说明会出现死循环,即不是快乐数了,这时就需要返回了
while(n!=1 && !set.contains(n)){
set.add(n);
//重置n的值
n = getSum(n);
}
return n == 1;
}
public int getSum(int n){
int sum = 0;
String str = String.valueOf(n);
for(int i=0;i<str.length();i++){
int tmp = Integer.valueOf(String.valueOf(str.charAt(i)));
sum += tmp * tmp;
}
return sum;
}
利用每次n/10 来累加各位置的和
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<>();
//如果n之前已经出现了,说明会出现死循环,即不是快乐数了,这时就需要返回了
while(n!=1 && !set.contains(n)){
set.add(n);
//重置n的值
n = getSum(n);
}
return n == 1;
}
public int getSum(int n){
int sum = 0;
while(n>0){
//每次取最后一位
int tmp = n%10;
sum += tmp * tmp;
//重置n的值
n = n/10;
}
return sum;
}
核心:
缺少对数字处理的常识
从个位后高位依次取数
获取最后一位数组,用n%10,取余即可,忽略最后一位数字,则用n/10 取整即可
tmp = n%10; 每次取个位
n = n /10; 数字忽略个位,缩小10倍,位数整体减1
从高位后往个位依次取数
用栈依次入栈之后再取即可
int sum = 0;
Stack<Integer> stack = new Stack();
while (n>0){
stack.push(n%10);
n = n/10;
}
//依次获取最高位数字
stack.pop();
也可以直接用字符串的方式取(相对占用内存较多)
数字的位运算
- 高效的乘除 2 运算: n乘以 2 可写为: n<<1 ; n除以 2 可写为: n>>1
- 快速判断奇偶性: 与数字
1
进行按位与(&
)操作,如果结果为0
,则该数为偶数;如果结果为1
,则该数为奇数; 7 & 1 = 1 为奇数;8 & 1 = 0 为偶数
1. 两数之和
建议:本题虽然是 力扣第一题,但是还是挺难的,也是 代码随想录中 数组,set之后,使用map解决哈希问题的第一题。
建议大家先看视频讲解,然后尝试自己写代码,在看文章讲解,加深印象。
题目链接:. - 力扣(LeetCode)
题目链接/文章讲解/视频讲解:代码随想录
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
//记录 值是key 即对应的索引的位置 为val
Map<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length;i++){
if(map.containsKey(target-nums[i])){
res[0] = i;
res[1] = map.get(target-nums[i]);
break;
}
map.put(nums[i],i);
}
return res;
}
- 为什么会想到用哈希表
- 记录已表里的数组,避免重复遍历
- 哈希表为什么用map
- 因为返回的结果是索引位置,所以同步记录 值 及 索引
- 本题map是用来存什么的
- key: 数组元素值 val: 元素再数组的中偏移量(索引)