topcode【随机算法题】【2026.5.22打卡-java版本】

前 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 参数,反序列化成一个 HashMapDTO 里的 List

  • Service 层处理数据:去重用 Set,聚合用 Map<Key, List>,排序用 TreeMapPriorityQueue

  • 需要极速响应时,局部加个 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):如果已经拿到了要删除的那个节点对象本身 ,就可以通过 prevnext 直接绕过它,达到 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 的 TreeMapTreeSet 就是用红黑树实现的。

  • 需要有序性、范围查询时直接用 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

步骤分解:

  1. T1 执行 Entry<K,V> next = e.next;,此时 e = Anext = B。然后 T1 挂起(时间片用完)。

  2. T2 获得 CPU,顺利完成了整个 transfer。T2 把链表反转成了 C -> B -> A -> null(在新数组中)。此时这些节点的 next 指向已经全部改变:

    • C.next = B

    • B.next = A

    • A.next = null

  3. T1 恢复执行,但 T1 局部变量里仍然保存着旧的引用:

    • e = A

    • next = B (这是关键,T1 以为下一个还是 B)

  4. T1 开始处理当前节点 A:用头插法把 A 放入自己的新表某个桶。假设那个桶当前是空的,执行后 A.next = null,桶头指向 A。

  5. T1 接着 e = next;e = B。然后再次进入循环:next = e.next; 但此时 B 的 next 已经被 T2 改成指向 A 了!所以 next = A

  6. T1 现在处理 B:用头插法将 B 插入桶。此时桶头是 A,所以:

    • B.next = A (这一步把 B 指向 A)

    • 桶头更新为 B

      此时链表变为 B -> A -> null,看起来没问题,但注意 Anext 还是 null

  7. T1 循环继续:e = next;e = A。再次获取 next = e.next;A.next,此时还是 null,所以 next = null

  8. 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 而言,好的哈希算法 = 能把任意键尽量均匀地"打散"到各个桶里

具体来说,要满足两点:

  1. 离散性高

    不同的键,即使很相似(比如 "Aa""BB"),产生的哈希值也要差异巨大,避免聚集在同一个桶。

  2. 计算足够快

    每次 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 设计者的推导链是:

  1. 假设哈希算法是完美的 ------ 元素均匀独立散列。

  2. 在负载因子 0.75 下,泊松分布预测桶内元素个数的概率。

  3. 计算得出:桶内元素 ≥ 8 的概率约为 0.0000006,即通常不会发生。

  4. 因此,一个桶里链表长度达到 8,说明这个假设已经失效了 ------ 要么哈希函数特别差(很多键冲突),要么遭到了恶意碰撞攻击。

  5. 一旦出现这种退化,继续用链表则查、插、删都可能变成 O(n)。

  6. 于是引入红黑树:链表长度 ≥ 8 且数组长度 ≥ 64 时,将链表转为红黑树,把最坏情况控制在 O(log n)。

  7. 反之,当元素减少到 ≤ 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】。

【最后最后,请相信自己可以的!!!!!】

相关推荐
世纪末的小黑11 小时前
【LeetCode自用】LeetCode自用记录贴,题目一:两数之和
数据结构·算法·leetcode
Dicky-_-zhang11 小时前
敏感数据加密存储实战
java·jvm
夕除11 小时前
spring boot 12
java·开发语言·python
Brilliantwxx11 小时前
【C++】 认识STL set与map(基础接口+题目OJ运用)
开发语言·数据结构·c++·笔记·算法
罗超驿11 小时前
21.jdbc 学习笔记:从原理到实践的全流程梳理
java·数据库·mysql·面试
05候补工程师11 小时前
【线性代数】核心考点复习笔记:二次型配方法、施密特正交化步骤与特征值经典题型详解
经验分享·笔记·线性代数·考研·算法
Mahir0811 小时前
Spring 全家桶常见注解全解:从入门到精通
java·后端·spring·面试·常见注解
Deep-w11 小时前
【MATLAB】基于遗传算法的直流电机 PI 控制器参数优化研究
开发语言·算法·matlab
执笔论英雄12 小时前
GPU内存架构-DSMEM与L2
java·spring·架构