当你第一次看到"根据后序和中序输出先序"这样的题目时,可能会觉得无从下手。别担心,这其实是一个经典的递归问题。让我用一个生活中的例子来解释:假设你有一盒乐高积木的组装说明书丢失了,只剩下拆解步骤记录(后序)和零件分类清单(中序),你需要通过这些信息还原最初的组装顺序(先序)。
关键在于理解三种遍历方式的本质区别:
在实际解题时,我发现后序遍历的最后一个元素必定是整个二叉树的根节点。这个根节点在中序遍历序列中就像一个分水岭,左边所有元素构成左子树,右边所有元素构成右子树。这种特性使得递归解法成为可能。
让我们用PTA的样例数据来具体分析:
code复制后序: [2,3,1,5,7,6,4]
中序: [1,2,3,4,5,6,7]
第一步,取后序最后一个元素4,这就是整棵树的根。在中序序列中找到4的位置(索引3),左边[1,2,3]是左子树,右边[5,6,7]是右子树。
这个划分过程可以用以下代码实现:
python复制def find_root(in_order, root_val):
for i in range(len(in_order)):
if in_order[i] == root_val:
return i
return -1 # 理论上不会发生,题目保证输入有效
递归最容易出错的地方就是边界条件的处理。经过多次调试,我总结出两个关键终止条件:
在代码中表现为:
c复制if((fin <= start)||(root<0)) return;
这里fin表示子树结束位置的下一个索引,start表示子树开始位置。当fin <= start时,表示当前子树范围为空。
左子树的后序根计算是最容易出错的部分。经过多次推导,我发现公式应该是:
code复制左子树根位置 = 当前根位置 - (右子树长度 + 1)
用具体例子说明:
对应代码中的表达式:
c复制makePre(post,min,root-fin+i,i,start); //左子树
这里root-fin+i的计算需要仔细理解:fin-i实际上就是右子树的长度。
右子树的后序根相对简单,就是当前根的前一个位置:
c复制makePre(post,min,root-1,fin,i+1); //右子树
因为后序遍历的顺序是左→右→根,所以右子树的根必定紧邻在整树根的前面。
结合以上分析,完整的C语言实现如下:
c复制#include <stdio.h>
void makePre(int post[], int min[], int root, int fin, int start){
if((fin <= start)||(root<0)) return;
int i;
int head = post[root];
for(i=start; i<fin;i++) {
if (min[i] == head) {
break;
}
}
printf(" %d",head);
makePre(post,min,root-fin+i,i,start); //左子树
makePre(post,min,root-1,fin,i+1); //右子树
}
int main(){
int num;
scanf("%d",&num);
int post[num]; //后序
int min[num]; //中序
for(int i=0; i<num;i++){
scanf("%d",&post[i]);
}
for(int i=0; i<num;i++){
scanf("%d",&min[i]);
}
printf("Preorder:");
makePre(post,min,num - 1,num,0);
return 0;
}
在实际编码中,我遇到了几个典型错误:
解决这些问题的方法就是:
每次递归都需要在中序序列中查找根节点位置,最坏情况下(树退化为链表)时间复杂度为O(n²)。对于平衡二叉树,通过预处理建立哈希表可以将查找优化到O(1),整体复杂度降为O(n)。
递归调用栈的深度取决于树的高度,最坏情况下(链表)为O(n),平均情况下(平衡树)为O(logn)。除了递归栈外,算法只需要常数级别的额外空间。
虽然递归解法直观简洁,但了解非递归实现也有其价值。理论上可以使用栈来模拟递归过程:
不过非递归实现的下标计算会更加复杂,维护多个栈来保存各种索引状态。在实际面试或考试中,除非明确要求,否则推荐优先使用递归解法。
这种遍历序列转换技术在多个领域有实际应用:
常见的变种问题包括:
我在处理一个文件系统恢复项目时,就曾运用类似的递归技术,通过目录的删除记录(类似后序)和原始结构快照(类似中序)来恢复误删的文件结构。