Java树结构全解析:从二叉树到红黑树,面试与实战一网打尽
你正在准备Java面试吗或者你刚学完数组和链表正站在“树”这个门槛前面想知道它到底是个什么东西这篇文章就是写给这两种人的。树可以说是Java数据结构里最“值钱”的一块内容面试必考、工作中绕不开、刷题时能见到各种稀奇古怪的变体。从最基础的二叉树到让无数人掉头发的红黑树再到数据库索引背后的B树它们全是树这个大家族里的成员。我这里先给你交个底树不难难的是没有人帮你把“为什么要这样设计”讲透。这篇文章我会从纯Java的视角出发把树的底层概念、经典实现、高频考点和实际场景挨个拆开。你不需要提前掌握什么高深的东西只要看得懂递归、看得懂简单的类定义就能跟着我一步步把树这块硬骨头啃下来。要准备面试也好要补基础也好这篇文章都能让你少走很多弯路。1. 树的本质为什么面试官总爱问树1.1 树在数据结构里的位置先想一个问题数组和链表解决的是什么问题是一对一的线性关系——一个元素后面跟着一个元素像排队一样。但现实世界里的关系远没这么简单。公司的组织架构是一对多文件系统是一对多网站的导航菜单是一对多。你需要在内存里表示这种“一个父亲多个孩子”的结构线性结构就束手无策了这时候就得请出树。很多人第一节课被树的术语吓到觉得它很抽象。其实树就是“嵌套的、有层次的链表”——链表每个节点有一个next指针树里把next拆成了left和right甚至更多。在Java里TreeNode就是一个普通类里面有几个指向同类对象的引用而已。这个概念一旦想通了后面所有花样都只是在这个基础上加规则。再往深处看树之所以无处不在是因为它天生具备两个优势一是能表达层次关系二是通过特定规则排列之后可以实现极快的查找。第二个优势尤其重要二叉搜索树、平衡树、B树全都是冲着“查找速度”去的。你在JDK源码里看到的TreeMap、HashMap的树化、MySQL索引用的B树本质上都是在利用树的这个特性。1.2 树的专业术语一次性说透我在面试中问过很多候选人树的基础发现很多人对“深度”和“高度”傻傻分不清。这里给你一个不会忘的理解方式深度是从上往下数根节点深度为0有的教材是1以你手头教材为准高度是从下往上数叶子节点高度为0。树的层数是从根开始的第几层这个基本不产生歧义。常用术语就这几个根节点树的最顶层节点一棵树只有一个根。叶子节点没有任何子节点的节点俗称“叶子”。父节点、子节点、兄弟节点字面意思不用特别记。度节点拥有的子树个数。二叉树的度最多是2。子树树里的任何一个节点连同它下面的所有后代本身也是一棵树。还有两个容易混的概念满二叉树和完全二叉树。满二叉树是“每一层都装满”完全二叉树是“除了最后一层其他层都满而且最后一层的节点都靠左排列”。这两个概念在堆排序和数组存储里特别重要因为完全二叉树可以用数组来存下标为i的节点它的左孩子下标是2i1右孩子是2i2父节点是(i-1)/2。这个性质后面学堆的时候会用到。1.3 为什么Java没有提供一个“万能”的Tree类很多Java初学者会有个疑问List有ArrayList、LinkedListMap有HashMap、TreeMap怎么没有“Tree”这样一个现成的树类给你用说实话这是Oracle的“有意为之”。树这个结构本身太泛了——二叉树、多叉树、搜索树、平衡树、Trie树每种树的操作逻辑完全不同。你没法用一个万能类去覆盖所有场景。所以JDK的套路是在具体场景里内置具体实现。TreeMap和TreeSet底层就是一棵红黑树自平衡的二叉搜索树HashMap在链表长度过长时会自动转成一棵红黑树你不需要自己造轮子直接用就行。但一旦涉及自定义的树结构比如做一个菜单树、组织架构树、Trie树JDK就不管你了得自己写TreeNode类。另外提醒一句别把数据结构里的树和Linux设备树、Android视图树搞混。Linux设备树是一种描述硬件信息的配置文件格式Android的View树是UI组件的嵌套结构它们只是“碰巧借用了树这个名词”本质上和Java数据结构里的树不是一回事。面试时如果聊到这块别绕进去。2. 二叉树与二叉搜索树最核心的基础2.1 二叉树节点的Java定义二叉树是每个节点最多只有两个孩子节点的树。在LeetCode和面试手写代码时我们用的TreeNode定义基本都是这个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; } }这个类就三个字段一个存值两个存指向左右子树的引用。就这么简单。你用new TreeNode(5)就创建了一个孤零零的节点然后手动把left和right指来指去一棵二叉树就串起来了。我见过不少初学者在这卡壳他们总觉得树是一个“大整体”必须要有什么特殊语法来创建。其实树就是一个个普通对象通过引用关系串起来的结构跟链表一模一样只是每个节点有两个指针而不再是一个。理解到这个层面你写代码就不会犯怵了。2.2 二叉搜索树的查找与插入左小右大二叉搜索树BST的规则只有一句话对于任意节点左子树所有节点的值都小于它右子树所有节点的值都大于它。注意是“所有”不是“直接孩子”。这规则一出来查找就变成了“走迷宫时不断根据提示选左还是选右”的过程public TreeNode searchBST(TreeNode root, int target) { if (root null || root.val target) { return root; } return target root.val ? searchBST(root.left, target) : searchBST(root.right, target); }插入的思路和查找基本一致先找到合适的空位再把新节点挂上去。这里有个容易犯错的地方递归插入必须把返回值赋给root.left或root.right很多人漏掉这一句导致插入后树没变。public TreeNode insertIntoBST(TreeNode root, int val) { if (root null) { return new TreeNode(val); } if (val root.val) { root.left insertIntoBST(root.left, val); } else if (val root.val) { root.right insertIntoBST(root.right, val); } return root; }平均情况下BST的查找、插入时间都是O(log n)这个“log n”的来源就是每一层都能排除掉一半的方向跟你翻字典时直接翻到中间然后往前或往后翻一个道理。但请注意我强调了“平均情况”。2.3 二叉搜索树的删除面试手撕的高频坎删除是BST里最麻烦的操作很多人笔试就挂在它上面。麻烦在哪儿在于删除一个节点之后你还要保持“左小右大”的性质。分三种情况目标节点是叶子直接返回null让父节点的指针指向空即可。目标节点只有一个孩子直接让孩子顶上来返回这个孩子。目标节点有两个孩子这个最麻烦。你需要找到一个合适的节点来顶替它。经典做法是找中序后继右子树里最小的那个节点把它复制到目标节点位置上然后再去右子树里删除那个“最小值节点”。为什么要找中序后继不找别的因为中序后继是右子树中最小的节点它满足两个条件大于左子树的所有节点、小于右子树里除了它自己之外的所有节点。用它顶替整棵树的性质不会被破坏。而且中序后继最多只有一个右孩子删除它又回到了情况1或2。public TreeNode deleteNode(TreeNode root, int key) { if (root null) return null; if (key root.val) { root.left deleteNode(root.left, key); } else if (key root.val) { root.right deleteNode(root.right, key); } else { if (root.left null) return root.right; if (root.right null) return root.left; TreeNode minNode findMin(root.right); root.val minNode.val; root.right deleteNode(root.right, minNode.val); } return root; } private TreeNode findMin(TreeNode node) { while (node.left ! null) node node.left; return node; }这段代码你最好亲手在纸上画一棵三层的树模拟一下删除根节点的流程。我当年就是靠画图把这个操作彻底搞明白的光看代码很难真正内化。2.4 BST退化成链表引出自平衡BST看似完美但有一个致命缺陷如果按有序序列插入比如1,2,3,4,5,6,7它会变成一条只有右孩子的“斜树”这时候查找的复杂度直接退化成O(n)。你想想那跟线性扫描链表有什么区别为了解决这个问题计算机科学家想出了一条路想办法让树在插入和删除之后尽量保持“对称”不能让某一侧越长越倾斜。这条路衍生出了两个明星结构AVL树和红黑树。这也是JDK里TreeMap和HashMap树化时真正用的东西。接下来我们就看看它们是怎么把自己“掰平”的。3. 平衡的艺术AVL树与红黑树3.1 AVL树严格平衡的“强迫症”AVL树是第一个被发明的自平衡二叉搜索树。它的规则非常狠任意节点的左右子树高度差绝对值不超过1。这个高度差被称为平衡因子。一旦插入或删除导致某个节点的平衡因子变成了2或-2就必须通过旋转来恢复平衡。旋转有四种标准姿势LL左左在左孩子的左子树上插入导致失衡对失衡节点做一次右旋。RR右右在右孩子的右子树上插入导致失衡对失衡节点做一次左旋。LR左右在左孩子的右子树上插入导致失衡先对左孩子做左旋再对失衡节点做右旋。RL右左在右孩子的左子树上插入导致失衡先对右孩子做右旋再对失衡节点做左旋。旋转的代码核心就是调整引用关系private TreeNode rotateRight(TreeNode y) { TreeNode x y.left; TreeNode t2 x.right; x.right y; y.left t2; return x; } private TreeNode rotateLeft(TreeNode x) { TreeNode y x.right; TreeNode t2 y.left; y.left x; x.right t2; return y; }为什么旋转能恢复平衡不会破坏BST性质因为旋转只是调整了树型结构而左小右大的相对顺序没有变。以右旋为例x是y的左孩子旋转后y变成了x的右孩子这个变化自始至终满足“x y”同时t2原本是x的右子树里面所有节点都大于x且小于y旋转后t2正好挂在y的左子树上依然符合BST规则。这就是旋转“不破坏有序性”的根本原因。3.2 红黑树面试八股文里的“硬骨头”红黑树在面试里出现频率极高但你不用慌。网上把红黑树讲得玄乎其玄其实大家真正需要理解的是它解决问题的思路而不是手写完整实现面试真正让你手写红黑树的情况非常少。红黑树在BST基础上增加了颜色标记用五条性质来约束树的形态每个节点要么是红色要么是黑色。根节点是黑色。所有叶子节点NIL空节点都是黑色。红色节点的两个子节点必须是黑色不能出现连续两个红色节点。从任意节点到它的每个叶子节点的所有路径上黑色节点的数量相同。第5条性质是红黑树平衡的根源。它保证了“最长路径不超过最短路径的两倍”——因为最短路径是全黑路径最长路径是红黑交替路径红色不能连续出现所以黑色节点数相同的情况下红色最多只能让路径长度翻倍。这种平衡比AVL的“严格高度差不超过1”要宽松但已经足够把操作复杂度控制在O(log n)。JDK里的TreeMap就是一棵红黑树。每次put时新节点默认是红色然后根据情况和父节点、叔叔节点的颜色做变色或者旋转。为什么新节点是红色因为插入红色节点不容易破坏第5条性质黑色节点数量不变如果插入的是黑色节点那路径上的黑色节点数直接不一样情况更麻烦。这个逻辑很像“先给你一个能满足大多数规则的默认答案再在不满足规则的局部慢慢修”。HashMap里也有红黑树的身影当单个桶的链表长度超过8、且数组容量不小于64时链表会转成红黑树当节点数减少到6时红黑树会改回链表。8和6之间留了缓冲避免频繁转换带来的性能抖动。这个阈值是怎么来的源码注释里给了一个泊松分布的计算大意是负载因子0.75时单个桶里链表长度超过8的概率极低如果真超过8说明哈希函数分布异常这时候转红黑树来兜底。3.3 AVL和红黑树怎么选一张表说清面试常见追问是“HashMap为什么不直接用AVL树”。答案的核心在于操作成本。AVL树的查找确实更快但插入删除的旋转次数更多因为它的平衡约束太严格稍微一折腾就要转红黑树的平衡约束宽松旋转次数少整体插入删除性能更稳定。JDK选择的不是“某个操作最快”而是“所有操作综合下来最优”。指标AVL树红黑树平衡标准严格高度差不超过1宽松最长路径不超过最短路径两倍查找性能更优略逊一点但同属O(log n)插入/删除旋转次数相对多相对少应用场景读多写少的场景读和写都频繁的通用场景我给个更直白的类比AVL像完美主义者每个细节都要对齐代价是操碎了心红黑树像“差不多先生”规则只要不越界就行代价是整体稍微松散一点但省心。HashMap选择红黑树就是看中了它在频繁增删场景下的综合性价比。4. 进阶树结构B树、字典树、哈夫曼树与表达式树4.1 B树与B树数据库索引背后的树你搜索“B树”时会看到大量数据库相关的帖子这两者强绑定。B树是多路平衡查找树它和二叉树的本质区别是一个节点可以存储多个key也可以有多个孩子。为什么数据库要用多路而不学红黑树只用二叉树因为数据库数据存在磁盘上磁盘IO慢得惊人。每读一个节点就相当于一次磁盘IO树越矮访问次数越少。B树通过“一个节点塞很多key”让树变得又矮又宽高度可能是两三层查询一次最多两三次IO比二叉树动辄几十次IO香多了。B树是B树的改进版也是MySQL InnoDB索引的真实结构。B树有两个关键特性所有数据都存在叶子节点内部节点只存索引叶子节点之间用链表串起来。这样设计的好处是查询任何一个数据都要走到叶子节点时间稳定而且叶子节点有序且相连做范围查询比如查所有年龄在20到30岁之间的人时只要在叶子链表上顺序遍历就行不需要来回回溯。顺带提一下热词里的“梅克尔帕特里夏树Merkle Patricia Tree简称MPT”它在以太坊里被用来组织账户状态和交易数据。简单说MPT就是“字典树默克尔树”的结合体既能按key高效查找又能通过根哈希快速校验整棵树的完整性。它和B树解决的不是一类问题一个是区块链场景下的防篡改一个是关系型数据库里的高效查询但对“组织大量数据”这件事而言树依旧是最靠谱的方案。这里也顺便说一句网上搜“Linux设备树”搜到的东西其实是描述硬件信息的配置文件格式跟数据结构里的树完全是两码事学习时别被这个同名概念干扰。4.2 字典树敏感词过滤和自动补全字典树Trie也叫前缀树解决的是“多个字符串的公共前缀复用”问题。它的每个节点不存完整的字符串而是存一个字符或者说一个转移状态。根节点是空节点从根走到某个标记节点路径上经过的字符拼起来就是一个完整单词。Java实现一个面向26个小写字母的Trie非常直接class Trie { private Trie[] children new Trie[26]; private boolean isEnd; public void insert(String word) { Trie node this; for (char c : word.toCharArray()) { int idx c - a; if (node.children[idx] null) { node.children[idx] new Trie(); } node node.children[idx]; } node.isEnd true; } public boolean search(String word) { Trie node searchPrefix(word); return node ! null node.isEnd; } public boolean startsWith(String prefix) { return searchPrefix(prefix) ! null; } private Trie searchPrefix(String prefix) { Trie node this; for (char c : prefix.toCharArray()) { int idx c - a; if (node.children[idx] null) return null; node node.children[idx]; } return node; } }Trie的时间复杂度很吸引人插入和查询都是O(单词长度)跟有多少个单词无关。这在敏感词过滤、搜索框自动补全、IP路由的最长前缀匹配里都很实用。空间上字符集越大越费内存比如汉字Trie树每个节点如果存一个数组会非常浪费所以工程里通常用HashMap代替定长数组来节省空间。你可以在面试时主动提这点会让面试官觉得你是真做过东西而不是背过题。4.3 哈夫曼树从压缩算法到Java实现哈夫曼树又叫最优二叉树它的核心目标是让出现频率高的字符编码更短让整体编码长度最短。具体做法是先把每个字符看成一棵只有根节点的树权值就是出现频率然后不断从森林里挑两棵权值最小的树合并成一棵新树新树的权值是两者之和。合并这件事天然适合用优先队列Java里的PriorityQueue来做。这段过程用Java描述就是PriorityQueueTreeNode pq new PriorityQueue(Comparator.comparingInt(n - n.val)); // 初始化为每个字符创建一个节点val为频率放入pq while (pq.size() 1) { TreeNode left pq.poll(); TreeNode right pq.poll(); TreeNode parent new TreeNode(left.val right.val); parent.left left; parent.right right; pq.offer(parent); } TreeNode root pq.poll();构建完成后从根出发向左走记0、向右走记1路径上的0/1序列就是叶子节点对应字符的哈夫曼编码。注意哈夫曼编码是前缀编码也就是说任何一个字符的编码都不是另一个字符编码的前缀这样才能在解码时做到无歧义、不需要分隔符。这个性质正是哈夫曼树的结构带来的——所有字符都在叶子节点上路径天然不会互相包含。哈夫曼树的应用远不止文件压缩。JPEG图像压缩、ZIP压缩、视频编码里都能看到它的影子。它的重要性不在于代码多复杂而在于它展示了“如何用树来建模一个优化问题”。4.4 表达式树把算术表达式变成一棵树表达式树是把一个算术表达式表示成二叉树叶子节点是操作数内部节点是运算符。比如表达式(3 4) * 5根节点是*左子树是和它的两个叶子3、4右子树是叶子5。构建方法很有意思把中缀表达式转成后缀表达式比如3 4 5 *然后从左到右扫描遇到操作数就压栈遇到运算符就弹出两个节点作为它的左右孩子再把运算节点压栈。扫描完栈顶就是表达式树的根。对这棵表达式树做后序遍历先左子树、再右子树、最后根节点你得到的序列正好就是后缀表达式而计算后缀表达式的过程其实就是从叶子往根一步步收敛求值的过程。做编译器或解释器的朋友看到这里应该会会心一笑因为这就是语法分析里抽象语法树AST的雏形。表达式树的价值在于它把“运算的顺序”显式地表达成了“树的形态”你看树结构就能知道先算谁后算谁而不用再人为地去记运算符优先级规则。5. 树的遍历与经典算法题从理解到秒杀5.1 前中后序与层序遍历两种层级、两种写法树的遍历是后面所有算法题的基础。前序、中序、后序三者非常容易混这里给一个不死记硬背的口诀“前中后”指的是根节点被访问的顺序。前序就是“根-左-右”中序就是“左-根-右”后序就是“左-右-根”。你用递归写的时候只要记住打印根的位置位置放前就是前序放中间就是中序放最后就是后序// 前序 public void preorder(TreeNode node) { if (node null) return; System.out.println(node.val); preorder(node.left); preorder(node.right); } // 中序把println放中间后序放最后代码结构完全一样递归三行代码谁都会背但面试真正的考察点是非递归写法——用栈模拟系统递归过程。前序非递归最直观一路往左走边压栈边打印走到头了弹出栈顶拐到右子树。DequeTreeNode stack new ArrayDeque(); TreeNode cur root; while (!stack.isEmpty() || cur ! null) { while (cur ! null) { System.out.println(cur.val); // 访问根 stack.push(cur); cur cur.left; // 不断往左 } cur stack.pop(); cur cur.right; // 回到上一层转向右 }层序遍历是另一种思路它不递归顺着子树走而是借助队列一层一层地往外扩。每轮从队列里取出当前层的所有节点这里用size记录当前层节点数处理完再让它们的左右孩子入队这样就能严格按层次从左到右访问。层序遍历在很多“按层处理”的题目里是标配比如求二叉树最大宽度、打印成锯齿形。5.2 树的直径两次DFS的巧妙解法树的直径定义为树中任意两个节点之间最长路径上的边数。这道题很经典因为它有一个反直觉的定理从任意一个点出发找到离它最远的点A再从A出发找到离A最远的点B那么A到B的距离就是树的直径。这个定理在带权树且权值非负时成立面试时可以直接用。实现上第一次BFS/DFS找到最远点第二次从最远点再跑一遍记下最大距离即可。不过在LeetCode上树的直径还有用“递归计算左右子树最大深度并全局更新”的写法int ans 0; public int diameterOfBinaryTree(TreeNode root) { depth(root); return ans; } private int depth(TreeNode node) { if (node null) return 0; int leftDepth depth(node.left); int rightDepth depth(node.right); ans Math.max(ans, leftDepth rightDepth); return Math.max(leftDepth, rightDepth) 1; }核心思路是经过某个节点的最长路径等于它左子树的最大深度加上右子树的最大深度。每个节点都算一遍全树的最大值就是直径。这个是典型的“递归时顺便更新全局答案”的模式很多树形DP题都是这么玩的。5.3 最近公共祖先一道题看懂递归返回值设计求两个节点的最近公共祖先LCA是面试高频题。代码不长但很多人对着答案看不明白它能“找到最近”的原理public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root null || root p || root q) { return root; } TreeNode left lowestCommonAncestor(root.left, p, q); TreeNode right lowestCommonAncestor(root.right, p, q); if (left ! null right ! null) { return root; } return left ! null ? left : right; }理解这个递归的关键是搞清返回值语义它返回的是“以当前节点为根的子树里已经找到的p或q或它们的公共祖先”。如果左右子树的查询结果都不为空说明p和q分别位于左右两侧那么当前节点就是它们的最近公共祖先如果只有一侧不为空就说明两个目标都在那侧或者那侧已经找到了共同的祖先直接往上传即可。我建议你在纸上把一棵树画出来手动模拟几个用例。比如p在根左子树最深层、q在右子树你会看到递归一层层往上传递直到某个节点左右都非空就返回了。这道题吃透了你对“递归返回值”这个抽象的理解会上一个台阶。5.4 树的序列化与反序列化为什么它是压轴题序列化反序列化是不少大厂终面题。它要求你实现两个函数一棵树转成字符串字符串再还原成原来的树。常见的做法是前序遍历加占位符null节点用特殊符号表示。// 序列化1,2,null,null,3,4,null,null,5,null,null public String serialize(TreeNode root) { StringBuilder sb new StringBuilder(); preorderSerialize(root, sb); return sb.toString(); } private void preorderSerialize(TreeNode node, StringBuilder sb) { if (node null) { sb.append(null,); return; } sb.append(node.val).append(,); preorderSerialize(node.left, sb); preorderSerialize(node.right, sb); }反序列化时把字符串按逗号拆开从左到右重建。前序遍历的顺序天然保证先重建根再重建左子树再重建右子树。遇到null就返回空节点。整个逻辑用递归写起来很简洁前提是你真正理解了前序序列在数组展开后的顺序规则。序列化反序列化之所以是压轴题是因为它综合考察了你对遍历顺序的理解、对递归的理解、对边界条件的敏感度。如果你能把这道题干净利落地写出来面试官基本就认可你的树基础了。6. 常见问题与排查技巧实录6.1 递归爆栈与StackOverflow递归是树题最自然的写法但有个隐患如果树的深度非常大比如链式退化的BST深度等于节点数递归层数过多就会栈溢出报StackOverflowError。这种情况在本地测试还好在线上或者极端用例里就可能翻车。解决思路有两个。一是改用显式栈的迭代写法自己在堆上模拟递归过程不再依赖系统调用栈二是优化递归逻辑本身比如求深度时用尾递归Java对尾递归没有优化实际帮助不大或者像后序遍历那样用Morris遍历把空间压到O(1)。Morris遍历的原理是利用空指针做临时线索来回溯理解成本偏高面试时能说出思路就是加分项不要求一定手写出来。什么时候用递归什么时候用迭代我给你一个实用判断标准如果树是平衡的或你确定深度不会太大递归更清晰、代码更好维护如果题目场景是极端不平衡的“链表树”或者你做了深度限制但上限很低那就用迭代。6.2 空指针、边界条件与其他“低级错误”刷树题最容易翻车的地方不在思路而在空指针。我总结了几条血泪经验任何递归入口都要先判断根节点是否为null。漏掉这个节点个数为0的用例直接挂。访问node.left或node.right之前先确认node本身不是null。很多错误都是一拿到节点就往下钻却不考虑null。求深度、求平衡因子这类题递归返回值的默认情形要想清楚。返回0还是返回-1指的是不同约定别混。如果用全局变量记录答案比如上面的树的直径多组测试用例之间要记得重置。LeetCode这类在线判题平台一个方法对应一次调用但如果你自己写测试代码循环跑多个用例全局变量不清零就会出鬼问题。你还可能碰到一个隐蔽问题递归函数返回类型设计得不对。比如判断一棵树是否对称很多人想着返回boolean但里面需要比较两个节点返回值就想不清楚了。这时候把函数签名改成isMirror(TreeNode left, TreeNode right)递归结构会清晰很多。卡壳的时候先停下来检查自己递归函数的入参和返回值语义是否自洽。6.3 面试时怎么答树相关的题我的三个心得先别急着动笔写代码。面试官问“讲讲红黑树”你不要上来就背五条性质可以先说一句“红黑树是一种自平衡的二叉搜索树核心约束是黑色节点数量在每条路径上保持一致所以我用颜色和旋转来维护这个约束”这比直接机械背性质更有条理也让面试官觉得你理解的是设计思想。问“HashMap为什么用红黑树”时把话题引向“8和6阈值”和“泊松分布”往往是加分点。你不一定算出准确概率但至少要知道负载因子小于0.75的情况下链表长度超过8的概率极小一旦出现说明输入严重冲突红黑树是兜底方案6和8之间的1个节点差距是为了防止频繁转换。最后一点树的题别光刷不练。我个人的建议是LeetCode的树专题从二叉树前中后序遍历开始再到最大深度、直径、最近公共祖先、序列化这10来道题吃透大部分树的面试题你都能对付。脑内跑递归不如在纸上画树、手动模拟递归栈画着画着你就会发现树没有你想象中那么难。最后再分享一个小技巧我在学树的初期总喜欢把递归调用过程写成注释比如在preOrder(root.left)上方写“处理左子树这颗子问题”。这个习惯让我很快建立了“递归 直接信任子问题已经解决”的思维模式。刷树题卡住的时候别死磕细节回到“当前节点要做什么、子问题要返回什么”这两问上很多题目就通了。