public List<Integer> findAnagrams(String ss, String pp) {
List<Integer> ret = new ArrayList<>();
char[] s = ss.toCharArray();
char[] p = pp.toCharArray();
int[]hash1 = new int[26];
for (char ch : p) {
hash1[ch - 'a']++;
}
int[]hash2 = new int[26];
int m=p.length;
for(int left=0,right=0,count=0;right<s.length;right++){
char in=s[right];
if(++hash2[in-'a']<=hash1[in-'a'])count++;
//进窗口+维护count
if(right-left+1>m)//判断要不要滑出窗口
{
char out=s[left++];
if(hash2[out-'a']--<=hash1[out-'a'])count--;
//出窗口+维护窗口
}
if(count==m)ret.add(left);
}
return ret;
}
public List<Integer> findSubstring(String s, String[] words) {
//与上一题不同地方1. 哈希表,hashmap<string,int>2. left 和right 指针移动的步长是单词长度3. 滑动窗口的次数不同
//需要练习容器的使用!超级不会(用ai问,练中学,将算法转为代码)
List<Integer> ret = new ArrayList<>();
//hash1用来保存字典中所有单词的频次
Map<String, Integer> hash1 = new HashMap<>();
for (String str : words) {hash1.put(str, hash1.getOrDefault(str, 0) + 1);}
int len=words[0].length();//单词长度
int m=words.length;//单词个数
for(int i=0;i<len;i++){//窗口执行次数
Map<String, Integer> hash2 = new HashMap<>();//保存窗口内所有单词的频次
for(int left=0,right=0,count=0;right+len<=s.length();right+=len){
//进窗口+维护count
String in=s.substring(right,right+len);
//这个方法我不会啊,存在则返回in+1,不存在就0+1,问ai
hash2.put(in, hash2.getOrDefault(in, 0) + 1);
if(hash2.get(in)<=hash1.getOrDefault(in, 0) + 1){//如果是get方法会报错用存在返回值
count++;
}
if(right-left+1>m*len){
String out=s.substring(left,left+len);
if(hash2.get(out)<=hash1.getOrDefault(out,0) + 1){
count--;
}
hash2.put(out, hash2.getOrDefault(out,0) + 1);
left+=len;
}
if(count==m)ret.add(left);
}
}
return ret;
}
关于这两天的题,lsq提出几个很笨的问题
1hash的数组充当hash表怎么做到的
2map和set的区别还有treemap treeset hashmap hashset 这六个有什么区别
3在new对象的时候 List<Integer> ret = new ArrayList<>();和 List<Integer> ret = new ArrayList<Interger>();区别
4了解一下容器的内置方法
一、int[] hash是怎么"充当哈希表"的?
以你的 **findAnagrams** 为例:
int[] hash1 = new int[26];
for (char ch : p) {
hash1[ch - 'a']++;
}
1️⃣ 本质思想
这是一个 "用数组实现的哈希表"。
| 字符 | 映射方式 | 数组下标 |
|---|---|---|
| 'a' | 'a' - 'a' | 0 |
| 'b' | 'b' - 'a' | 1 |
| ... | ... | ... |
| 'z' | 'z' - 'a' | 25 |
👉 字符 → 数字(索引) → 直接访问
2️⃣ 为什么算哈希表?
哈希表的本质是:
key → value 的快速映射
这里:
- key:字符
'c' - hash 函数:
ch - 'a' - value:出现次数
✅ O(1) 查找
✅ 不需要 HashMap的开销
3️⃣ 什么时候能用这种方式?
✅ 字符集固定(如小写字母、ASCII)
❌ 不适合字符串、对象、动态 key
对比一下👇
| 方式 | 示例 |
|---|---|
| 数组哈希 | int[] count = new int[26] |
| HashMap | Map<Character, Integer> |
| HashSet | Set<Character> |
所以hash数组是存储出现的次数,是key
一、先给结论 ✅
**
int[] hash既不是HashMap,也不是HashSet。**它更像是"手写版 HashMap(只有 value 的简化版)"
但它 绝对不是 HashSet。
二、为什么它不是 HashSet?
1️⃣ HashSet是什么?
Set<Character> set = new HashSet<>();
set.add('a');
set.contains('a');
HashSet的特点是:
| 特性 | HashSet |
|---|---|
| 存什么 | 单个元素 |
| 是否允许重复 | ❌ |
| 是否有"次数"概念 | ❌ |
| 是否关心频率 | ❌ |
👉 HashSet 只关心:有没有
2️⃣ 你的 int[] hash在干嘛?
int[] hash1 = new int[26];
hash1['a' - 'a']++; // a 出现了几次
它在干的事是:
✅ 统计 出现次数
✅ 一个字符 → 一个数字(频率)
✅ 关心数量,不只是"有没有"
👉 这和 HashSet的语义 完全不一样
三、那它是不是"没有 key"?
你说得很关键的一句话 👇
"因为它没有 key 值"
其实它有 key,只是你看不见而已 😄
真正的对应关系
| 字符 | key | value |
|---|---|---|
| 'a' | 0 | hash[0] |
| 'b' | 1 | hash[1] |
| 'c' | 2 | hash[2] |
👉 **数组下标 = key(隐式)**
👉 数组值 = value
所以它其实是:
"用数组实现的 HashMap<Character, Integer>"
四、三种东西放在一起对比(重点)
| 方式 | key | value | 能否统计次数 |
|---|---|---|---|
int[] hash |
下标(隐式) | 出现次数 | ✅ |
HashSet |
无 | 无 | ❌ |
HashMap<Char, Int> |
字符 | 次数 | ✅ |
举个直观例子(统计 "aab")
✅ int[]
[2, 1, 0, 0, ...]
✅ HashMap
{'a':2, 'b':1}
❌ HashSet
{'a', 'b'}
👉 HashSet 连"2 次 a"都记不住
五、那为什么不用 HashSet 做异位词?
假设我们这样写:
Set<Character> set = new HashSet<>();
set.add('a');
set.add('a');
结果只剩:
{'a'}
❌ 根本没法判断是不是异位词
✅ 必须知道 每个字符出现几次
所以 必须用"计数结构"
六、什么时候用谁?(经验总结)
| 场景 | 用谁 |
|---|---|
| 字符 / 数字频率统计 | int[]/ HashMap |
| 判断是否出现过 | HashSet |
| 键值对映射 | HashMap |
| 去重 | HashSet |
| 字符集固定(a-z) | ✅ int[26](最快) |
七、一句话终极记忆 🧠
HashSet 管"有没有",HashMap 管"是多少",数组是手写版 HashMap。
二、Mapvs Set(先搞清楚"是谁")
1️⃣ 最根本区别
| 类型 | 存什么 | 是否键值对 |
|---|---|---|
Map |
key → value | ✅ 是 |
Set |
单个元素 | ❌ 否 |
一句话记住:
Map 管配对,Set 管去重
三、HashMap / TreeMap / HashSet / TreeSet六者区别
我用一张对比表讲清楚 👇
| 容器 | 是否键值对 | 底层结构 | 是否有序 | 是否允许 null | 特点 |
|---|---|---|---|---|---|
HashMap |
✅ | 哈希表 | ❌ 无序 | ✅ key 可为 null | 最快 |
TreeMap |
✅ | 红黑树 | ✅ 按 key 排序 | ❌ | 自动排序 |
LinkedHashMap |
✅ | 哈希 + 链表 | ✅ 插入顺序 | ✅ | 记录顺序 |
HashSet |
❌ | HashMap |
❌ | ✅ | 去重 |
TreeSet |
❌ | TreeMap |
✅ 排序 | ❌ | 去重 + 排序 |
LinkedHashSet |
❌ | 哈希 + 链表 | ✅ 插入顺序 | ✅ | 去重 + 顺序 |
记忆口诀 🧠
HashMap 快,TreeMap 有序,HashSet 去重,TreeSet 又去重又有序
此处lsq插播一个问题,map是不是hashmap缩写了
一、结论先行 ✅
Map不是HashMap的缩写
Map是接口,HashMap是实现类
它们是 父子关系(设计上的),不是简称关系。
二、用一句话解释
| 名字 | 是什么 |
|---|---|
Map |
**接口(规定能干嘛)** |
HashMap |
**具体实现(实际干活的类)** |
就像:
| 现实世界 | Java 世界 |
|---|---|
| "车" | Map |
| "丰田 / 宝马" | HashMap / TreeMap |
三、看你的代码就明白了
Map<String, Integer> hash1 = new HashMap<>();
拆解来看:
Map<K, V> ← 接口(你"用谁")
↓
HashMap<>() ← 实现类(你"怎么干")
✅ 好处:
以后你想换实现,一行代码都不用改
Map<String, Integer> map = new TreeMap<>(); // 照样能用
四、为什么不直接写 HashMap?
你也可以写:
HashMap<String, Integer> map = new HashMap<>();
❌ 但不推荐,原因是:
| 写法 | 问题 |
|---|---|
HashMap声明 |
被"锁死"在 HashMap |
Map声明 |
可以随时换实现 |
✅ 面向接口编程(面试必考点)
五、同样的套路,你早就见过 👇
1️⃣ List / ArrayList
List<Integer> list = new ArrayList<>();
| 接口 | 实现 |
|---|---|
List |
ArrayList |
2️⃣ Set / HashSet
Set<Integer> set = new HashSet<>();
| 接口 | 实现 |
|---|---|
Set |
HashSet |
六、对照表(非常重要)✅
| 接口(Interface) | 常见实现类 |
|---|---|
Map |
HashMap, TreeMap, LinkedHashMap |
List |
ArrayList, LinkedList |
Set |
HashSet, TreeSet, LinkedHashSet |
七、记忆口诀 🧠
**接口是"能干嘛"**
**实现类是"怎么干"**
Map ≠ HashMap,就像 车 ≠ 丰田
八、结合你现在的水平,我建议你记住这一条就够了 ✅
写代码时:
Map<K, V> map = new HashMap<>(); List<T> list = new ArrayList<>(); Set<T> set = new HashSet<>();
等你哪天想换 TreeMap,直接改右边就行。
Java接口继承图

