news 2026/9/10 20:59:23

二叉树复试冲刺:遍历、重建与高频算法题精讲

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
二叉树复试冲刺:遍历、重建与高频算法题精讲

计算机复试学习笔记 Day9,今天正式从线性结构进入树的专题。前八天我先后过完了C++语法基础、链表、栈队列、排序算法和字符串处理,机试里练得手热,但从今天的复习量来看,前面的内容只能算开胃菜。二叉树在复试机试中的出现频率高得惊人,几乎每三套题里就有一套涉及树的操作;面试环节更不用说,“讲一下二叉树的遍历方式”“手写一个最近公共祖先”基本属于人手一题。这篇笔记把Day9的完整复习脉络整理出来,包含核心知识点的原理拆解、五道高频题的AC代码与踩坑记录、面试八股速记,以及我个人的复盘安排,给正在准备复试的同学一份可以直接照着走的材料。

1. 为什么我把Day9留给二叉树:复试中的性价比之王

1.1 先算一笔账:二叉树在复试中到底占多少分

如果你现在还在纠结“数据结构那么多章节,到底优先复习哪块”,我的建议非常直接:线性表之外,先把二叉树吃透,再谈图论和查找排序。这不是我拍脑袋得出的结论,而是刷过几所不同层次院校的机试真题后得到的直观感受。以我统计的20套计算机复试机试题目为例,涉及树结构的有12套,占比60%,其中二叉树相关的题目接近80%。这个比例已经不算“可能考”的范畴了,而属于“大概率会遇到”的必考点。

二叉树之所以受出题老师偏爱,本质上是因为它非常考验编程基本功。它天然具备递归结构,能够检验一个人对递归调用栈的理解程度;它又有“遍历”“重建”“深度”“公共祖先”等多个独立命题点,每一道都能从基础版升级到进阶版,区分度很高。你在机试里写不写得出来、写得好不好,直接决定你和竞争者之间的差距。面试中二叉树也是高频区,因为面试官可以顺着“遍历”一路问下去:递归实现、迭代实现、Morris遍历、为什么要用栈、能不能用队列……一个问题串起一整片知识网络,远比孤立的八股题更有考察价值。

1.2 Day9的学习目标与内容范围

基于上述判断,我把Day9的学习内容定得比较满,但也是可执行的:

  • 手写三种深度优先遍历(先序、中序、后序),递归版和非递归版都要过关;
  • 层序遍历,熟悉队列在这种场景下的应用;
  • 由中序+先序/后序序列重建二叉树,这是机试里经常挂靠的考点;
  • 五道高频算法题:求深度、判断平衡树、求最近公共祖先、之字形层序遍历、层序序列重建;
  • 面试八股速记:四种遍历对比、递归与栈的关系、二叉树与二叉搜索树、堆之间的区别。

这套组合覆盖了“原理理解—代码实现—题目应用—面试表达”四个层级,一天复习完压力不小,但如果你的递归基础比较牢,半天可以过完,剩下半天用来刷题和整理错误清单。我建议不要把时间平均分配,而是把精力优先放在重建二叉树和最近公共祖先上,这两道题最容易在笔试和面试之间来回切换考察,难度也适中。

2. 核心知识点拆解:从三种遍历到二叉树重建

2.1 节点定义与三种遍历:递归的“访问时机”是关键

很多同学对三种遍历的代码背得出,但一问“中序和前序到底差在哪”就答不上来。这里我给出一个理解核心:遍历顺序不同,不是代码结构不同,而在于递归过程中对当前节点的访问时机不同。

先看二叉树的节点定义,机试里最常用的是C++结构体写法:

struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} };

如果只定义了一个带int参数的构造器,那么创建叶子节点就得写成new TreeNode(5),左右指针会自动初始化为空指针。这在平时刷题时能省下很多重复代码,值得养成默写习惯。

三种遍历的递归写法:

