前 K 个高频元素
要点:桶排序
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 1. 频率统计:O(n)
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
// 2. 创建桶数组:索引是频率,值是该频率对应的数字列表
List<Integer>[] bucket = new List[nums.length + 1]; // 因为频率最大为 n
for (int num : freqMap.keySet()) {
int freq = freqMap.get(num);
if (bucket[freq] == null) {
bucket[freq] = new ArrayList<>();
}
bucket[freq].add(num);
}
// 3. 从高频率向低频率收集结果
int[] res = new int[k];
int index = 0;
for (int i = bucket.length - 1; i >= 0 && index < k; i--) {
if (bucket[i] != null) {
for (int num : bucket[i]) {
res[index++] = num;
if (index == k) break;
}
}
}
return res;
}
}
单词拆分
要点:的动态规划,两个for
java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
//用set存储
Set<String> wordSet = new HashSet<>(wordDict);
//
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for(int i = 1; i<=s.length(); i++){
for(int j = 0 ; j < i; j++){
if(dp[j] && wordSet.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
最长递增子序列
要点:动态规划。两个for
java
class Solution {
public int lengthOfLIS(int[] nums) {
//动态规划
int[] dp = new int[nums.length];
int max = 0;
//dp[0] =0;
for(int i =0; i < nums.length ; i++ ){
dp[i] = 1;
for(int j = 0; j <=i; j++){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[i],dp[j] +1);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
}
最小路径和
要点:二维dp
java
class Solution {
public int minPathSum(int[][] grid) {
//二维dp,注意n,m
int m = grid[0].length;
int n = grid.length;
int[][] dp = new int[n][m];
dp[0][0] = grid[0][0];
for(int i = 1; i < m; i++){
dp[0][i] = grid[0][i] + dp[0][i-1];
}
for(int j = 1; j <n; j++){
dp[j][0] =grid[j][0] + dp[j-1][0];
}
for(int i = 1; i < n; i++){
for(int j = 1; j < m; j++){
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[n-1][m-1];
}
}
分割等和子集
要点:0-1背包,逆序
java
class Solution {
public boolean canPartition(int[] nums) {
int sum =0 ;
for(int i = 0; i < nums.length; i++){
sum += nums[i];
}
if(sum % 2 != 0){
return false;
}
int target = sum/2;
for(int i = 0; i < nums.length; i++){
if( nums[i] > target){
return false;
}
}
boolean[] dp = new boolean[target+1];
dp[0] =true;
for(int num : nums){
for(int i = target; i >= num; i--){
dp[i] = dp[i] || dp[i-num];
}
}
return dp[target];
}
}
颜色分类
要点:灵神的全是2, 部分1,然后0
java
class Solution {
public void sortColors(int[] nums) {
int p0 =0;
int p1 = 0;
for(int i = 0; i < nums.length; i++){
int temp = nums[i];
nums[i] = 2;
if(temp <= 1){
nums[p1] = 1;
p1++;
}
if(temp == 0){
nums[p0] = 0;
p0++;
}
}
}
}
随机知识
1. 数组 / ArrayList ------ 固定列表、按位置访问
场景:学生成绩列表、排行榜,需要按索引取数据、遍历。
java
import java.util.ArrayList;
import java.util.List;
// 项目中的 DTO
class StudentScore {
String name;
int score;
// 构造器、getter 略
}
public class ArrayListDemo {
public static void main(String[] args) {
List<StudentScore> scores = new ArrayList<>();
scores.add(new StudentScore("张三", 92));
scores.add(new StudentScore("李四", 85));
// 按索引快速取
StudentScore first = scores.get(0);
// 遍历(项目中常结合 Stream 做过滤/统计)
scores.stream()
.filter(s -> s.score > 90)
.forEach(s -> System.out.println(s.name));
}
}
项目怎么用 :从数据库查出的多条记录,就是封装成 List<Entity> 返回给前端;Excel 导入的数据行也存成 ArrayList 统一校验。
2. LinkedList ------ 需要频繁增删的头尾操作
场景:音乐播放列表(下一首、切歌)、作为双端队列使用。
java
import java.util.LinkedList;
public class LinkedListDemo {
public static void main(String[] args) {
LinkedList<String> playlist = new LinkedList<>();
playlist.add("晴天");
playlist.add("七里香");
playlist.addFirst("夜曲"); // 插到最前面
playlist.removeLast(); // 移除最后一首
// 模拟播放下一首(队列操作)
String current = playlist.poll(); // 拿到并移除头部
}
}
项目真实用法 :
纯 LinkedList 用得不多,更多是把它当作队列 或栈 的底层(比如 Deque 接口用 LinkedList 实现),或者用于LRU 缓存:
java
// 利用 LinkedHashMap 快速实现 LRU
LinkedHashMap<Integer, String> lru = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 100; // 超过容量自动删最老
}
};
这背后就用到了双向链表维护访问顺序。
3. 栈 (Deque) ------ 后进先出,需要"倒退"
场景:编辑器撤销、浏览器后退、括号匹配验证。
java
import java.util.ArrayDeque;
import java.util.Deque;
public class StackDemo {
public static void main(String[] args) {
Deque<String> history = new ArrayDeque<>();
history.push("页面A");
history.push("页面B");
String back = history.pop(); // 回到页面B -> A
// 括号匹配检查
String code = "if (a == b) { return; }";
System.out.println(isParenthesesValid(code));
}
static boolean isParenthesesValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')');
else if (c == '{') stack.push('}');
else if (c == '[') stack.push(']');
else if (c == ')' || c == '}' || c == ']') {
if (stack.isEmpty() || stack.pop() != c) return false;
}
}
return stack.isEmpty();
}
}
项目怎么用:
-
Spring 拦截器链:多级拦截器前后执行依赖栈结构。
-
递归改迭代:自己用栈模拟递归,避免栈溢出。
-
表达式求值 :订单里的动态公式计算(如
价格*数量 + 运费)。
4. 队列 ------ 先进先出,按顺序处理
场景:短信发送队列、订单处理流水线、消息解耦。
java
import java.util.Queue;
import java.util.LinkedList;
class SmsTask {
String phone, content;
}
public class QueueDemo {
public static void main(String[] args) {
Queue<SmsTask> smsQueue = new LinkedList<>();
smsQueue.offer(new SmsTask()); // 生产者放入
while (!smsQueue.isEmpty()) {
SmsTask task = smsQueue.poll(); // 消费者取出
// 调用短信接口发送
}
}
}
项目真实用法 :
实际不会直接用简单队列,而是用 阻塞队列 配合线程池,或直接上消息中间件(RabbitMQ/Kafka)。
线程池的任务队列:
java
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(1000);
ThreadPoolExecutor pool = new ThreadPoolExecutor(
10, 20, 60, TimeUnit.SECONDS, workQueue);
所有提交到线程池的任务,其实就在一个队列里等待执行。
5. HashMap ------ 高速键值映射,乱序
场景:根据 ID 查用户、本地缓存、词频统计。
java
import java.util.HashMap;
import java.util.Map;
class User { String name; }
public class HashMapDemo {
// 模拟用户缓存
private static Map<Integer, User> userCache = new HashMap<>();
public static User getUser(int id) {
return userCache.computeIfAbsent(id, key -> loadFromDb(key));
}
static User loadFromDb(int id) { /* 查数据库 */ return new User(); }
// 词频统计
public static void main(String[] args) {
String text = "apple orange apple banana";
Map<String, Integer> freq = new HashMap<>();
for (String word : text.split(" ")) {
freq.merge(word, 1, Integer::sum);
}
System.out.println(freq);
}
}
项目怎么用:
-
请求参数映射 :
@RequestParam Map<String, Object> params -
本地缓存 :服务里经常
map.put(token, userInfo) -
分组聚合 :统计每个部门人数,用
Map<String, Integer>
注意 :并发场景必须换成ConcurrentHashMap,否则有死循环风险。
6. TreeMap / TreeSet ------ 需要按键排序、范围查询
场景:按分数排序的排行榜、IP 区间匹配、按日期排序的账单。
java
import java.util.TreeMap;
public class TreeMapDemo {
public static void main(String[] args) {
// 按分数自动排序
TreeMap<Integer, String> rank = new TreeMap<>();
rank.put(92, "张三");
rank.put(88, "李四");
rank.put(95, "王五");
// 范围查询:分数在 90 及以上的
System.out.println(rank.tailMap(90)); // {92=张三, 95=王五}
}
}
项目怎么用:
-
区间规则匹配 :根据订单金额匹配返利区间,
TreeMap.floorEntry(amount)直接找到命中的档位。 -
动态排序 :要求接口返回的数据按某字段排序,但数据更新频繁,用
TreeMap维护比每次查出来sort更高效。 -
ZSET 替代 :Redis 的有序集合在 Java 里常模拟为
TreeMap<score, value>做本地排序。
7. PriorityQueue ------ 反复取最大/最小
场景:紧急工单优先处理、TopK 热搜、任务调度器。
java
import java.util.PriorityQueue;
import java.util.Comparator;
class Order {
int id;
int priority; // 数字越小越紧急
}
public class PriorityQueueDemo {
public static void main(String[] args) {
// 小顶堆:优先级高的(数字小)先出队
PriorityQueue<Order> urgentQueue = new PriorityQueue<>(
Comparator.comparingInt(o -> o.priority));
urgentQueue.offer(new Order(1, 5));
urgentQueue.offer(new Order(2, 1)); // 紧急
urgentQueue.offer(new Order(3, 3));
while (!urgentQueue.isEmpty()) {
Order o = urgentQueue.poll(); // 总是先处理 priority=1 的
System.out.println("处理订单:" + o.id);
}
}
}
项目怎么用:
-
延时任务 :把任务放入堆,按
executeTime排序,线程每次取堆顶,时间到了就执行。 -
海量数据 TopN :内存放不下全部数据,用一个大小为
N的堆,遍历一次即可得到最大的 N 个。 -
合并有序小文件:每个文件读一行放入堆,每次都取最小的,写入大文件。
8. 图 ------ 网状关系,多对多
场景:社交关注关系、地铁换乘路径、依赖任务调度。
java
import java.util.*;
public class GraphDemo {
// 邻接表表示
private Map<String, List<String>> adj = new HashMap<>();
public void addEdge(String from, String to) {
adj.computeIfAbsent(from, k -> new ArrayList<>()).add(to);
}
// BFS 寻找最短路径(如推荐二度好友)
public int distance(String start, String target) {
if (start.equals(target)) return 0;
Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
int steps = 0;
while (!queue.isEmpty()) {
int size = queue.size();
steps++;
for (int i = 0; i < size; i++) {
String node = queue.poll();
for (String neighbor : adj.getOrDefault(node, Collections.emptyList())) {
if (neighbor.equals(target)) return steps;
if (visited.add(neighbor)) queue.offer(neighbor);
}
}
}
return -1;
}
}
项目怎么用:
-
工作流引擎:流程图本身就是 DAG(有向无环图),用拓扑排序检查依赖和安排执行顺序。
-
仓库货架路径规划:节点是货架位置,边是可通行的通道,BFS 算最短拣货路径。
-
社交"你可能认识的人" :用 BFS 遍历二度关系。
一般不会手写图结构,会引入
JGraphT这样的库,或直接用 Neo4j 图数据库。
总结:项目里怎么看待数据结构
所有业务代码,本质上都是在 选容器 → 放数据 → 取数据。
-
从 Controller 收到 JSON 参数,反序列化成一个
HashMap或DTO里的List。 -
Service 层处理数据:去重用
Set,聚合用Map<Key, List>,排序用TreeMap或PriorityQueue。 -
需要极速响应时,局部加个
HashMap做缓存。
你所熟悉的所有 Java 项目,底层都充满了这些集合的身影。它们决定了你代码的性能和可读性。真正内行与外行的区别,就在于能否一眼看出"这个地方该用 LinkedList 还是 ArrayList",而不是随手 new ArrayList()。
随机知识2
1. 链表:Java 里怎么定义?删除一定要先查吗?
定义方式(节点类)
Java 没有指针,用 对象引用 代替。通常是写一个内部静态类:
java
class ListNode {
int val;
ListNode next; // 单链表
ListNode prev; // 如果是双向链表才有
ListNode(int val) { this.val = val; }
}
删除必须先查吗?
-
单链表 :是的,删除某个值的节点,必须从头遍历找到它的前驱,否则无法断开链接。只有删除头节点时是 O(1)。
-
双向链表 (如 Java 的
LinkedList):如果已经拿到了要删除的那个节点对象本身 ,就可以通过prev和next直接绕过它,达到 O(1) 删除,不需要再查。 -
头尾操作:双向链表维护了头尾引用,所以
removeFirst() / removeLast()是 O(1)。
LeetCode 经典题
-
反转链表 (206)
-
环形链表 (141)
-
合并两个有序链表 (21)
-
删除链表的倒数第 N 个结点 (19)
-
LRU 缓存 (146) ------ 双向链表 + 哈希表
2. 栈:除了括号,还有哪些题?
栈的核心作用是 "最近相关性" 和 "暂时保存,之后处理"。
-
有效括号 (20) :你提到的典型题,判断
()[]{}是否有效。 -
逆波兰表达式求值 (150):后缀表达式计算,数字入栈,遇运算符弹出两个数。
-
用栈实现队列 (232):用两个栈实现先进先出。
-
最小栈 (155):要能 O(1) 获取最小值,用辅助栈存每步的最小值。
-
单调栈(重点):
-
每日温度 (739) :找到下一个更高温度要等几天。
-
接雨水 (42) :非常经典,用单调递减栈积水。
-
柱状图中最大的矩形 (84) :单调递增栈。
-
3. 队列:还有哪些题?
你提到了二叉树层次遍历,这本质是 BFS(广度优先搜索) 队列。
-
二叉树层序遍历 (102):最基础的 BFS。
-
岛屿数量 (200):BFS/DFS 沉没岛屿。
-
打开转盘锁 (752):最短路径找密码,BFS 典型应用。
-
滑动窗口最大值 (239) :用双端队列,维护一个递减队列,队头始终是当前窗口最大值。
-
用队列实现栈 (225):对比栈实现队列。
4. 树:二叉搜索树为什么快?平衡树到底干嘛?
二叉搜索树 (BST)
左小右大,查找时每次和根比较,平均砍掉一半子树,所以平均 O(log n) 。但最坏情况(顺序插入)会退化成一条链表,变成 O(n),所以需要平衡树。
平衡树
能自动保持树的高度为 O(log n),无论你怎么插入删除,不会退化成链表。
-
AVL 树:严格平衡,左右子树高度差不超过 1,查找飞快,但插入删除需要更多旋转。
-
红黑树:非严格平衡,牺牲一点平衡换取更快的插入/删除速度,综合性能优秀。
实际开发中 :
Java 的 TreeMap、TreeSet 就是用红黑树实现的。
-
需要有序性、范围查询时直接用
TreeMap。 -
不需要排序就用
HashMap。
LeetCode 遍历题
-
前/中/后序遍历 (144, 94, 145) ------ 递归和迭代(栈实现)
-
验证二叉搜索树 (98)
-
二叉搜索树的最近公共祖先 (235)
-
平衡二叉树 (110) ------ 判断一棵树是否是高度平衡的
5. 堆:LeetCode 怎么考?
堆本质是用数组实现的完全二叉树 ,可以 O(1) 拿到最值,插入删除 O(log n)。
Java 里 PriorityQueue 就是堆。
-
数组中的第K个最大元素 (215):小顶堆维持 K 个最大元素,堆顶就是答案。
-
前 K 个高频元素 (347):先用 HashMap 统计频率,再用堆取前 K。
-
合并K个升序链表 (23):把所有链表头节点放入小顶堆,每次取最小拼接。
-
数据流的中位数 (295):用两个堆(大顶 + 小顶)动态维护中位数。
6. 图:怎么用代码表示?题目有哪些?
常用表示法:邻接表
java
Map<Integer, List<Integer>> graph = new HashMap<>();
每个节点存它所有邻居的列表。
经典题
-
克隆图 (133):用 HashMap 存原节点到克隆节点的映射,DFS/BFS 深拷贝。
-
课程表 (207):判断有向图是否有环(拓扑排序 / BFS / DFS)。
-
网络延迟时间 (743):最短路径,用 Dijkstra 算法(优先队列)。
-
岛屿数量 (200):可以看作网格上的图,上下左右相邻为边。
7. 散列结构
你已经明白原理,题主要是快速查找、去重和统计。
-
两数之和 (1) :哈希表存
值→下标,O(n) 解决。 -
字母异位词分组 (49):每个字符串排序后作为键,分组。
-
最长连续序列 (128) :用
HashSet去重,O(n) 寻找连续区间。
随机知识3
头插法,就是在链表头部插入新节点。你之前总结得很好------JDK7 的 HashMap 在扩容时用头插法转移节点,在多线程下可能导致环形链表。下面我把头插法的具体做法 和成环的详细过程拆解出来。
一、头插法到底是什么?
普通链表插入有两种:
-
尾插法:新节点挂到链表最后,遍历时要走到末尾,顺序和插入顺序一致。
-
头插法:新节点直接成为新的头节点,原来的头节点变成它的后继。每次插入都是 O(1),无需遍历到尾。
头插法代码(单链表):
java
// 假设有一个头节点 head,新节点 newNode
newNode.next = head; // 新节点指向原来的头
head = newNode; // 头指针指向新节点
在 JDK7 HashMap 的 transfer 方法中,遍历旧链表的每个节点,用头插法放入新桶的链表,导致新链表和旧链表顺序完全相反。
二、JDK7 transfer 中的头插法(关键源码简化版)
java
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null; // 释放旧桶引用
do {
Entry<K,V> next = e.next; // 1. 先记住下一个节点
int i = indexFor(e.hash, newCapacity);
// 2. 头插:e.next 指向新桶的当前头节点
e.next = newTable[i];
// 3. 新桶的头变成 e
newTable[i] = e;
// 4. 继续处理下一个节点
e = next;
} while (e != null);
}
}
}
假设旧链表是 A -> B -> C -> null,单线程扩容后新链表会变成 C -> B -> A -> null(完全反转)。因为每次取出的节点都被插到新桶头部。
三、多线程下如何形成环形链表?
现在有两个线程 T1 和 T2 同时对这个链表扩容。
初始状态:
旧桶里链表:A -> B -> C -> null
步骤分解:
-
T1 执行
Entry<K,V> next = e.next;,此时e = A,next = B。然后 T1 挂起(时间片用完)。 -
T2 获得 CPU,顺利完成了整个
transfer。T2 把链表反转成了C -> B -> A -> null(在新数组中)。此时这些节点的next指向已经全部改变:-
C.next = B -
B.next = A -
A.next = null
-
-
T1 恢复执行,但 T1 局部变量里仍然保存着旧的引用:
-
e = A -
next = B(这是关键,T1 以为下一个还是 B)
-
-
T1 开始处理当前节点 A:用头插法把 A 放入自己的新表某个桶。假设那个桶当前是空的,执行后
A.next = null,桶头指向 A。 -
T1 接着
e = next;即e = B。然后再次进入循环:next = e.next;但此时 B 的 next 已经被 T2 改成指向 A 了!所以next = A。 -
T1 现在处理 B:用头插法将 B 插入桶。此时桶头是 A,所以:
-
B.next = A(这一步把 B 指向 A) -
桶头更新为 B
此时链表变为
B -> A -> null,看起来没问题,但注意A的next还是null。
-
-
T1 循环继续:
e = next;即e = A。再次获取next = e.next;即A.next,此时还是null,所以next = null。 -
T1 处理 A:用头插法,桶头目前是 B,所以:
-
A.next = B -
桶头变为 A
此时链表变成了
A -> B -> A -> B -> ...环形链表形成!
-
因为 T2 已经修改了 B.next = A,而 T1 又让 A.next = B,形成循环。
后续如果有 get 操作遍历到这个桶的链表,就会陷入死循环,CPU 飙升。
四、为什么 JDK8 改成了尾插法?
JDK8 的 resize 方法改为尾插法,保持节点原来的顺序,这样在多线程扩容时不会反转链表,也就避免了"指针互相引用形成环"的问题(但 HashMap 仍然不是线程安全的,只是这个特定 bug 被消除了)。同时 JDK8 引入了红黑树,在链表长度 >=8 且数组长度 >=64 时转成树,提高极端情况下的查询效率。
简单总结:头插法因为 O(1) 插入且最近插入的节点可能更常被访问,被用在 JDK7 的 HashMap 中;但扩容反转顺序的特性,在多线程并发操作同一链表时,会导致局部变量记录的错误指针形成闭合环。
随机知识4
一、什么是好的哈希算法?
对 HashMap 而言,好的哈希算法 = 能把任意键尽量均匀地"打散"到各个桶里 。
具体来说,要满足两点:
-
离散性高
不同的键,即使很相似(比如
"Aa"和"BB"),产生的哈希值也要差异巨大,避免聚集在同一个桶。 -
计算足够快
每次
get/put都要算哈希,不能太重。
JDK8 中 HashMap 的哈希方法做了高低位异或扰动:
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
将 hashCode 的高 16 位与低 16 位异或,让高位特征也参与桶下标的决定(因为桶下标是 (n-1) & hash,只用低位),这样可以减少低位相同、高位不同的碰撞。
在理想情况下,好的哈希算法可以让元素近似于随机、独立地掉进各个桶,每个桶被选中的概率相等(均匀分布)。
二、泊松分布是什么?怎么用在这里?
当大量元素独立且以很小的概率落入许多桶时,每个桶里元素的数量就服从泊松分布。
泊松分布适用于:
-
事件发生次数独立、随机
-
单次发生概率很小(桶数量多,每个桶被选中的概率 =
1/桶数) -
总次数很大(元素很多)
公式:P(X = k) = (λ^k * e^(-λ)) / k!
其中 λ 是平均每个桶里的元素个数。
在 HashMap 里,λ = 负载因子(默认 0.75),为什么?
-
负载因子
loadFactor定义了平均每个桶里存放的元素数量(也就是 λ)。 -
扩容阈值 =
capacity * loadFactor,当元素总数达到这个值就扩容,所以平时元素数量/桶数 ≈ 负载因子。
所以 λ = 0.75,代入泊松分布公式得到:
| 桶中元素个数 (k) | 概率 (大约) |
|---|---|
| 0 | 0.606 |
| 1 | 0.303 |
| 2 | 0.076 |
| 3 | 0.012 |
| 4 | 0.0015 |
| 5 | 0.00023 |
| 6 | 2.9 × 10⁻⁵ |
| 7 | 3.1 × 10⁻⁶ |
| 8 | 3.3 × 10⁻⁷ (约千万分之六) |
也就是说,在一个好的哈希算法下,一个桶里塞了 8 个以上节点的概率只有千万分之六,极小。
三、负载因子为什么是 0.75?
负载因子是 空间和时间的折中点:
-
太小(如 0.5):扩容频繁,浪费很多空桶,但碰撞少。
-
太大(如 1.0):空间利用高,但桶里元素变多,查找可能退化。
-
0.75 是实践得出的平衡值:
-
由泊松分布可知,此时桶内元素个数达到 8 是极小概率事件,链表查询还不至于退化太严重。
-
同时空间利用率约为 75%,是可接受的。
-
四、红黑树引入的数学依据(串联)
结合以上几点,JDK8 设计者的推导链是:
-
假设哈希算法是完美的 ------ 元素均匀独立散列。
-
在负载因子 0.75 下,泊松分布预测桶内元素个数的概率。
-
计算得出:桶内元素 ≥ 8 的概率约为 0.0000006,即通常不会发生。
-
因此,一个桶里链表长度达到 8,说明这个假设已经失效了 ------ 要么哈希函数特别差(很多键冲突),要么遭到了恶意碰撞攻击。
-
一旦出现这种退化,继续用链表则查、插、删都可能变成 O(n)。
-
于是引入红黑树:链表长度 ≥ 8 且数组长度 ≥ 64 时,将链表转为红黑树,把最坏情况控制在 O(log n)。
-
反之,当元素减少到 ≤ 6 时,树又转回链表,避免维护树的额外开销。
完整条件:
-
链表化 → 树化:
链表长度 >= 8且数组容量 >= 64(否则优先扩容,不树化) -
树化 → 链表化:
节点数 <= 6
五、一句话总结
好的哈希算法让元素均匀分布,桶内元素个数服从泊松分布,在负载因子 0.75 时,桶里节点达到 8 的概率只有千万分之六。一旦出现,说明哈希均匀假设被打破,此时用红黑树取代链表,防止性能退化到 O(n)。
碎碎念:后续会更新每天学习的八股和算法 题,开始准备秋招的第12天。努力连续更新100天!以后每天就按,秋招项目【java+agent】,科研,必做项目,算法,八股,锻炼身体来总结。1.秋招项目agent算是接入了,但是面试八股这边还没准备好,可以先准备准备面试会问的重点。2.科研今天完全没搞 3.项目继续搞,今天也没搞,效率确实有点低。4.八股继续深挖吧,就总结了上面的5.算法,刷了但是前两天的题又忘记了。
总结:其实一天有效时间就那么多,要合理分配,今天确实不知道为什么有点坐不住,坚持坚持哇!!!明天吧秋招项目的面试题整理【2h】, 科研看看能不能跑点数据,or搭建网络【2h】,项目结合秋招项目整理整理agent的知识【2h】, 八股感觉先把之前的看一遍,和ds老师一起搞明白,深度思考思考【2h】。还有参加的培训项目也得再看看【1h】。
【最后最后,请相信自己可以的!!!!!】