哈希表基础
解决的问题:一般哈希表都是用来快速判断一个元素是否出现集合里。
哈希碰撞:拉链法和线性探测法。
常见的三种哈希结构:数组,集合set,映射map
Hashmap的用法:键值对存储。键唯一,值可重复。允许一个 null 键和多个 null 值。不保证顺序(Java 8 后按插入顺序大致保持)
java
import java.util.HashMap;
import java.util.Map;
// 创建
Map<String, Integer> map = new HashMap<>();
// 添加/更新元素
map.put("apple", 1); // 添加
map.put("apple", 2); // 更新
map.put("banana", 3);
map.put("cherry", 4);
// 获取元素
int value = map.get("apple"); // 2
Integer val = map.get("orange"); // null(不存在)
// 检查键是否存在
boolean hasApple = map.containsKey("apple"); // true
boolean hasOrange = map.containsKey("orange"); // false
// 遍历
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 获取键集合、值集合
Set<String> keys = map.keySet();
Collection<Integer> values = map.values();
// 删除
map.remove("apple");
HashSet的用法:去重集合,快速判断元素是否存在
java
import java.util.HashSet;
import java.util.Set;
Set<Integer> set = new HashSet<>();
// 添加元素
set.add(1);
set.add(2);
set.add(3);
set.add(1); // 重复,不会添加
// 检查元素是否存在
boolean contains = set.contains(2); // true
// 删除元素
set.remove(2);
// 遍历
for (int num : set) {
System.out.println(num);
}
// 获取大小
int size = set.size();
// 清空
set.clear();
LinkedHashMap(保持插入顺序):适合需要顺序的 LRU 缓存实现。
java
import java.util.LinkedHashMap;
import java.util.Map;
Map<String, Integer> map = new LinkedHashMap<>();
// 添加元素
map.put("z", 1);
map.put("a", 2);
map.put("b", 3);
// 遍历会按插入顺序
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey()); // 输出顺序:z, a, b
}
1两数之和
题目:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。可以按任意顺序返回答案。
这个题的思路是,利用hashmap来存已经遍历过的数组元素,比如我遍历到了元素3,target=9,那么我就要找,hashmap里面是否已经添加过target-3这个元素。如果已经有,那么就返回这两个的下表。
这题要想清楚四个点:
为什么会想到用哈希表?什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候。这里也就是要找target-3这个元素。
哈希表为什么用map?本题不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放。
本题map是用来存什么的?map用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)
map中的key和value用来存什么的?key存数组元素,value存数组下标。因为本题给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标,下标是要寻找的东西。
java
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result=new int[2];
Map<Integer,Integer> map= new HashMap<>(); //存遍历过的数组元素及其下标
if(nums == null || nums.length == 0){
return result;
}
for(int i=0;i<nums.length; i++){
int temp=target-nums[i];
if(map.containsKey(temp)){
result[0]=i;
result[1]=map.get(temp);
break;
}
map.put(nums[i],i);
}
return result;
}
}
之前没有注意过map.put(nums[i],i);和if的位置关系。如果先put再if会有一个元素用两次的问题。
49字母异位词分组
题目:给你一个字符串数组,请你将字母异位词组合在一起。可以按任意顺序返回结果列表。
示例 1:输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
解释:在 strs 中没有字符串可以通过重新排列来形成 "bat"。
字符串 "nat" 和 "tan" 是字母异位词,因为它们可以重新排列以形成彼此。
字符串 "ate" ,"eat" 和 "tea" 是字母异位词,因为它们可以重新排列以形成彼此。
这个题在242有效的字母异位词做过一个简单版本的。
核心思想:利用每个字符串的字符计数来生成一个唯一的 key,然后将签名相同的字符串放在同一个列表中。
java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String,List<String>> map=new HashMap<String,List<String>>();
for(String str:strs){
int[] counts=new int[26];
int len=str.length();
for(int i=0;i<len;i++){ // 遍历当前字符串的每个字符,统计每个字母出现的次数
counts[str.charAt(i)-'a']++;
}
StringBuffer sb=new StringBuffer(); // 使用 StringBuffer 来高效地拼接字符串,因为字符串拼接操作较多
for(int i=0;i<26;i++){// 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串,作为哈希表的键
if(counts[i]!=0){
sb.append((char)('a'+i));
sb.append(counts[i]);
}
}
String key=sb.toString(); //拼接后作为key
List<String> list=map.getOrDefault(key,new ArrayList<String>()); //key对应的是所有异位词的list
list.add(str);
map.put(key,list);
}
return new ArrayList<List<String>>(map.values());
}
}
过程模拟:生成完key之后是怎么处理的。
String[] strs = {"eat", "tea", "tan"};
第 1 步:处理第一个字符串 "eat"
统计字符次数得到 counts:a:1, e:1, t:1。
拼接成 key:"a1e1t1"。
执行 List list = map.getOrDefault(key, new ArrayList());
此时 map 为空,所以 getOrDefault 返回一个新创建的 ArrayList(假设叫 list1)。
list.add(str) → list1 现在包含 ["eat"]。
map.put(key, list) → 将 key="a1e1t1" 与 list1 放入 map。
此时 map 的内容为:{ "a1e1t1" : ["eat"] }
第 2 步:处理第二个字符串 "tea"
统计字符次数:a:1, e:1, t:1,生成的 key 同样是 "a1e1t1"。
执行 List list = map.getOrDefault(key, new ArrayList());
因为 map 中已存在 key="a1e1t1",所以 getOrDefault 返回之前存储的列表,即 ["eat"](我们称它为 list1 的引用)。
list.add(str) → 向这个列表中添加 "tea",现在该列表变为 ["eat", "tea"]。
map.put(key, list) → 将 key="a1e1t1" 与更新后的列表(仍然是同一个列表对象)重新放入 map。
此时 map 的内容更新为:{ "a1e1t1" : ["eat", "tea"] }
第 3 步:处理第三个字符串 "tan"
统计字符次数:a:1, n:1, t:1,生成的 key 为 "a1n1t1"(与之前不同)。
执行 List list = map.getOrDefault(key, new ArrayList());
map 中没有 "a1n1t1",所以返回一个新创建的 ArrayList(叫 list2)。
list.add(str) → list2 添加 "tan",变为 ["tan"]。
map.put(key, list) → 将 key="a1n1t1" 与 list2 放入 map。
最终 map 的内容:
{ "a1e1t1" : ["eat", "tea"], "a1n1t1" : ["tan"] }
语法知识补充:
StringBuffer
StringBuffer 是一个可变的字符序列,适合在循环中频繁拼接字符串。相比直接用 + 拼接(每次都会创建新的字符串对象),StringBuffer 效率更高。这里先用 append 方法逐个添加字符和数字,最后调用 toString() 得到最终的 String 对象作为 key。
map.getOrDefault(key, defaultValue)
这是一个方便的方法:如果 map 中存在 key,则返回对应的 value;否则返回指定的默认值(这里是一个新的 ArrayList)。这样可以避免先判断 containsKey 再手动创建列表的繁琐步骤。
map.put(key, list)
将 key 和对应的列表存入 map。如果 key 已经存在,新的 list 会覆盖旧的,但由于我们是从 map 中取出原来的 list 并添加了元素,再 put 回去,所以实际上更新了 map 中该键对应的列表内容。
return new ArrayList<List>(map.values())
map.values() 返回 map 中所有 value 的集合(Collection<List>)。通过 ArrayList 的构造方法,可以将这个集合转换成一个新的 ArrayList 并返回。注意这里使用了泛型,ArrayList<List> 表示这个列表的元素类型是 List。
128最长连续序列
题目:给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:输入:nums = [100,4,200,1,3,2],输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
使用哈希集合
java
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> st=new HashSet<>();
for(int num:nums){
st.add(num);
}
int ans=0;
for(int x:st){
if(st.contains(x-1)){
continue;
}
int y=x+1;
while(st.contains(y)){
y++;
}
ans=Math.max(ans,y-x);
}
return ans;
}
}