leecodecode【面试150】【2026.6.26-7.1打卡-java版本】
二叉树的右视图
要点:层次遍历的套路
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> rightSideView(TreeNode root) {
if(root == null){
return new ArrayList<>();
}
//层次遍历
List<Integer> ans = new ArrayList<>();
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
for(int i = 0; i < size; i++){
TreeNode temp = queue.poll();
if(temp.left != null){
queue.offer(temp.left);
}
if(temp.right != null){
queue.offer(temp.right);
}
if(i == size -1){
ans.add(temp.val);
}
}
}
return ans;
}
}
二叉树的层平均值
同上
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> ans = new ArrayList<>();
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
double sum = 0;
for(int i = 0; i < size; i++){
TreeNode temp = queue.poll();
sum += temp.val;
if(temp.left != null){
queue.offer(temp.left);
}
if(temp.right != null){
queue.offer(temp.right);
}
}
ans.add(sum/size);
}
return ans;
}
}
二叉树的层序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
//队列
if(root == null){
return new ArrayList<>();
}
Deque<TreeNode> deque = new ArrayDeque<>();
List<List<Integer>> anss = new ArrayList<>();
deque.offer(root);
while(!deque.isEmpty()){
int size = deque.size();
List<Integer> ans = new ArrayList<>();
for(int i = 0; i < size; i++){
TreeNode temp = deque.poll();
ans.add(temp.val);
if(temp.left != null){
deque.offer(temp.left);
}
if(temp.right != null){
deque.offer(temp.right);
}
}
anss.add(ans);
}
return anss;
}
}
二叉树的锯齿形层序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
if(root == null){
return new ArrayList<>();
}
//偶数反转
Deque<TreeNode> deque = new ArrayDeque<>();
List<List<Integer>> anss = new ArrayList<>();
//boolean isOuShu = false;
deque.offer(root);
while(!deque.isEmpty()){
int size = deque.size();
List<Integer> ans = new ArrayList<>();
for(int i = 0; i < size; i++){
TreeNode temp = deque.poll();
ans.add(temp.val);
if(temp.left != null){
deque.offer(temp.left);
}
if(temp.right != null){
deque.offer(temp.right);
}
}
if(anss.size() % 2 > 0){
Collections.reverse(ans);
}
anss.add(ans);
}
return anss;
}
}
二叉搜索树的最小绝对差
要点:中序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int getMinimumDifference(TreeNode root) {
//中序遍历
List<Integer> ans = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
//stack.push(root);
while(!stack.isEmpty() || root != null){
while(root != null){
stack.push(root);
root = root.left;
}
TreeNode temp = stack.pop();
ans.add(temp.val);
root =temp.right ;
}
int minDiff = Integer.MAX_VALUE;
for (int i = 1; i < ans.size(); i++) {
minDiff = Math.min(minDiff, ans.get(i) - ans.get(i - 1));
}
return minDiff;
}
}
class Solution {
public int getMinimumDifference(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
Integer prev = null; // 用 null 表示还没有前驱
int minDiff = Integer.MAX_VALUE;
while (!stack.isEmpty() || cur != null) {
// 一路向左
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
// 访问当前节点
cur = stack.pop();
if (prev != null) {
minDiff = Math.min(minDiff, cur.val - prev);
}
prev = cur.val;
// 转向右子树
cur = cur.right;
}
return minDiff;
}
}
二叉搜索树中第 K 小的元素
要点:中序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int kthSmallest(TreeNode root, int k) {
//中层遍历
Deque<TreeNode> stack = new ArrayDeque<>();
while(!stack.isEmpty() || root != null){
while(root != null){
stack.push(root);
root = root.left;
}
TreeNode temp = stack.pop();
k--;
if(k == 0){
return temp.val;
}
root = temp.right;
}
return 0;
}
}
验证二叉搜索树
要点:中序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isValidBST(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<>();
Double pre = -Double.MAX_VALUE;
while(!stack.isEmpty() || root != null){
while(root != null){
stack.push(root);
root = root.left;
}
TreeNode temp = stack.pop();
if(temp.val <= pre){
return false;
}
pre = (double)temp.val;
root = temp.right;
}
return true;
}
}
岛屿数量
要点:bfs
class Solution {
public int numIslands(char[][] grid) {
//bfs
int ans = 0;
int m = grid.length;
int n = grid[0].length;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] == '1'){
dfs(i, j, grid);
ans++;
}
}
}
return ans;
}
public void dfs(int i, int j, char[][] grid){
if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0'){
return;
}
grid[i][j] = '0';
dfs(i+1,j,grid);
dfs(i-1,j,grid);
dfs(i,j+1,grid);
dfs(i,j-1,grid);
}
}
随机知识
HashMap 1.7 vs 1.8 源码对比 + 画图文字描述 + 口述自测答案
一、底层结构核心差异
表格
| 维度 | JDK1.7 HashMap | JDK1.8 HashMap |
|---|---|---|
| 数组节点 | Entry<K,V> 单向链表 |
Node<K,V> 单向链表 + TreeNode 红黑树节点 |
| 链表插入 | 头插法(新节点放链表头部) | 尾插法(新节点追加到链表尾部) |
| 长链表优化 | 无优化,链表无限变长,查询 O (n) | 链表长度≥8 树化为红黑树,查询 O (logn);≤6 退化为链表 |
| 扩容 rehash | 全部节点重新计算 hash,头插倒置链表 | 利用 hash 高位拆分高低链表,尾插保留原有顺序 |
二、为什么从 1.7 改成 1.8?两大核心原因
1. 头插法扩容并发死链(最关键改动动机)
1.7 头插法扩容逻辑
扩容时新建 2 倍长度数组,遍历原链表,每次把当前节点插到新链表头部,链表顺序完全反转。 并发场景下,两个线程同时触发扩容:
- 线程 A 遍历链表
A→B→null,刚处理完 A,时间片切走; - 线程 B 完整完成扩容,新链表变成
B→A→null; - 线程 A 恢复执行,继续拿 B 节点,头插后形成
A→B,B 的 next 又指向 A,环形链表; - 后续
get()、containsKey()遍历链表无限循环,CPU 100%。
1.8 尾插法解决死链
扩容时节点追加到高低链表尾部,原有节点相对顺序不变,不会倒置链表,并发扩容不会产生循环引用,彻底根除环形链表问题。
2. 长链表查询性能太差,引入红黑树优化
1.7 哈希冲突严重时,一条链表挂载几十上百节点,查询需要从头到尾遍历,时间复杂度 O (n); 1.8 当链表过长转为红黑树,查找、插入、删除 O (logn),大幅降低高冲突下查询耗时。
三、图文还原:1.7 环形链表形成过程(文字绘图,可直接手绘)
初始状态:原数组桶内链表 A -> B -> null,扩容 newTab 长度翻倍
- 线程 A 执行 transfer,指针
e = A,next = B,还未迁移 A,被挂起; - 线程 B 完整执行扩容迁移:
- 迁移 B:newTab 链表
B - 迁移 A:头插到 B 前面,newTab 链表
A -> B -> null
- 迁移 B:newTab 链表
- 线程 A 恢复执行,当前 e=A,next=B:
- 将 A 头插入新链表:
A - e 更新为 next=B,B 不为 null,继续迁移 B
- B 的 next 原本是 null,但线程 B 已经把 B.next 指向 A
- 头插 B 到链表头部,得到
B -> A,A.next = B
- 将 A 头插入新链表:
- 最终形成环:
A ↔ B,无限循环
手绘简记:
plaintext
原始链表:A → B → null
线程B扩容后:A → B → null
线程A继续处理:B.next=A,A.next=B 环形闭环
四、1.8 红黑树插入流程图(文字版手绘)
- 新 Node 计算 hash,定位数组下标桶
- 判断桶首节点:
- 桶为空:直接放数组,结束
- 桶不为空,key 相等:覆盖旧值,结束
- 桶是链表节点:
- 循环遍历链表尾部,同步统计链表长度
- 找到同 key 则覆盖;没找到则追加到链表尾部(尾插)
- 追加完成后,判断链表长度
len >= 8:执行treeifyBin()树化
- treeifyBin 流程:
- 判断数组长度是否小于 64:不树化,直接扩容(扩容打散冲突更高效)
- 链表节点转为 TreeNode 双向链表
- 双向链表构建红黑树,平衡染色、左旋右旋调整树结构
- 桶已经是红黑树根节点:
- 树中查找 key,匹配则覆盖
- 无匹配则插入新 TreeNode,自动平衡红黑树
流程图简化结构:
plaintext
计算hash定位桶
↓
桶空?→ 直接存入
↓否
首节点key相等?→ 覆盖
↓否
首节点是TreeNode?→ 红黑树插入+平衡
↓否(普通链表)
遍历链表尾部尾插,计数长度
↓
长度≥8 && tab容量≥64 → 链表转红黑树
↓
长度≥8 && tab容量<64 → 扩容打散
五、自测口述标准答案:为什么阈值是 8 转树、6 退链,不是 6 转树?
从概率、性能、红黑树开销三个层面解释:
-
哈希分布概率依据 HashMap 默认 hash 扰动算法下,单个链表节点数量符合泊松分布。链表长度达到 8 的概率极低,仅千万分之一,正常业务几乎不会出现长链表;如果阈值设为 6,轻微哈希冲突就频繁树化,完全没必要。
-
树化 / 退树的开销平衡 红黑树节点 TreeNode 比普通 Node 内存占用大很多,存储父、左右、颜色、前驱后继指针,维护平衡树还要左旋、右旋、染色,插入删除开销远高于普通链表。 如果阈值设 6,少量冲突就频繁转树,内存与计算开销陡增;等到链表长度到 8,线性遍历性能衰减已经非常明显,此时树化收益远大于开销。
-
6 作为退链阈值,防止频繁切换震荡 树化阈值 8、退化阈值 6,中间留出差值缓冲。如果进树和退树阈值相同(比如都是 8),当链表节点在 8 个左右浮动时,会反复执行树化、退链,频繁转换结构造成性能抖动。设置 6 作为退化阈值,预留缓冲区间,避免频繁切换数据结构。
-
链表与红黑树查询性能拐点 链表长度小于 8 时,顺序遍历的 CPU 缓存连续访问效率很高,O (n) 遍历速度并不慢;超过 8 个节点后,线性遍历耗时显著上升,红黑树 O (logn) 的优势才能体现出来,因此选择 8 作为树化临界点。
总结:8 是兼顾哈希概率、查询性能、结构转换开销的最优临界点,6 作为退化阈值缓冲,防止结构频繁震荡。
碎碎念:后续会更新每天学习的八股和算法 题,开始准备秋招的第46/7/8/9/50/【身体原因休息五天】51天。努力连续更新100天!以后每天就按,秋招项目【java +agent】,科研,必做项目,算法,八股,锻炼身体来总结。
总结:坚持,不管干什么都得坚持
1.算法面试150 97/150 2h
2.秋招项目,【java 项目】,
【agent 项目 】,
3.科研要跑一下,
4.检测项目,4h
6.背八股,
7.锻炼身体,
坚持,不完美也开始,坚定目标学习,心平气和。
更多推荐
所有评论(0)