void preorder(TreeNode* root) { // 先序:根 -> 左 -> 右 if (root == nullptr) return; cout << root->val << " "; // 访问时机:递归之前 preorder(root->left); preorder(root->right); } void inorder(TreeNode* root) { // 中序:左 -> 根 -> 右 if (root == nullptr) return; inorder(root->left); cout << root->val << " "; // 访问时机:左递归之后、右递归之前 inorder(root->right); } void postorder(TreeNode* root) { // 后序:左 -> 右 -> 根 if (root == nullptr) return; postorder(root->left); postorder(root->right); cout << root->val << " "; // 访问时机:两次递归之后 }

看到差别了吗?三段代码的结构完全一样,唯一改动的是中间那一行输出的位置。面试里很多同学一紧张把三种遍历背混,根本原因就是没有抓住“访问时机”这四个字。先序是“一进来就处理根”,中序是“左边处理完再处理根”,后序是“两边都处理完才处理根”。这样理解后,哪怕换一棵复杂的树,你也能一边画递归展开图一边写代码,而不是死记。

2.2 递归与迭代:怎么用栈模拟“前中后序”

机试里面试官不会只满足于递归版,随手就会问“非递归怎么写”。非递归遍历的核心思路是用栈模拟函数调用栈,把系统为递归开辟的栈帧改成显式栈。

中序非递归的写法是最经典的,也是最值得反复练习的:

void inorderIterative(TreeNode* root) { stack<TreeNode*> st; TreeNode* cur = root; while (cur != nullptr || !st.empty()) { while (cur != nullptr) { // 一路往左压栈 st.push(cur); cur = cur->left; } cur = st.top(); st.pop(); // 弹出一个,访问它 cout << cur->val << " "; cur = cur->right; // 转向右子树,继续循环 } }

这个版本的核心理解点在于cur = cur->right这行。很多同学会问:如果右子树为空怎么办?循环会继续尝试while (cur != nullptr),发现为空就直接跳到弹出的逻辑,这样下一次从栈中弹出的就是上一层节点,恰好符合中序“先左、后根、再右”的推进方式。

先序非递归可以换个思路,直接在压栈时访问:

void preorderIterative(TreeNode* root) { if (root == nullptr) return; stack<TreeNode*> st; st.push(root); while (!st.empty()) { TreeNode* node = st.top(); st.pop(); cout << node->val << " "; if (node->right) st.push(node->right); // 注意先右后左 if (node->left) st.push(node->left); } }

因为栈是后进先出,想要先访问左子树,就得把右子树先压栈。这个细节我见过不少同学写反,结果输出的遍历顺序直接乱了。

后序非递归最麻烦,常见写法是双栈或加prev标记。我一般推荐一个“取巧但不失格调”的方式:用先序的变体,根-右-左,然后反转结果:

vector<int> postorderIterative(TreeNode* root) { vector<int> res; if (root == nullptr) return res; stack<TreeNode*> st; st.push(root); while (!st.empty()) { TreeNode* node = st.top(); st.pop(); res.push_back(node->val); if (node->left) st.push(node->left); // 注意这里和先序相反 if (node->right) st.push(node->right); } reverse(res.begin(), res.end()); return res; }

这个写法的正确性在于:先序的“根-左-右”反转后得到“右-左-根”,而我们用“根-右-左”压栈,反转后刚好是“左-右-根”,正好是后序。机试时如果时间紧张,这个方案能省不少调试心力,但面试时仍然要能讲清楚双栈法的思路,不然会被追问到死角。

2.3 层序遍历与完全二叉树判断:队列的现实应用

层序遍历在“广度优先”场景里几乎是唯一解,它依赖队列的先进先出特性:

void levelOrder(TreeNode* root) { if (root == nullptr) return; queue<TreeNode*> q; q.push(root); while (!q.empty()) { TreeNode* node = q.front(); q.pop(); cout << node->val << " "; if (node->left) q.push(node->left); if (node->right) q.push(node->right); } }

队列每弹出一个节点,就把它的左右孩子加进去,天然实现了“同一层从左到右、层与层之间从上到下”的访问顺序。如果还需要区分每一层的边界,可以每次在进入while时先取int size = q.size(),然后循环处理这一个尺寸,这样就能逐层输出。

层序的经典变形是判断完全二叉树。这个知识点看上去冷门,但机试曾经考过。核心判断方法是:层序遍历过程中,一旦遇到一个空节点,那么队列里后面出现的所有节点都必须为空;如果后面还有非空节点,就不是完全二叉树。用代码表达就是给空节点也入队:

bool isCompleteTree(TreeNode* root) { queue<TreeNode*> q; q.push(root); bool end = false; while (!q.empty()) { TreeNode* node = q.front(); q.pop(); if (node == nullptr) { end = true; } else { if (end) return false; q.push(node->left); q.push(node->right); } } return true; }

这里要注意,空指针也继续进入队列,遇到空节点就把标志位置为true,之后再遇到非空节点就直接判false。如果你已经习惯给入队前加空指针判断,在这里要改掉,否则判断逻辑会失效。

3. 由遍历序列重建二叉树:一道必须会的经典题

3.1 为什么必须要有中序遍历

机试里有一类题目是“给定先序和中序,重建二叉树”,或者“给定中序和后序,重建二叉树”。这里面有个面试必问的点:为什么先序+后序不能唯一确定一棵二叉树?

答案很简单:因为先序和后序都只能体现父子关系,但无法区分左右子树。举个例子,一棵只有左孩子的树和一棵只有右孩子的树,先序序列都是“A B C”,后序序列也都是“C B A”,但形态完全不同。而中序序列能提供“根节点左侧一定是左子树、右侧一定是右子树”的划分信息,从而把树的形状唯一确定下来。所以“由前序+后序”重建二叉树是不成立的,只有在满二叉树等特殊情况下才可能。

这个“为什么”属于概念认知层面的考察。面试时如果能答得干脆并且补充这个反例,面试官一般会认为你的数据结构基础是扎实的,而不是只会背题。

3.2 手写重建代码与区间边界推导

重建二叉树的核心思路是递归切分:从先序序列中取第一个节点作为根,再在中序序列中找到这个根的位置,根的左边是左子树的中序序列,右边是右子树的中序序列;左右子树的长度也就能算出来,再回头从先序序列里切出对应的左右子树先序序列。如此递归下去。

给出一份可以直接AC的C++实现,使用中序+先序:

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { unordered_map<int, int> pos; int n = inorder.size(); for (int i = 0; i < n; i++) pos[inorder[i]] = i; return build(preorder, 0, n - 1, inorder, 0, n - 1, pos); } TreeNode* build(vector<int>& pre, int preL, int preR, vector<int>& in, int inL, int inR, unordered_map<int, int>& pos) { if (preL > preR || inL > inR) return nullptr; int rootVal = pre[preL]; TreeNode* root = new TreeNode(rootVal); int rootPos = pos[rootVal]; int leftSize = rootPos - inL; root->left = build(pre, preL + 1, preL + leftSize, in, inL, rootPos - 1, pos); root->right = build(pre, preL + leftSize + 1, preR, in, rootPos + 1, inR, pos); return root; }

最需要记牢的是左子树长度的计算:leftSize = rootPos - inL。然后在先序序列里,左子树的范围是[preL + 1, preL + leftSize],右子树从preL + leftSize + 1开始。这个区间推导我自己第一次写的时候错了好几轮,反复在边界上撞墙。如果你在机试时忘记,最快的验证方式就是带入一个只有两个节点的例子手动走一遍,把每一步的preLpreRinLinR列出来,马上能发现错在哪。

3.3 复杂度分析与哈希优化

如果不做任何优化,重建过程中每次都要在中序序列里线性查找根节点位置,时间复杂度是O(n^2)。复试机试的树节点数量通常不大,O(n^2)可能能过,但一旦节点数量到达一万级别就会超时,所以建议养成先用哈希表记录中序下标的习惯,这样查找根节点位置变成O(1),整体复杂度下降到O(n)。

哈希表的类型是unordered_map<int, int>,key是节点值,value是中序数组下标。需要注意的是,如果题目给定的节点值有重复,这种方法会失效。不过考研机试里的二叉树节点值一般都是唯一的,真题基本不会刻意设置重复值来为难人。如果遇到重复值,就需要退回到查找法或者通过额外信息区分,但概率极低,你可以把它当成一个备选项来了解。

4. 机试实战:五道高频题与我的调试记录

4.1 求二叉树深度:先写出最简单也能过的边界

这道题虽然简单,但适合用来检查递归三要素是否到位:

int maxDepth(TreeNode* root) { if (root == nullptr) return 0; return max(maxDepth(root->left), maxDepth(root->right)) + 1; }

递归三要素在树这类结构上体现得特别明显:终止条件是空节点返回0,状态参数是当前节点,返回值是当前子树的最大深度。每一层递归只关心两件事:左子树深度、右子树深度,然后取最大值加1。这个过程非常像搭积木,从叶子节点开始一层层向上返回已知深度,最后汇总到根节点。

不少同学刚上手时会在叶子节点的处理上纠结:叶子节点的左右孩子都是空,返回0,那么叶子本身返回max(0, 0) + 1 = 1,逻辑上是通畅的。这道题的难点其实不在递归版,而在“非递归版怎么求深度”,可以用层序遍历,每处理完一层计数器加1,本质上把“层数”和“深度”对应起来了。机试时如果像避免递归爆栈,可以优先考虑这个写法。

4.2 判断平衡二叉树:从O(n^2)到O(n)的关键一步

平衡二叉树的定义是每个节点的左右子树高度差绝对值不超过1。初次接触的同学很容易写出“从上到下”的递归:

int height(TreeNode* root) { if (!root) return 0; return max(height(root->left), height(root->right)) + 1; } bool isBalanced(TreeNode* root) { if (!root) return true; int diff = abs(height(root->left) - height(root->right)); return diff <= 1 && isBalanced(root->left) && isBalanced(root->right); }

这个版本的写法非常直观,但问题在于同一个节点会被height反复计算多次,时间复杂度是O(n^2)。在节点数量达到几千的时候尚可接受,但复试机试的测试用例有时会把树构造得很深,O(n^2)可能卡得很紧。

优化思路是改成自下而上,用后序遍历处理:左右子树先算高度,如果不平衡直接一路返回-1,如果平衡则返回本层高度。代码如下:

int heightOrNot(TreeNode* root) { if (root == nullptr) return 0; int leftH = heightOrNot(root->left); if (leftH == -1) return -1; int rightH = heightOrNot(root->right); if (rightH == -1) return -1; if (abs(leftH - rightH) > 1) return -1; return max(leftH, rightH) + 1; } bool isBalanced(TreeNode* root) { return heightOrNot(root) != -1; }

这个写法的优雅之处在于,用-1同时表达了“不平衡”和“终止递归”两层含义,省去了额外定义全局变量判断状态的麻烦。从O(n^2)降到O(n),是这道题里最能体现数据结构功底的一步,面试被追问优化时你如果能主动说出这个版本,印象分会明显不一样。

4.3 最近公共祖先:递归返回值的含义想清楚了吗

最近公共祖先(Lowest Common Ancestor, LCA)是我在Day9里反复练的一题,因为它的递归逻辑不复杂,但“返回值语义”很容易想岔。

题目要求:给定一棵二叉树和两个节点p、q,找到它们的最近公共祖先。递归写法如下:

TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { if (root == nullptr || root == p || root == q) return root; TreeNode* left = lowestCommonAncestor(root->left, p, q); TreeNode* right = lowestCommonAncestor(root->right, p, q); if (left != nullptr && right != nullptr) return root; return left != nullptr ? left : right; }

理解这段代码的关键在于你要想清楚lowestCommonAncestor的返回值到底代表什么。递归在每一层会做三种判断:如果当前节点正好是p或q,说明找到了目标节点,直接返回当前节点;如果左右递归都返回非空,说明p和q分别位于当前节点的左右子树中,那当前节点就是答案;如果只有左边非空,说明两个节点都在左子树方向,那就把左子树的结果继续向上传。

我建议用一棵三层的小树手动走一遍这个递归过程,把每一次函数调用的“输入节点”和“返回内容”写在纸上。走两遍之后你会发现,代码里的每一行都对应着一个非常自然的状态,而不是靠死记。

4.4 之字形层序遍历:BFS加一层标记就够了

之字形遍历(z字形)要求奇数层从左到右、偶数层从右到左,本质上是层序遍历的变形。一种简单可行的方案是:先按普通层序遍历把每一层存成一个数组,然后根据层号判断是否需要反转。另一种是使用双端队列,但机试时我倾向于写更直观的数组反转版:

vector<vector<int>> zigzagLevelOrder(TreeNode* root) { vector<vector<int>> res; if (!root) return res; queue<TreeNode*> q; q.push(root); bool leftToRight = true; while (!q.empty()) { int sz = q.size(); vector<int> level(sz); for (int i = 0; i < sz; i++) { TreeNode* node = q.front(); q.pop(); int idx = leftToRight ? i : sz - 1 - i; level[idx] = node->val; if (node->left) q.push(node->left); if (node->right) q.push(node->right); } leftToRight = !leftToRight; res.push_back(level); } return res; }

这个写法会在每一层根据方向标记决定元素放入数组的哪个位置,免去了最后统一反转的开销。机试里这种“边遍历边定向填充”的技巧也能帮你少写一行reverse,虽然不影响AC,但能减少出错点。另有一个需要注意的地方:sz必须在进入for循环前就保存下来,不能在循环里用q.size(),因为队列在这个过程中会不断变化。

4.5 真实调试记录:我这几天踩过的四个坑

Day9的刷题过程中,我整理了四个踩得比较深的坑,每个都是从错误到AC的真实经历,很可能你也会遇到。

第一个坑是递归出口漏判。有一次写求深度的代码,我只判断了root == nullptr但忘了root->leftroot->right可能为空,于是递归调用传入空指针,下一秒就在访问函数里崩了。解决方式:养成每个递归函数开头先把空指针出口写掉,再写业务逻辑。

第二个坑是重建二叉树区间算错。我最开始推导时把preL + leftSize误写成了preL + leftSize + 1,结果在小数据量时碰巧能过,一到三个节点的测试用例就栈溢出。排查方式是打印递归入口的四个区间下标,肉眼就能看出区间覆盖错误。

第三个坑是平衡二叉树的O(n^2)超时。直观版代码在数据量小时完全没问题,但机试环境卡得很严的时候,一个退化成链表的树会让计算量爆炸。改成后序版本后本地跑同样的测试用例,耗时从几十毫秒降到几毫秒,效果是立竿见影的。

第四个坑是之字形遍历的层序边界。我一开始没在for循环前保存sz,而是直接循环q.size(),结果队列不断增长,循环次数失控,输出的层数翻倍。这个错误的根源是“对队列在遍历过程中的动态变化没有清醒认识”,切记先把本层节点数量固定下来再处理。

5. 面试八股速记:树的“送命题”怎么答

5.1 四种遍历方式对比记忆

面试官如果问“树有哪些遍历方式”,很多人会下意识开始背代码。其实更好的回答是先列名字,再说一句本质区别:

遍历方式访问顺序典型实现一句话记忆
先序根-左-右递归/栈先处理根,再左右
中序左-根-右递归/栈顺着左链一路压栈
后序左-右-根递归/双栈/反转两次递归之后处理
层序逐层从左到右队列一圈一圈往外扫

面试时如果能精确说出后序非递归的三种实现方式,顺便提到“先序反转”这个小技巧,会显得你对细节有把握。不要只扔一句“遍历就是按照某种顺序访问节点”,这种回答等于什么都没说。

5.2 为什么中序遍历和二叉搜索树绑在一起

二叉搜索树(BST)的性质是左子树所有节点值都小于根,右子树所有节点值都大于根。这导致它的中序遍历结果一定是递增序列,因此面试常考“判断一棵树是不是BST”以及“BST中第k小的元素”,前者通常用中序序列是否严格递增来判断,后者用中序遍历加计数器。

这里有个容易忽略的细节:判断BST时不能用“左节点 < 根 < 右节点”这种局部比较,因为BST要求的是整个左子树都小于根,而不仅仅是左孩子。正确的检查方式是从上往下传一个取值范围区间(min, max),递归时左子树更新max为当前节点值,右子树更新min为当前节点值。如果某个节点超出了区间,就说明结构不合法。这个知识点是面试中常见的陷阱题,我在Day9的八股速记里把它列为优先背诵对象。

5.3 递归与非递归的边界:会不会爆栈、怎么答

面试官问到“递归和非递归你怎么选”,不要只说“递归简洁但可能爆栈”这种模糊答案。你可以补上几个关键信息:二叉树比较平衡时递归深度大概是O(log n),一般不会有问题;但如果树退化成链状结构,递归深度会到O(n),在千万级节点时确实可能栈溢出。此时优先使用显式栈来模拟递归,或者考虑层序遍历。

另外有些机试环境会限制递归深度,比如有默认栈大小限制,如果你觉得上下文里递归深度不可控,可以提前写一个vector<TreeNode*>模拟栈的方案。我一般会先写递归版作为可读性最好的基准实现,再在递归深度不可控时升级为迭代版,这样能在“思路清晰”和“运行稳定”之间取得平衡。

5.4 二叉树、二叉搜索树、堆:别把三者搞混

这三者很容易被混为同一个概念,但面试里考察点完全不同。二叉树的定义最宽泛,只要每个节点最多两个子节点就是二叉树;二叉搜索树在二叉树基础上加了有序性约束,因此查找、插入、删除都可以利用大小关系进行;堆则是一种特殊的完全二叉树,它只关注“父节点和子节点的相对大小关系”,而不是整个树的全局有序性,所以堆适合实现优先队列,而BST适合实现有序集合。

面试官有时会追问“堆排序和BST排序有什么区别”。堆排序只保证堆顶是最大或最小值,但不出全局递增序列;BST的中序是全局有序的,但建树和维护平衡的成本更高。两者各有适用场景,这个对比如果能在复试时主动讲清楚,会让面试官觉得你不是背题的选手,而是真正理解数据结构设计意图的人。

6. 复盘:Day9自测清单与后续安排

6.1 今天的内容是否真的吃透了:自测题

复习了一整天之后,我给自己列了一份自测清单,不看书、不看笔记,直接口头回答或者手写代码,今天的内容才算真正消化:

  • 能在5分钟内手写三种递归遍历和先序中序的迭代版吗?
  • 能解释“为什么先序+后序不能唯一重建二叉树”并用反例说明吗?
  • 能直接从“中序+后序”序列手写重建二叉树,并说清区间边界吗?
  • 能写判断平衡二叉树的O(n)版本,而不是只会O(n^2)的直观版吗?
  • 最近公共祖先的递归返回值含义,能用自己的话讲明白吗?
  • 之字形层序遍历,如果不用反转函数,应该怎么填充数组?能说清sz为什么必须提前保存吗?
  • 面试被问“完全二叉树怎么判断”,能否立刻联系到层序遍历和空节点标志位?

这七条如果全部能脱口而出,Day9就算是稳扎稳打地拿下了。如果有几条模糊,我建议当天晚上不要开新专题,先把模糊的地方重新过一遍,因为二叉树是后面图论、查找树、堆排序的基础,这里欠的债后面会加倍还。

6.2 机试常见问题与排查心得

机试和平时本机调试差异最大的地方在于评测环境和输入输出格式。树相关的题目的输入通常有两种形式:一是直接以序列形式给出,要求你重建后再操作;二是通过带空标记的层序序列给出,比如用null表示空节点。这时候就需要写一个专门的从层序序列建树的工具函数,否则后面所有题都无从下手。

我给出一份常用的从vector<string>层序序列构建二叉树的代码:

TreeNode* buildFromLevelOrder(vector<string> data) { if (data.empty() || data[0] == "null") return nullptr; TreeNode* root = new TreeNode(stoi(data[0])); queue<TreeNode*> q; q.push(root); int i = 1; while (!q.empty() && i < data.size()) { TreeNode* node = q.front(); q.pop(); if (i < data.size() && data[i] != "null") { node->left = new TreeNode(stoi(data[i])); q.push(node->left); } i++; if (i < data.size() && data[i] != "null") { node->right = new TreeNode(stoi(data[i])); q.push(node->right); } i++; } return root; }

这段工具代码在机试中用途极广,可以说是二叉树客观题的“万能入口”。建议平时练习时就把它写在本地模板里,节省每次重写的时间。机试时间紧张,5到10分钟的时间差可能就是一道AC和一道TLE的区别。

6.3 一点个人体会与Day10的调整

按照我原本的计划,Day10应该直接进入图论的BFS/DFS。但Day9结束后我复盘了一下,发现图论的很多技巧其实与树高度重叠,比如DFS的思想、层次遍历的思想,从树切到图更多是“加一个visited数组”的事情。所以我决定把Day10调整为“图论基础与树形DP入门”,然后在Day11再回头补一轮二叉树错题。

这个调整其实来自一个学习原则:不要让新知识掩盖未消化的旧知识。Day9的题量不算少,但如果第二天马上换到完全不同的专题,人脑的记忆巩固会非常差。相反,图论里天然包含大量树结构,可以顺手巩固树的操作,同时把知识面扩展开,效率更高。

最后再分享一个Day9实测有效的小技巧:复习二叉树时一定要养成“画递归展开图”的习惯。遇到陌生的递归题,慢下来,拿一张草稿纸,把一个三层的树递归调用过程完整展开一遍。很多看起来玄妙的递归代码,展开一遍之后你会觉得每一个返回值都清楚地落位了,之后再遇到同类题就不需要画图了,因为这种模式已经固化在你的直觉里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 18:03:28

电动商用车驱动力分配控制模型:TruckSim与Matlab联合仿真实践

前阵子做电动商用车动力学控制方向的仿真验证&#xff0c;最让我头疼的不是控制算法本身&#xff0c;而是怎么把一辆带双电机全驱构型的电动轻卡&#xff0c;在TruckSim2019里搭出来&#xff0c;再和Matlab2017a的Simulink模型连起来跑完整的驱动力分配逻辑。这活儿听起来像“汽…

作者头像 李华
网站建设 2026/9/9 18:03:23

6GB显存也能跑:单图生成3D模型到虚幻引擎完整工作流

开头先讲一个场景&#xff1a;你在团队里负责做一批游戏关卡摆件&#xff0c;手里只有一张概念草图&#xff0c;甲方又说“先出个白模看效果”。以往你会打开建模软件从零开始拉线&#xff0c;一个下午可能只能出一版。现在有了 AI 3D 生成方案&#xff0c;从单张图片出三维网格…

作者头像 李华
网站建设 2026/9/10 18:55:34

从App Store评论看顶部导航组件:AI时代用户体验的关键

做产品做得久&#xff0c;就会养成一个习惯&#xff1a;隔几天去App Store热门榜单逛一圈&#xff0c;看哪个品类在往上走。我以前也是盯着榜单看赛道&#xff0c;直到前阵子组里在做内容分发类应用的改版&#xff0c;我才发现一个一直存在但从来没认真看过的细节——榜单里那些…

作者头像 李华