二叉树OJ入门:从递归思维到遍历模板的实战指南
我这几年带新人刷OJ几乎每次都从二叉树开始。原因很简单在数组、链表、栈这些线性结构之后二叉树是第一个真正要求你转换思维的考点——它让你接触递归、理解分治、习惯从整体到局部的思考方式。而且在华为OJ、东华OJ、GESP六级这些平台或考试里二叉树基础题型的出现频率高得离谱从遍历到深度、从搜索二叉树到满二叉树几乎是必考范围。这篇文章我就以过来人的身份把二叉树在OJ入门阶段最常见的题型、对应的模板代码、以及我踩过坑之后的经验沉淀全部摊开讲清楚。内容不追求炫技主打一个照着写能AC理解了能变通。1. 为什么二叉树是OJ入门的第一道分水岭1.1 从线性思维到递归思维的转变数组、链表、字符串这些线性结构刷题套路相对固定双指针不够加哈希表哈希表不够上滑动窗口。思维路径是直的从前往后走一遍就完事。但二叉树一出现情况立刻变了。树的结构天然是分层的、递归的你没法用一层for循环从头走到尾。你必须接受一件事一个问题可以被拆成两个规模更小的同类问题分别处理完再合并结果。这个过程刚开始很别扭尤其对习惯循环思维的人来说会觉得函数怎么还能调用自己不会死循环吗我在带新人时发现所有对递归的恐惧都源于对函数调用栈没有直观感受。一个递归调用本质上是把当前状态压入调用栈处理完子问题后弹栈继续。你不需要在脑子里把这个过程完整走一遍你只需要相信三件事函数签名确定了输入输出终止条件保证递归能停下单层逻辑保证处理方式正确。这就是常说的递归三要素。三要素听起来简单实际做题时经常有人第二要素就写错。比如求二叉树深度很多人想当然地认为终止条件是没有孩子节点时返回1但更稳妥的写法是节点为空时返回0。这两种写法在非空树上可能结果一样但一旦遇到空树输入前一种写法直接报错。这就是为什么我说二叉树是分水岭——它不仅考算法思路还考你对边界条件的敏感度。1.2 OJ题中二叉树的常见输入形式在真正开始刷题之前有一个很重要但经常被忽略的问题OJ题里的二叉树到底怎么表示抽象地说树自然是一个结构体/类包含值和两个孩子指针。但实际OJ输入通常不会给你这种结构而是给数组或字符串。常见的输入格式有三种第一种按层序序列给出节点值空节点用特殊标记表示比如1,2,3,null,null,4,5。这种格式最常见尤其在华为OJ这类平台里。你要做的第一步是解析字符串把树构建出来。这里需要注意null在Java里是关键字不能直接解析在Python里就是None不同语言的解析细节不同。第二种只给一个中序序列或前序中序两个序列要求重建二叉树。这种情况更绕基本属于中等题范畴构建过程本身就是考点。第三种测试框架直接给你构建好的根节点对象。这种情况最简单你只需要专注于算法逻辑不用管输入解析。很多新人卡住不是不会写递归而是被输入解析卡住了。所以我建议在本地准备一套数组转二叉树、字符串转二叉树、二叉树转字符串、打印二叉树的工具函数把它们存成模板。一旦遇到各种OJ花式输入直接调用自己的工具箱把精力留给真正的算法。这是我刷了几百道题之后得出的最强经验没有之一。from collections import deque def list_to_tree(data): 按层序列表构建二叉树None表示空节点 if not data or data[0] is None: return None root TreeNode(data[0]) q deque([root]) i 1 while q and i len(data): node q.popleft() if i len(data) and data[i] is not None: node.left TreeNode(data[i]) q.append(node.left) i 1 if i len(data) and data[i] is not None: node.right TreeNode(data[i]) q.append(node.right) i 1 return root这个工具函数我几乎在每道二叉树题里都会用到。用的时候直接从本地复制过来省去每次手写解析的时间。2. 核心基石三种基本遍历的递归与迭代实现2.1 递归实现前序、中序、后序遍历的模板写法三种遍历的递归代码差异极小只是处理根节点和递归左右子树的顺序不同。我把标准模板直接给出# 前序遍历根 - 左 - 右 def preorder_traversal(root): res [] def dfs(node): if not node: return res.append(node.val) dfs(node.left) dfs(node.right) dfs(root) return res # 中序遍历左 - 根 - 右 def inorder_traversal(root): res [] def dfs(node): if not node: return dfs(node.left) res.append(node.val) dfs(node.right) dfs(root) return res # 后序遍历左 - 右 - 根 def postorder_traversal(root): res [] def dfs(node): if not node: return dfs(node.left) dfs(node.right) res.append(node.val) dfs(root) return res注意这里我特意用了辅助函数 外部列表的写法而不是递归函数里直接返回拼接的列表。原因很简单如果用return [root.val] preorder(root.left) preorder(root.right)虽然代码更短但每次都创建新列表并做拼接时间复杂度最坏会退化到O(n²)。在OJ提交里遇到大数据量容易超时。辅助函数写法避免了频繁的内存分配同时逻辑也更清晰。在复杂度上三种遍历的时间复杂度都是O(n)因为每个节点恰好访问一次空间复杂度在最坏情况下是O(h)其中h是树高递归调用栈的深度等于树高。如果树退化成链表比如只有左孩子h等于n空间复杂度就成了O(n)这也是某些极端数据下递归版可能栈溢出的原因。2.2 迭代实现手动栈模拟系统调用OJ上了量之后你迟早会遇到递归深度限制的问题。Python默认递归深度是1000如果题目构造了一个高度超过1000的树递归版直接报RecursionError。这时候必须用手动栈的迭代版。前序遍历的迭代版是所有遍历里最好写的核心思路是根先入栈弹出即处理右先入栈左后入栈def preorder_traversal_iter(root): if not root: return [] stack [root] res [] while stack: node stack.pop() res.append(node.val) # 注意入栈顺序先右后左 if node.right: stack.append(node.right) if node.left: stack.append(node.left) return res之所以先压右孩子再压左孩子是因为栈是后进先出。我们想让左子树先被处理就必须让左孩子最后入栈。每次pop时弹出的是后入栈的左孩子这样就保证了根 - 左 - 右的顺序。这个顺序我见过不少新手写反结果输出成了根 - 右 - 左怎么看怎么别扭。中序迭代版比前序麻烦一些需要先把左侧链路全部压栈再逐个弹出并转向右子树def inorder_traversal_iter(root): stack [] res [] cur root while cur or stack: while cur: stack.append(cur) cur cur.left cur stack.pop() res.append(cur.val) cur cur.right return res这个写法的核心是理解一路向左压栈的含义。中序遍历要求先访问左子树所以必须先把根和所有左子树节点压栈直到最左边的空节点然后弹出一个节点处理把指针移到它的右子树重复同样逻辑。相当于用一个外层循环维护当前处理位置用内层循环把左侧链条全部入栈。后序迭代有个取巧的实现就是根右左 反转。因为后序是左 - 右 - 根反过来就是根 - 右 - 左。而这个顺序与前序迭代几乎一样只要把左右入栈顺序换一下def postorder_traversal_iter(root): if not root: return [] stack [root] res [] while stack: node stack.pop() res.append(node.val) # 注意这里左先入栈右后入栈 if node.left: stack.append(node.left) if node.right: stack.append(node.right) res.reverse() return res这个取巧的方法虽然不正统但代码量最小、最容易记忆OJ上能AC就行。你甚至可以把前序和后序迭代合并成一套逻辑都是一个栈入口顺序不同最后是否要反转不同。如果面试官要求标准的后序迭代法你再额外准备一个用双栈或记录访问状态的版本。但刷题阶段能用且不出错比什么都强。2.3 层序遍历队列应用的教科书场景层序遍历也叫广度优先遍历是二叉树与队列结合最紧密的题型几乎每场入门比赛都会出现它的影子。核心是维护一个队列每次处理一层的节点from collections import deque def level_order(root): if not root: return [] q deque([root]) res [] while q: level_size len(q) level_nodes [] for _ in range(level_size): node q.popleft() level_nodes.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) res.append(level_nodes) return res这段代码里最关键的细节是level_size len(q)必须在处理当前层节点之前记录下来。因为随着popleft和append操作进行队列长度一直在变化不提前记录的话你根本无法知道哪些节点属于当前层很容易把下一层的节点也混进当前层的列表里。这个细节是层序遍历翻车的最常见原因。除了基本层序遍历基于它还有几个高频变形题二叉树的右视图。只收集每一层最后一个节点返回一个数组。实现方式就是在level_nodes.append之后判断当前节点是否是本层最后一个即索引等于level_size - 1。锯齿形层序遍历。第一层从左到右第二层从右到左交替输出。实现方式是在收集完level_nodes后根据当前层号决定是否反转。每层最大值或最小值。在遍历每一层时维护一个极值变量即可。这些变形题的骨架都是层序遍历模板变化只在于收集和输出环节。建议把上面的基础模板当成肌肉记忆遇到变形题先套模板再在循环体里加条件判断。3. 必刷题型拆解从考频最高的题中提炼思路3.1 二叉树的深度与递归原子能力二叉树的深度绝对称得上OJ二叉树题的入门第一题。我见过它在华为OJ、东华OJ、GESP真题和各类笔试里反复出现。它的完整递归代码短得不像话def max_depth(root): if not root: return 0 left_depth max_depth(root.left) right_depth max_depth(root.right) return max(left_depth, right_depth) 1这段代码的推导过程值得反复体会。树的深度定义是从根节点到最远叶子节点的最长路径上的节点数。空树深度为0非空树深度等于左子树深度和右子树深度中较大的那个再加1。这其实是一种递归定义树的高度由子树的高度决定一层一层往下直到空节点作为终止条件返回0。我习惯把求深度称为树的原子能力因为后续很多题目都要依赖它。比如判断平衡二叉树就是在递归过程中不断比较左右子树高度差求二叉树直径需要在计算深度的同时更新全局变量记录路径最大值。只要把max_depth吃透这些题目只差一层包装。从这题还能延伸出一个经典变体二叉树的节点计数。它和深度的递归结构几乎一样只是把max(left, right) 1换成了left right 1。两个题可以一起刷对比着看反复加深对递归分治的理解。3.2 相同树与对称树递归返回值的建模艺术判断两棵二叉树是否相同是我强烈建议新手认真做的一道题。它虽然简单但能很好地训练你把比较这个操作转换成递归的能力def is_same_tree(p, q): if not p and not q: return True if not p or not q: return False if p.val ! q.val: return False return is_same_tree(p.left, q.left) and is_same_tree(p.right, q.right)这个递归的设计逻辑是层层递进的先处理两个节点都为空的情况这是终止条件再处理一个为空一个不为空的情况这保证了后面可以安全访问节点的值再比较当前值最后递归比较左子树和右子树。每一步都是上一步的必要补充少一个判断就会出错。对称树的判断稍微绕一点但本质相同。判断一棵树是否对称可以看作判断它的左子树和右子树是否互为镜像。于是需要一个辅助函数专门比较两个节点是否镜像对称def is_symmetric(root): def mirror(left, right): if not left and not right: return True if not left or not right: return False return (left.val right.val and mirror(left.left, right.right) and mirror(left.right, right.left)) return mirror(root.left, root.right) if root else True注意这里的比较对象发生了交叉左子树的左孩子要和右子树的右孩子比较左子树的右孩子要和右子树的左孩子比较。如果方向上搞对了对称树的判断就瞬间清晰。这类题训练的是灵活定义递归函数含义的能力而不是机械地套模板。3.3 搜索二叉树BST的判定与中序遍历的巧妙结合搜索二叉树Binary Search Tree简称BST在OJ二叉树题里几乎是一个独立专题出现的频率非常高。它的性质就三条左子树所有节点值小于根节点值右子树所有节点值大于根节点值左右子树本身也满足前两条。入门阶段最常见的BST题目是合法性判断。很多人第一反应是递归判断左孩子小于根、右孩子大于根但这种解法有漏洞因为它只保证了左右孩子与根的关系没有保证左子树里的所有节点都小于根。比如一棵树根的左孩子是5左孩子的右孩子是10根的右孩子是8——如果你只比较父子和根的关系这棵树会被误判成合法BST但实际上10大于根节点8破坏了BST定义。一个可靠的解法是利用BST的一个关键性质BST的中序遍历结果是严格递增的。所以你可以对整棵树做中序遍历在遍历过程中检查序列是否严格递增。这个思路写出来的代码既准确又简洁def is_valid_bst(root): prev None def dfs(node): nonlocal prev if not node: return True if not dfs(node.left): return False if prev is not None and node.val prev: return False prev node.val return dfs(node.right) return dfs(root)这个解法用了中序遍历的递归模板在处理根节点的位置加入判断逻辑。prev记录已经遍历过的前一个节点的值如果当前节点值小于等于prev说明不满足严格递增直接返回False。这里用是因为BST定义通常不允许重复值如果题目允许重复改成就行但绝大多数OJ默认前者。另一个在OJ里常考的BST题是从有序数组构建高度平衡的二叉树。思路是取中间元素当根左半边递归构建左子树右半边递归构建右子树。这题把二分思想和递归建树结合写一遍就能体会到分治在树结构上的魅力。3.4 满二叉树与完全二叉树的性质和识别在GESP六级以及不少高校OJ题库里满二叉树和完全二叉树的判断是必考概念题。这俩名字常常让人混淆先厘清定义满二叉树所有非叶子节点都有两个子节点且所有叶子节点都在同一层。换句话说每个节点要么是叶子要么有两个孩子不会出现只有一个孩子的情况。深度为h的满二叉树节点总数是2^h - 1。完全二叉树除了最后一层每一层都被填满最后一层的节点都集中在左侧不会出现右侧有节点而左侧为空的情况。完全二叉树不要求最后一层是满的但要求从左到右连续。满二叉树的判定逻辑很直接递归检查每个非叶子节点的左右孩子是否都存在同时所有叶子节点的深度要一致。这个可以用一次深度优先遍历完成def is_full_tree(root): def dfs(node, depth, leaf_depth): if not node.left and not node.right: if leaf_depth[0] -1: leaf_depth[0] depth return leaf_depth[0] depth if not node.left or not node.right: return False return (dfs(node.left, depth 1, leaf_depth) and dfs(node.right, depth 1, leaf_depth)) leaf_depth [-1] return dfs(root, 0, leaf_depth) if root else True完全二叉树的判定则可以利用层序遍历的一个特性当我们按层序访问时如果遇到一个空节点之后还能遇到非空节点说明这不是完全二叉树。这个思路写成代码非常自然def is_complete_tree(root): if not root: return True q deque([root]) seen_empty False while q: node q.popleft() if not node: seen_empty True continue if seen_empty: return False q.append(node.left) q.append(node.right) return True这里的关键技巧是让空节点也入队。一旦遇到空节点把标记位置位如果后面又弹出非空节点说明树的节点不是从左到右连续排列的不是完全二叉树。这个方法比用下标判断节点位置要直观得多代码也短。4. 实操过程从读题到AC的完整方法论4.1 二叉树题目的三步审题法我观察过很多同学在OJ上提交二叉树题最大的问题不是算法不会而是拿到题不审题上来就写。我建议养成三步审题的习惯。第一步明确输入形式。二叉树题目的输入到底是直接给树根节点还是给数组、字符串需要先建树如果是数组是层序数组还是某种遍历序列这个判断做错了后续所有代码都会偏。比如层序数组直接可以用层序建树函数而前序中序数组则要重建二叉树。第二步明确输出。题目要求返回数组还是整数还是布尔值函数签名写对了吗需要原地修改树吗这些决定了你的递归函数的返回值设计。返回数组的递归通常用辅助列表收集返回整数的递归需要设计好状态传递。第三步明确题目中树的性质约束。题目是否提到不包含重复值这是一棵BST二叉树是完全二叉树等这些约束给了你优化空间。比如题目明确说是BST那你查找某个值时可以用二分思路复杂度从O(n)降到O(h)就不用傻乎乎遍历整棵树了。三步走完再动手写代码。很多时候你会发现自己思路已经清楚了因为审题的过程就是帮你把递归三要素顺了一遍输入决定了函数签名输出决定了返回值约束决定了是否可以走捷径。4.2 递归三要素在实战中的应用以最小深度为例理论讲再多不如用一个实战题把递归三要素完整串一遍。这里选二叉树的最小深度。先看函数签名def min_depth(root) - int输入树根返回从根到最近叶子节点的最短路径上的节点数。再看终止条件。空树返回0这是基本操作。但这里藏着一个大坑如果root非空但只有一个孩子为空你不能直接返回min(min_depth(left), min_depth(right)) 1因为空的子树深度是0min函数会错误地取到0导致返回1而实际上从根到这个空的叶子路径根本不存在。正确的处理是判断单侧子树为空的情况def min_depth(root): if not root: return 0 if not root.left: return min_depth(root.right) 1 if not root.right: return min_depth(root.left) 1 return min(min_depth(root.left), min_depth(root.right)) 1最后是单层递归逻辑。左右子树都非空时取较小深度加1只有一个孩子时只能走存在的那条分支。这样一来终止条件和单层逻辑之间有严格的配合缺一不可。做这道题最大的启发是递归的终止条件决定了递归的正确性它不是写代码时随手补的而是要先想清楚边界情形到底应该如何返回。如果你只套max_depth的模板最小深度这题就会做错。4.3 调试技巧用四类小样例做自测OJ提交都有次数限制频繁Wrong Answer还会影响心态。我强烈建议在提交之前自己在本地运行几类样例。第一类空树。判断空树的输出是否符合题意。有的题空树深度为0有的题目可能要求返回-1。搞清楚题目的定义先测这个最简单的输入能排除很多低级错误。第二类单个节点。一棵只含根节点的树验证遍历输出、深度计算、BST判断等逻辑是否正确。比如此时is_valid_bst应该返回Truemax_depth应该返回1。第三类链状树。构造一个只有左孩子的链比如节点值依次为1-2-3-4。这棵树的高度是4用来验证递归是否因为过深而报错验证最小深度是否正确最小深度应该是1还是4取决于叶子定义如果叶子定义是没有孩子则链的末端才是叶子最小深度是4。第四类对称树和完全二叉树。用对称树跑对称判断用完全二叉树跑完全性判断结果应该与定义一致。调试时如果输出不符合预期建议先写一个打印树的工具函数把树的结构打印出来。肉眼看到树长什么样比用调试器断点更快。我常用的打印函数很简单把树转成层序列表输出def tree_to_list(root): if not root: return [] q deque([root]) res [] while q: node q.popleft() if node: res.append(node.val) q.append(node.left) q.append(node.right) else: res.append(None) while res and res[-1] is None: res.pop() return res这个函数实际上就是层序遍历的变体把空节点也输出了最后一层多余的None会被清理掉。输出是[1,2,3,None,4]这种格式眼扫一眼就能看出树的结构是否和预期一致。4.4 常见问题与排查技巧实录从我自己和带过的同学的实际经验来看二叉树OJ题提交报错的原因高度集中。我把它们整理成一个简易诊断表提交症状常见原因排查方向RecursionError / 栈溢出递归没有终止条件或输入树是链状且高度超过1000检查终止条件换迭代版遍历输出顺序错误三种遍历的左右顺序写反或层序没有用队列用小树手动推一遍遍历顺序结果和预期差1空节点终止条件应为返回0误写成返回1检查空节点分支的返回值NoneType错误没有判断子节点是否为None就访问其属性在node.left / node.right后面先判空完全二叉树判定错误只判断每个节点是否都有左右孩子没有维护已遇空节点标记改用层序空节点标记法BST判定错误只比较父子节点的大小关系改用中序遍历是否严格递增最典型的例子是某同学在华为OJ上做检查BST这题只判断了根和左右孩子的大小结果一个错误构型的树被判为合法。还有一个同学在求最小深度时没考虑单侧子树为空的情况一个小样例就把他打回原形。这些坑光看文档是记不住的自己亲手踩一遍才能刻进脑子里。所以我特别建议AC之后故意把代码改错几个地方再跑一遍样例看看错误输出长什么样。这种错误对照比单纯做题更能帮你建立debug的感觉。5. 平台差异与刷题路线的建议5.1 华为OJ、东华OJ与GESP的考察侧重点同样是二叉树题不同OJ平台的出题风格和考察重点有区别。我刷过并带人刷过多个平台之后总结出一些感受。华为OJ的题目偏工程应用风格对输入输出格式要求非常严格树的表示通常是一维字符串比如1,2,3,null,null,4,5。这意味着你在华为OJ上写二叉树题三分之一的精力要花在字符串解析和建树上。我建议提前准备好字符串转树的工具函数本地调试时多测几个格式变体比如多个连续null、尾部null省略、带空格等到了现场才不会手忙脚乱。东华OJ的题目风格更接近教材题目描述直接输入输出友好不会在序列化格式上故意刁难人更适合用来磨算法本身。如果你是想把二叉树基础打牢东华OJ的经典题变体非常值得刷。把深度、节点计数、遍历、BST判断这些基础题刷透你的底子就稳了。GESP六级作为等级考试除了算法实现还考概念理解。比如满二叉树第k层最多有几个节点、深度为h的满二叉树总节点数、完全二叉树用数组存储时的父子下标关系这些理论点要能直接说出答案并简要推导。我遇到过不少写代码很厉害的同学因为理论题没复习到位而失分这非常可惜。理论公式其实很集中满二叉树第k层节点数为2^(k-1)总节点数2^h - 1完全二叉树用数组存储时第i个节点的左孩子是2i右孩子是2i1父亲是i/2。会推这几个公式GESP树相关的理论题基本无忧。5.2 一条可以照抄的二叉树刷题路线如果有新人问我二叉树怎么刷我会给一个明确的分阶段路线。按这个顺序走两周到一个月就能搭起完整的二叉树知识框架。第一阶段遍历大关。把前序、中序、后序的递归版和迭代版各默写三遍把层序遍历模板默写三遍。目标是不经过思考直接写出代码。这个阶段就像练字先练基本笔画不能跳过。第二阶段层序变形专题。把层序输出、右视图、锯齿形遍历、每层最大值、每层平均值、层节点数统计这六道题刷完。它们共享同一个模板区别只在循环体内的逻辑适合用来体会一个模板打天下的感觉。第三阶段树的属性专题。包括最大深度、最小深度、节点总数、叶子节点数、是否平衡、是否对称、是否相同、直径长度、路径和。这些问题都是基于递归结构的统计或判断做得多了自然形成条件反射。第四阶段BST专题。包括判断合法BST、BST的查找和插入、有序数组转BST、BST第k小节点、BST的最近公共祖先。这个专题需要你掌握BST性质并灵活应用。第五阶段构建与序列化。包括从前序中序重建树、从后序中序重建树、序列化与反序列化、二叉树转成字符串表示。这个阶段属于进阶如果目标是面试或高阶竞赛值得啃如果只是入门考场前四阶段已经能覆盖大部分考点。我这个路线不是随便排的每个阶段都是后一个阶段的基础。比如属性专题里的判断是否平衡依赖求深度的原子能力BST专题里的第k小节点依赖中序遍历模板重建树依赖对遍历顺序的理解。循序渐进不跳级是刷二叉树最稳的方式。5.3 几个提升效率的做题习惯最后分享几个我坚持了多年的做题习惯不一定适用于所有人但对多数新手来说能减少无效劳动。习惯一AC之后再改题。每做完一道题不要急着跳到下一题花五分钟想一想如果题目改一个条件我的代码还能用吗比如做完最大深度立刻想最小深度怎么写然后动手改做完二叉树遍历想想如果输出方式从数组变成字符串该怎么拼接。我从这个习惯里得到的好处比盲目刷十道新题都多。习惯二统一模板内化于心。二叉树递归的框架非常固定无非是进入递归、处理当前节点、进入另一个递归。建议统一用辅助函数 外部变量的写法不要每次换一种风格。代码风格统一后你的大脑会在潜意识里更快地套用模板减少出错概率。习惯三记录复杂度。树题的递归版本时间复杂度通常是O(n)空间复杂度是O(h)迭代版本的时间复杂度也是O(n)空间复杂度在某些场景可以优化到O(1)比如Morris遍历但OJ入门阶段不用掌握。每写完一题在笔记里顺手标注复杂度。长期下来你对为什么递归版可能爆栈会形成条件反射这在笔试现场很加分。说白了二叉树这个主题难不在概念而在熟练度。递归不是靠看会的是靠写会的。你可以在外面看十篇教程不如亲手写过五十遍递归。那种某一瞬间突然通了的感觉很多刷过树题的人都经历过——在那之前你可能很痛苦在那之后你会发现所有树的题都变成了同一道题的变体只是换了个递归函数。如果你现在正卡在某个二叉树题上不妨先退一步放下那道题把三种遍历的递归和迭代代码各自默写三遍再把求深度和层序遍历的模板跑通。我几乎可以保证做完这两件事你再回头看那道让你头疼的题焦躁感会消失大半。剩下的就交给时间和每天稳定的几道题吧。