Pasted image 20260429114024.png
四、List<Integer> ret = new ArrayList<>();
vs
List<Integer> ret = new ArrayList<Integer>();
✅ 完全等价,没有任何性能或行为差异
区别只是 Java 版本写法:
| 写法 | Java 版本 |
|---|---|
new ArrayList<Integer>() |
Java 5--6 |
new ArrayList<>() |
Java 7+(菱形语法) |
编译器会自动推断 <Integer>。
📌 推荐写法:
List<Integer> ret = new ArrayList<>();
五、容器常用内置方法(重点!结合你的代码)
我按 你代码里出现的 + 高频必会 来讲。
✅ 1️⃣ List常用方法(ArrayList)
| 方法 | 作用 |
|---|---|
add(e) |
添加元素 |
get(i) |
取第 i 个 |
size() |
元素个数 |
clear() |
清空 |
contains(e) |
是否包含 |
你的代码:
List<Integer> ret = new ArrayList<>();
ret.add(left);
✅ 2️⃣ Map常用方法(HashMap)
| 方法 | 作用 |
|---|---|
put(k, v) |
插入 |
get(k) |
取值 |
getOrDefault(k, def) |
有就取,没有给默认值 |
containsKey(k) |
是否含 key |
size() |
键值对数 |
clear() |
清空 |
你代码里的关键方法👇
hash1.getOrDefault(str, 0) + 1
意思是:
如果有这个 key,就拿它的值;没有就从 0 开始
✅ 3️⃣ Set常用方法(HashSet)
| 方法 | 作用 |
|---|---|
add(e) |
添加 |
contains(e) |
是否存在 |
remove(e) |
删除 |
size() |
元素个数 |
✅ 4️⃣ 数组 / 字符串相关(你代码里混用的)
| 方法 | 作用 |
|---|---|
toCharArray() |
String → char[] |
substring(l, r) |
截取字符串 |
s.length() |
字符串长度 |
arr.length |
数组长度 |
⚠️ 注意:
s.length(); // String
arr.length; // 数组
list.size(); // List
六、你这两段代码里"真正在用的容器能力"
findAnagrams
- ✅ 数组当哈希表
- ✅ O(1) 统计频率
- ✅ 滑动窗口 + 计数器
findSubstring
- ✅
HashMap<String, Integer> - ✅
getOrDefault - ✅ 多起点滑动窗口
七、下一步我可以怎么帮你?
你现在的问题已经非常"对路"了 👍
接下来你可以选一个继续:
1️⃣ 我帮你 把 findSubstring改成正确版本,并逐行手推
2️⃣ 专门练 **Map / Set 高频 API(配小练习)**
3️⃣ 总结一份 **Java 容器速查表(面试用)**
4️⃣ 用 数组 vs HashMap 对比讲一道新题
你想先走哪一步?我陪你慢慢啃 😊
总结
1. 将算法思想变成代码的能力很重要(自己要去实现一遍)
2. 对于内置方法,要掌握