软考软件设计师上午的选择题里,数据结构一直是必考模块,而二叉树在其中占比又特别高。我备考和帮人备考时最明显的感觉是:这块内容不难,但很碎——今天问遍历序列,明天问平衡因子,后天问哈夫曼编码。很多同学卡在及格线附近,往往就是树这一块有两三个考点没有真正吃透。这篇文章把我认为最核心的几组知识点串起来讲,包括形态性质、遍历与还原、二叉排序树和平衡树、哈夫曼树、线索二叉树以及树森林转换,目标是让你拿到一套能直接套用的做题方法,考前两三天也能快速过一遍。
1. 二叉树形态与性质:先把送分题捡起来
1.1 二叉树不是"每个节点最多有两个孩子"这么简单
软考上午题特别喜欢在一道题里同时考二叉树的定义和性质。你要先记住一个容易被忽略的点:二叉树不是简单的"度为2的树",它强调每个节点最多只有两个孩子,并且左右子树有严格次序,交换左右子树后就是另一棵二叉树了。这个区分在判断题里出现过很多次。
接着就是两条高频性质,几乎是每年都会露面的那种:
- 二叉树第 i 层最多有 2^(i-1) 个节点。因为每往下一层,节点数最多翻倍,从1开始,第 i 层就是 2^(i-1)。
- 深度为 k 的二叉树最多有 2^k - 1 个节点(k≥1)。这就是把每一层的最大值加起来,等比数列求和的结果。
很多同学不明白为什么要单独记一条"叶节点数等于度为2的节点数加1",也就是 n0 = n2 + 1。这个公式出现频率极高,而且它可以用很直观的方式理解:每产生一个度为2的节点,就相当于多了一条"从上面分叉下来的路",整棵树的叶子就会比分叉点个数多1。做题时直接套公式就行,不用自己去推。
我随手举个例子:一棵二叉树有11个度为2的节点,问它有多少个叶子。答案就是12,不需要看树长什么样,直接 n0 = n2 + 1。这种题就是典型的送分题,错了很可惜。
1.2 完全二叉树与满二叉树的节点计算套路
满二叉树比较好理解,每一层都是满的。真正容易出错的是完全二叉树,它的定义是"最后一层可以不满,但必须从左到右连续排列"。软考喜欢拿完全二叉树的编号做文章。
如果你用数组从1开始顺序存储一棵完全二叉树,就会得到这几个结论:
- 编号为 i 的节点,左孩子编号是 2i,右孩子编号是 2i+1;
- 它的父节点编号是 ⌊i/2⌋;
- 完全二叉树含 n 个节点时,高度是 ⌊log₂n⌋ + 1。
我记得有一年考题问的是:某完全二叉树共有700个节点,问叶子节点数。这种题看着复杂,其实有个快速结论:完全二叉树中度为1的节点最多只有1个,所以 n = n0 + n1 + n2,再结合 n0 = n2 + 1,就能解出来。700个节点的完全二叉树,叶子数为350个,度为1的节点为1个,你可以用这个结果去倒推体验一下。
再做一道常见的判断题:完全二叉树一定是满二叉树吗?反过来呢?答案是满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。这个逻辑搞清楚之后,很多选项都能一眼排除。
1.3 顺序存储与链式存储的适用边界
二叉树在计算机里有两种存储方式:顺序存储和链式存储。顺序存储就是把节点按层序放进数组,它最适合完全二叉树和满二叉树,因为空间不浪费,而且父子和兄弟关系都能用下标算出来。
但如果是普通二叉树,尤其是斜树,顺序存储就非常浪费。比如一棵深度为5、实际只有5个节点的右斜树,顺序存储可能要开 2^5 - 1 = 31 个位置,中间全是空位。这种时候就应该用链式存储,也就是二叉链表,每个节点包含 data、left、right 三个部分。
软考还经常考一个空链域数量的题:n 个节点的二叉链表,一共有 2n 个指针域,其中只有 n-1 个指针指向孩子节点,剩下的空链域就是 n+1 个。这个结论简单就是:每个节点除了根节点外都有一条来自父亲的边,所以非空指针一定是 n-1。
复习这个模块时,我建议你心里始终绷一根弦:性质公式一定要自己手动推一遍,而不是死记。考试的时候一旦选项比较模糊,理解公式的来龙去脉比硬背结论更稳。
2. 遍历序列还原二叉树:把递归切分练成肌肉记忆
2.1 前序+中序的还原原理与手推流程
这是软考二叉树里最能体现"会与不会"差距的题型。给出一棵二叉树的前序遍历和中序遍历,要求还原整棵树,或者进一步求它的后序遍历。很多同学拿到题就蒙,其实规律很简单:前序遍历的第一个节点就是根,而这个根在中序遍历里的位置恰好把左右子树分开。
举个例子,前序遍历是 ABDGHCEFI,中序遍历是 GDHBAECFI。
第一步,前序第一个 A 是根。在中序遍历里找到 A,A 左边是 GDHB,右边是 ECFI,所以左子树有4个节点,右子树有4个节点。
第二步,回到前序遍历,根 A 后面紧接着的4个节点 BDGH 属于左子树,再往后4个 CEFI 属于右子树。
第三步,对左子树重复同样的逻辑:前序 BDGH 中第一个 B 是左子树的根,中序 GDHB 中 B 在最右边,说明 B 只有左子树而没有右子树。继续分下去,D 是 B 的左孩子,G 和 H 分别是 D 的左右孩子。
右边也这样推:前序 CEFI 中 C 是右子树根,中序 ECFI 中 C 的左边是 E、右边是 FI,最后 F 是 I 的父节点。整棵树就还原出来了:
A / \ B C / / \ D E F / \ \ G H I看到没有,整个过程就是"前序定根,中序分左右",这是一个递归过程。考前我强烈建议你拿两三个例子亲手画一遍,不要只是在脑子里想,画一次之后速度会快很多。
2.2 后序+中序与层序+中序的推法差异
后序遍历和中序遍历的组合同样可以还原二叉树,但规律要反过来用:后序遍历的最后一个节点是根。还是用上面这棵树,后序遍历是 GHDBEIFCA。
第一步,后序最后一个 A 是根。中序里 A 左边 GDHB 是左子树,右边 ECFI 是右子树。
第二步,左子树的后序序列是 GHD B,其中最后一个 B 是左子树的根;中序 GDHB 里 B 在最右,说明 B 没有右子树。再细分得到 D 为 B 的左孩子,G、H 为 D 的左右孩子。
第三步,右子树的后序序列是 EIFC,最后一个 C 是根;中序 ECFI 表明 C 的左孩子是 E,右子树由 F、I 构成,而后序 EIFC 中 F 排在 I 前面且 F 是那一小段的根,所以 I 是 F 的右孩子。
层序+中序相对少一些,但也会考。层序遍历的第一个节点永远是根,剩下的节点要依靠中序来分配左右。做题思路是不断在中序里找"子树根"。因为层序本来就是从上到下、从左到右输出的,所以在给定中序后,从层序靠前的节点开始逐个放入对应子树段,就能还原。
不过,软考里层序的题目更多是选择题形式,比如给你一棵树层序遍历的结果,问中序或前序是什么。这时候你就需要理解层序与队列之间的关系。
2.3 还原题里容易踩的两个坑
第一个坑:把前序和后序搞混。前序的第一个是根,后序的最后一个才是根。这个错误在考场上特别常见,尤其是连续做两三道序列题之后,人容易机械套用。
第二个坑:认为"前序+后序"也能唯一确定一棵二叉树。这是经典误区。前序 AB、后序 BA 的时候,根是 A,但 B 可能是 A 的左孩子,也可能是 A 的右孩子,没法确定。所以只有前序和后序,无法唯一还原二叉树。软考曾在判断题里专门设过这个陷阱。
第三个坑是还原完之后没有校验。我建议你每次还原出树之后,快速用前序或中序走一遍,看能不能和题目给定序列对上。这一步看起来多花十秒,实际上能救回不少粗心分。
3. 递归遍历与非递归遍历:从背代码到画栈
3.1 三种递归遍历为什么容易混淆
二叉树的三种递归遍历,本质区别只有访问根的时机:前序是"根左右",中序是"左根右",后序是"左右根"。考概念的时候大家都会,但一旦给出具体二叉树问遍历结果,就容易出错。
我用上面那棵树做一遍输出,你对照感受:
- 前序:A B D G H C E F I
- 中序:G D H B A E C F I
- 后序:G H D B E I F C A
你会发现中序的结果特别重要,因为二叉排序树的中序遍历一定是递增序列,这是后面二叉树排序树章节的基石。考试里经常让考生根据一棵实际BST的插入过程来判断其中序遍历结果,其实只要记住"左根右"递归定义,就一定能写对。
3.2 前序和中序非递归实现的栈模板
很多资料一上来就放非递归代码,把初学者吓跑。其实非递归无非就是自己维护一个栈,模拟系统函数调用栈的进出过程。考场上虽然不一定让你写完整代码,但理解它有助于你做序列输出题。
前序非递归的模板特别简练:
根节点入栈 while 栈非空: 弹出 p,访问 p 若 p 有右孩子,右孩子入栈 若 p 有左孩子,左孩子入栈为什么先压右孩子再压左孩子?因为栈是后进先出,先压右孩子,左孩子就会先弹出,这样访问顺序才是"左子树在前、右子树在后"。
中序非递归稍微绕一点,核心思想是"一路向左走,走到不能再走就弹栈访问,然后再转向右子树":
p = 根节点 while p 不为空或栈非空: while p 不为空: 栈.push(p) p = p.left p = 栈.pop() 访问 p p = p.right你可以在纸上对树模拟几遍中序遍历的栈变化:先是连续入栈 A、B、D、G,G没有左孩子,弹出G,然后 p 指向 G 的右孩子,为空,所以继续弹D……整个过程就是递归展开的样子。
3.3 后序与层序的考试出现形式
后序非递归的模板比前序、中序都麻烦,软考很少直接考代码,但经常考"下列哪个序列不可能是后序遍历的结果"。这种题你要抓住后序遍历的核心特征:左右根,根一定是最后访问的。作为一个反例,如果一个序列的结尾是一个明明在中序里靠中间的节点,那就肯定不是后序。
后序还有一种简便记忆法:把前序遍历的"根左右"改成"根右左",然后整体倒过来,就得到后序遍历。这个技巧在一些选择题里非常好用,尤其是不用真正写出代码的时候。
层序遍历则必须用队列:根节点入队,出队时访问,然后把左右孩子依次入队。软考里有个很经典的延伸考点是判断一棵二叉树是否为完全二叉树:用层序遍历的思路,一旦遇到缺失孩子的情况,后面就不允许再出现带孩子的节点,否则就不是完全二叉树。这个判断方法比数节点个数更快,值得记下来。
4. 二叉排序树与平衡二叉树:构建、删除、旋转的核心逻辑
4.1 BST构建、查找效率的同一根主线
二叉排序树也叫二叉查找树,它的规则很直观:左子树所有节点都小于根节点,右子树所有节点都大于根节点。由此推出一个最常用的结论——中序遍历BST得到的是递增序列。这个结论在软考里几乎年年出现,要么直接考输出,要么隐藏在查找逻辑里。
构树的过程就是不断比较:从根开始,比当前节点小就走左,大就走右,直到找到空位置插入。比如依次插入 50、30、80、20、40、90、35、45,构建出来的BST就是下面这样:
50 / \ 30 80 / \ \ 20 40 90 / \ 35 45这里我想多说一句查找效率的问题。BST查找的平均比较次数等于查找路径上的节点数,因此树越"胖"越平衡,效率越高。如果一个BST退化成一条链,比如插入序列已经是完全有序的 10、20、30、40、50,那查找复杂度就是 O(n),和线性表没有区别。软考经常拿这个说事,问你"二叉排序树一定能提高查找效率吗",答案是否定的,关键看树的形态。
成功查找时的平均查找长度 ASL 是软考实打实的计算点。以上面那棵树为例,各节点层数分别是 1、2、2、3、3、3、4、4,加起来22,除以节点数8,得到ASL成功为2.75。考题里如果给你一棵现成的BST,让你算ASL,你只需要按层统计节点个数就够了。
4.2 AVL四态旋转的分类与判断
平衡二叉树(AVL)是在BST基础上增加了平衡约束:任意节点的左右子树高度差不超过1。高度差,也就是左子树高度减右子树高度,叫平衡因子,取值只可能是 -1、0、1。
插入节点后如果某个节点的平衡因子绝对值变成2,就要旋转调整。旋转按失衡的两个方向分成四类:
| 类型 | 典型形态 | 调整方式 |
|---|---|---|
| LL | 失衡节点的左子树的左子树插入 | 对失衡节点做右旋 |
| RR | 失衡节点的右子树的右子树插入 | 对失衡节点做左旋 |
| LR | 失衡节点的左子树的右子树插入 | 先对左孩子左旋,再对失衡节点右旋 |
| RL | 失衡节点的右子树的左子树插入 | 先对右孩子右旋,再对失衡节点左旋 |
光记这四类名称还不够,你得会判断。我拿一个简单序列举例:依次插入 15、20、12、18、10、8。
插入前五个节点时树一直保持平衡,直到插入8。此时节点12的左子树下是10再往下是8,左子树高度变2,而右子树为空,所以12的平衡因子是2,需要调整。再判断插入方向:12的左孩子是10,而8是10的左孩子,典型LL型,所以对12做一次右旋。调整后10成为这个子树的根,8和12分别是它的左右孩子,最终整棵树依然平衡。
做题时最容易错的是LR和RL。核心做法是:先把"连续下降的那条路径"找出来,如果路径先左后右,就是LR;先右后左,就是RL。旋转时不要去想复杂的结构,只需要记住LR要"两次旋转",并且第一步的旋转点是失衡节点的孩子,不是失衡节点本身。
4.3 平均查找长度的考场速算
AVL经常和ASL结合出题。因为平衡树的高度较低,成功查找的ASL通常接近 log 级别。不过考场给的树一般很小,直接按层数累加就行。
失败查找的ASL偶尔也会考。你需要知道,失败查找比较次数等于"落入外部节点时经过的内部节点数",这等于把树的所有空指针位置看作外部节点,然后逐层统计。这类题只要作图的时候把空孩子位置补成虚线方块,计算就一目了然。
我在备考时的一个体会是:BST的删除规则必须牢固掌握,因为软考有时候会把删除和插入混在一起考。删除分三种情况:叶子节点直接删;只有一个孩子就用孩子顶上来;有两个孩子时,通常用左子树的最大节点或者右子树的最小节点来替换被删节点,然后再删掉那个用于替换的节点。记住"左子树最大"这个方向就够了,因为这样替换后二叉排序树性质依然保持。
5. 哈夫曼树和哈夫曼编码:计算WPL的快捷方法
5.1 构造规则与WPL的两种算法
哈夫曼树也叫最优二叉树,它的核心指标是带权路径长度 WPL,也就是所有叶子的权值乘以路径长度的总和。构造算法本身不难,一句话:每次从集合里取出两个权值最小的节点,合并成一个新节点,新节点的权值就是两者之和,再放回集合,不断重复直到只剩一个根。
我用经典权值集合 2、3、4、7 来演示:
- 取出2和3,合并得5,集合变为4、5、7;
- 取出4和5,合并得9,集合变为7、9;
- 取出7和9,合并得16,构造完成。
最终树中,7的路径长度是1,4的路径长度是2,2和3的路径长度都是3,所以 WPL = 7×1 + 4×2 + 2×3 + 3×3 = 30。
这里我要推荐一个考场上的快捷算法:不需要计算每个叶子深度再相乘,只需要把每次合并产生的新权值再次累加。上面三步产生的中间权值是5、9、16,加起来也是30。道理很简单,每次合并一次,相当于被合并的权值整体向上走了一层,累加的中间权值正好等于所有叶子权值与深度乘积的总和。用这个办法做题,速度至少快一倍。
5.2 编码不唯一但WPL唯一的理解
哈夫曼编码就是在哈夫曼树上约定"左分支为0、右分支为1",从根到叶子的路径串起来就是该叶子的编码。上面例子里,如果规定左0右1,一种可能的编码是:
- 7 → 0
- 4 → 10
- 2 → 110
- 3 → 111
每个编码都不是另一个编码的前缀,这就是前缀码,解码时不会产生歧义。压缩效果就体现在高频字符用短码、低频字符用长码。考试会问"某字符集的哈夫曼编码长度之和是多少",本质上还是在算WPL,只不过把权值看成出现次数。
很多同学会纠结:是不是只有一棵特定的树才最优?不是。当有相同权值存在,或者合并顺序不同时,树的结构可以不一样,编码方案也不唯一,但所有方案得到的最小WPL是一样的。软考判断题特别喜欢考"哈夫曼树唯一吗",你要能马上反应过来:不唯一。
5.3 高频陷阱题汇总
哈夫曼树有几个隐藏结论,选择题直接拿来用很方便:
- 哈夫曼树中不存在度为1的节点,每个内部节点都有两个孩子。
- 有 n 个叶子节点的哈夫曼树,总的节点数是 2n-1。
- 权值越大的叶子越靠近根,路径越短。
- 哈夫曼编码是前缀码,也是可变长编码。
有一种题目会反过来问:给定编码总长度和各个频率,求某一字符的编码长度。这时候你可以先构造完整的哈夫曼树,再数对应叶子的深度。还有一种是要求你判断某一组编码是否能作为哈夫曼编码。除了看它是不是前缀码外,还要注意,哈夫曼编码对应的树必须是"无单字节乏力"的满二叉树结构,也就是每个非叶子都恰好有两个孩子。只要不满足,选项就是错的。
另外,构造时如果遇到权值相同的节点,选择不同会影响树的形状,但只要合并策略正确,WPL依然最优。做题时不必为了"标准答案"纠结。
6. 线索二叉树与树森林转换:冷门考点的快速记忆
6.1 线索二叉树的标志位和后继判定
线索二叉树看起来冷门,但软考真的会考,而且一旦考到就是不折不扣的送分题,因为题型很固定。它解决的问题是:普通二叉树在遍历时,要知道某个节点的前驱和后继必须重新遍历一遍,很麻烦。于是我们把节点的空指针利用起来,让左空指针指向前驱,右空指针指向后继。
关键在标志位。每个节点增加两个标记 ltag 和 rtag,0表示指针指向孩子,1表示指针指向前驱或后继。你必须记住这一点,因为它决定了指针的语义。
考试最常见的考法是:给一棵中序线索二叉树,问某个节点在中序遍历中的后继是谁。判断规则只有一条:如果该节点的右指针不是线索(也就是 rtag=0),那么后继是它右子树的最左下节点;如果右指针是线索(rtag=1),那么右指针直接指向的后继节点就是答案。
举个例子,前面那棵树的中序遍历是 GDHBAECFI。节点D有右子树,右孩子是H,H没有左孩子,所以D的中序后继是H;节点E没有右孩子,它的右指针被线索指到它的中序后继F。这类题画图时线索线都是虚线,看到虚线就要想到"这代表前驱或后继,而不是孩子"。
6.2 树转二叉树"左孩子右兄弟"的映射表
树和二叉树之间的转换,软考从来不考具体画图,而是考遍历对应关系。树转二叉树的核心规则叫"左孩子右兄弟":第一个孩子保留在左指针,其他孩子依次作为上一个孩子的右孩子,这样一棵多叉树就变成了二叉树。
这里最容易错的是遍历对应关系,我整理成了固定表:
| 树或森林的遍历方式 | 对应二叉树的遍历方式 |
|---|---|
| 树的先根遍历 | 二叉树的先序遍历 |
| 树的后根遍历 | 二叉树的中序遍历 |
| 森林的先序遍历 | 二叉树的先序遍历 |
| 森林的中序遍历 | 二叉树的中序遍历 |
注意,树的"后根遍历"不是对应二叉树的后序遍历,而是对应二叉树的中序遍历。这个反直觉的结论就是考题的重灾区,一定要单独强化记一下。我自己当时备考就栽过一次,后来再也没错。
6.3 森林转二叉树的链表特征
森林转换成二叉树可以理解为先把每棵树都转成二叉树,再把后续每棵树的根作为前一棵树根的右孩子串起来。所以森林转出来的二叉树,根节点一定没有左孩子以外的问题?不对,根节点一定有左孩子,除非第一棵树根本身没有孩子。
更实用的结论是:最终二叉树根节点的右链上有多少个节点,就代表原森林里有多少棵树。因为右链串起来的都是"兄弟树"。举个例子,如果森林由3棵树组成,转换后二叉树的根沿着右指针一直走,一定能经过另外两个树的根。
做题遇到"森林F对应的二叉树根节点右子树为空,说明什么"这类题目,答案就是原森林只有一棵树。这些结论整体逻辑是一致的,不要单独背,理解了"根连接兄弟树"之后,顺理成章就能写出来。
7. 考前复习路线与易错清单
7.1 知识依赖顺序与时间预算
备考二叉树模块,我不建议按教材顺序从头到尾啃,而是按照依赖关系安排顺序。我自己的复习顺序是这样的:
- 先花半天搞定形态和性质:n0 = n2 + 1、完全二叉树计算、顺序存储与链式存储;
- 再用一天主攻遍历:递归遍历、非递归栈过程、序列还原。这一块建议多画图;
- 接着用一天处理二叉排序树和平衡树:构建、删除、平衡因子、旋转;
- 哈夫曼树单独用半天,重点是WPL快速算法和前缀码判断;
- 线索二叉树和树森林转换放在最后半天,因为它们和其他章节的关联度低,背清楚两个表格就行。
如果你整体复习时间只剩两天,那就压缩第一步和第五步,把大部分时间给遍历和BST/AVL,因为真正的大分值题目集中在这两块。
7.2 我的踩坑记录与应试技巧
写到这里,我想分享几个实战中特别容易踩的点。
第一,做题习惯。还原二叉树时我喜欢用简单的括号表示法先草稿,比如 A(B(D(G,H),),C(E,F(,I))),写起来快,不占地方。这种简写方式让我在考场节省了不少时间,你不妨也试试。
第二,判断AVL旋转方向时,不要只盯着失衡节点看,而要看插入点相对于失衡节点到底在左左、左右、右左还是右右。先找失衡节点,再看它的孩子方向,最后看插入点方向,三步固定不动。
第三,算哈夫曼WPL时,如果对中间累加算法不放心,就按叶子深度相乘来验算一遍。两道算法结果一致才敢往答题卡上填。
第四,完全二叉树判断用层序遍历,不要数个数。层序遍历过程中,一旦遇到一个节点缺少左孩子或右孩子,后面所有节点都必须是没有孩子的叶子,否则就不是完全二叉树。
最后再提醒一句:复习到最后阶段,把那些最容易被绕的短句反复过几遍——"前序第一个是根,后序最后一个是根""树的后根遍历对应二叉树的中序遍历""哈夫曼树无度为1的节点""中序线索二叉树中右指针为空时指向后继节点"。这些句子在考场上一眼能判断对错,干扰选项基本就废掉了。二叉树这一块拿稳分,上午考试就能踏实很多。