UVa 548二叉树的递归遍历
二叉树的遍历Traversal
n个结点就有n!个遍历方式,研究其中有规律的方式。可以联想到多维数组的排列,算数组存储地址。任何树、森林都可以和二叉树相互转化,因此会着重看二叉树。
一个二叉树的三个要素是根、左子树、右子树。抓住根的绝对位置和左、右相对位置,就知道根左右的前序,根右左的逆前序,左根右的中序,右根左的逆中序,左右根的后序,右左根的逆后序。这6种都是深度优先遍历DFS,因为它总是优先往深处访问。再加上层次遍历这种广度优先遍历,一共就有7种遍历方式。
给定前中后序遍历,推原树:围绕二叉树的三要素,找根和分两拨。因此只给前序和后序无法分两拨,无法唯一还原一棵二叉树。
二叉树采用二叉链表的存储结果,就仍然是两个类,一个结点类,一个链表类。root指针是找到第一个人住的房间就可以串套起来。
以前序遍历为例:
void A(*p) { // 遍历以p指针指向的结点为根结点的二叉树
if (p != NULL) {
①cout<<p->data<<endl; // 根结点
②A(q); // A(p->lchild);
③A(r); // A(p->rchild);
}
}
①②③三句话对应三要素。
递归,程序容易写,但是计算机难执行。每个结点都会指三次。第一次指向就输出,第二次指向是从左边回来,第三次指向是从右边退栈回来。指针从何处出发,一定回到何处。指针指向谁,谁就完成了,退栈。(刚进来,一战,二战)->退栈。这些都是计算机中的“废话”,一句都不能少,都至关重要。
所有的递归都可以换成迭代,还有一个公式可以这样做。通过分析递归程序工作过程,可以得到非递归版本程序。
指针偏好是往左走,逮住机会往左走,实在空了才往右做。
do {
while (p != NULL) {
cout<<p->data;
push(p);
p=p->lchild; // 往左走
}
if (栈不空) { // 实在不行走右边
p=pop(s);
p=p->rchild;
}
} while ((栈不空) || (p != NULL));
中序就是把cout换个位置。
对比,递归版和循环版都存在堆栈,只不过循环版节约了堆栈,而且没有第三次指向。
后序遍历非递归算法是唯一一个不得不被迫使用goto语句的,如果不用,程序会很复杂。
写程序要注意,自顶向下,逐步求精;尽量采用结构化模块(单输入、单输出、无死循环、无死语句);清晰第一,效率第二;尽量少用goto语句;拿模块换模块。
应用二叉树遍历有很多实例,比如统计结点个数,统计叶结点个数,算二叉树高度。
所有递归程序都有条件(if)。嵌套调用,相互调用。从哪出发最后会回到哪儿,条件圈,堆栈圈,if结束,堆栈结束。
对于这道题目来说,首先根据中序遍历和后序遍历构造出这棵树,再搜索出从根结点到叶结点的一条最短路径。或者边构造边找。
连续读入未知个数的整数
int read_list(int* a) {
int n = 0;
char *p = s;
int cnt = 0;
for (;;) {
int r = sscanf(p, "%d%n", &a[cnt], &n);
if (r != 1) break;
cnt++;
p = p + n;
}
return cnt;
}
&n表示当前读的字符,下步跳多少,r就是返回值,读了多少整数。
分治法建树
采用静态结构,需要给每个结点分配房间。可以按照cnt的顺序一个一个来,该题目的特点是权值各不相同而且范围是(0,10000),所以房间号就是权值,权值就是房间号,可以省去一个data数组。
左子树遍历完了才会遍历右子树,所以左右子树各自是连在一起的。首先从后序遍历里面找到根,接着从中序遍历里面从左往右走到根,找到左子树,同时找到左子树的结点个数,再继续走就是右子树。后序遍历是左右根,所以从左往右走完那么多个结点个数就是左子树,再继续直到根之前就是右子树。
判空条件就是中序遍历序列,中序遍历连一个结点都没有了,就堆栈回去。
// 中序遍历根据根划分两拨,后序遍历找根(分治)
int build(int L1, int R1, int L2, int R2) { // L1-R1是中序遍历指针,L2-R2是后序遍历指针,对应二叉树
if (L1 > R1 || L2 > R2) {
return 0;
}
int root = post_order[R2];
int p = L1;
while (in_order[p] != root) {
p++;
}
int cnt = p - L1; // 左子树的结点个数
lchild[root] = build(L1, p - 1, L2, L2 + cnt - 1);
rchild[root] = build(p + 1, R1, L2 + cnt, R2 - 1);
return root;
}
搜索最优路径——递归遍历
void traverse(int root, int sum) {
if (root == 0) {
return;
}
sum = sum + root;
if (lchild[root] == 0 && rchild[root] == 0) {
if (sum < best_sum || (sum == best_sum && root < best_leaf)) {
best_leaf = root;
best_sum = sum;
}
}
traverse(lchild[root], sum);
traverse(rchild[root], sum);
}
完整代码
#include <cstdio>
#include <cstring>
#include <sstream>
#include <algorithm>
using namespace std;
const int MAXN = 10010;
char s[100010];
int in_order[MAXN];
int post_order[MAXN];
int lchild[MAXN];
int rchild[MAXN];
int best_leaf, best_sum;
bool is_blank(int c) {
return c == '\n' || c == '\r' || c == '\t';
}
void Init() {
best_sum = 0x3f3f3f3f;
memset(lchild, 0, sizeof(lchild));
memset(rchild, 0, sizeof(rchild));
return;
}
int read_list(int* a) {
int n = 0;
char *p = s;
int cnt = 0;
for (;;) {
int r = sscanf(p, "%d%n", &a[cnt], &n);
if (r != 1) break;
cnt++;
p = p + n;
}
return cnt;
}
// 中序遍历根据根划分两拨,后序遍历找根(分治)
int build(int L1, int R1, int L2, int R2) { // L1-R1是中序遍历指针,L2-R2是后序遍历指针,对应二叉树
if (L1 > R1 || L2 > R2) {
return 0;
}
int root = post_order[R2];
int p = L1;
while (in_order[p] != root) {
p++;
}
int cnt = p - L1; // 左子树的结点个数
lchild[root] = build(L1, p - 1, L2, L2 + cnt - 1);
rchild[root] = build(p + 1, R1, L2 + cnt, R2 - 1);
return root;
}
void traverse(int root, int sum) {
if (root == 0) {
return;
}
sum = sum + root;
if (lchild[root] == 0 && rchild[root] == 0) {
if (sum < best_sum || (sum == best_sum && root < best_leaf)) {
best_leaf = root;
best_sum = sum;
}
}
traverse(lchild[root], sum);
traverse(rchild[root], sum);
}
int main() {
int first = 1;
while (fgets(s, 100010, stdin) != NULL) {
if (s[strlen(s) - 1] == '\n') {
s[strlen(s) - 1] = 0;
}
if (is_blank(s[0]) || s[0] == '\0') {
continue;
}
if (first == 1) {
int node_num = read_list(in_order);
Init();
first = 0;
continue;
}
int node_num = read_list(post_order);
first = 1;
build(0, node_num - 1, 0, node_num - 1);
traverse(post_order[node_num-1], 0);
printf("%d\n", best_leaf);
}
return 0;
}