关于遍历二叉树递归算法的讨论
一、背景
为了后续有效地进行讨论,我们先引入两个算法:
// 首先是经典的先序遍历二叉树算法:
Status PreOrder(BiTree T, Status(* Visit)(TElemType e)){
if(T){
if(Visit(T->data)) // 此处 Visit 成功则 return 1
if(PreOrder(T->lchild,Visit))
if(PreOrder(T->Rchild,Visit)) return 1;
return 0;
} else return 1;
}
// 其次是判断结点 u 是否是结点 v 的子孙的算法:
// 数据结构略有不同:L[i] 和 R[i] 分别为二叉树第 i 个结点的左孩子和右孩子结点
// i = 0 表示结点为空
// 例如:int L[8] = {0, 2, 4, 6, 0, 0, 0, 0};
// int R[8] = {0, 3, 5, 7, 0, 0, 0, 0};
Status descendent(int L[], int R[], int u, int v){
if(u && v){
if(L[v]==u || R[v]==u)
return 1;
else if(descendent(L, R, u, L[v]))
return 1;
else return descendent(L, R, u, R[v]);
}else return 0;
}
这两个算法牵扯到两种常见的递归形式,我们在后面将进行讨论。
二、对算法的解释
为了便于查看,将两个算法再分别贴在下方:
Status PreOrder(BiTree T, Status(* Visit)(TElemType e)){
if(T){
if(Visit(T->data)) // 此处 Visit 成功则 return 1
if(PreOrder(T->lchild,Visit))
if(PreOrder(T->Rchild,Visit)) return 1;
return 0;
} else return 1;
}
对于我个人来说,初见这个算法时比较重要的理解点是(初见算法一定要剖析各部分的作用):
- 该函数的主流程由多层嵌套的 if 结构组成,这个结构的特点是:三个条件全部满足才能返回1,否则任意条件不满足都会返回0,并且不会继续执行后面的判断。
- 该函数的递归基是:结点是否存在,若不存在则
return 1
(可能有人会想为什么 return 1而不是0,返回值与第一点中的主流程是密切相关的,因为多层嵌套的 if 结构是顺序执行的类型,所以 return 1就可以不再递归,回溯至上一级,确保父节点在处理完左子树后能继续处理右子树) - return 0的作用:一旦结点数据无法访问,函数就会报错(返回0),错误会逐层向上传递,导致整个递归栈快速回退,避免后续无效操作
Status descendent(int L[], int R[], int u, int v){
if(u && v){
if(L[v]==u || R[v]==u)
return 1;
else if(descendent(L, R, u, L[v]))
return 1;
else return descendent(L, R, u, R[v]);
}else return 0;
}
其实本质上这两个算法所解决的问题都是类似的,判断u 是否是结点 v 的子孙等价于从 v 出发能否到达结点 u ,访问操作也可以用 u 是否是 v 的孩子来替代,从存储结构上来说,实质上也等同于一个静态的二叉链表
这个函数的关注要点其实也与上面的函数类似,那么值得我们注意的,其实是该函数的else-if 级联结构,这个结构的特点与多层嵌套的 if 可能会让人感到混乱
三、从语法本身区分
下面从这两个结构语法本身讨论
#include<stdio.h>
int if_3(int a,int b,int c){
if(a)
if(b)
if(c) return 1;
return 0;
}
int if_else(int a, int b, int c){
if(a)
return 1;
else if(b)
return 1;
else if(c)
return 1;
else return 0;
}
int main(){
for(int i = 0; i<2; ++i){
int a = i;
for(int i = 0; i<2; ++i){
int b = i;
for(int i = 0; i<2; ++i){
int c = i;
int x = if_3(a, b, c);
int y = if_else(a, b, c);
printf("%d%d%d :if_3 result:%d if_else result:%d\n",a , b, c ,x ,y);
}
}
}
return 0;
}
/*
000 :if_3 result:0 if_else result:0
001 :if_3 result:0 if_else result:1
010 :if_3 result:0 if_else result:1
011 :if_3 result:0 if_else result:1
100 :if_3 result:0 if_else result:1
101 :if_3 result:0 if_else result:1
110 :if_3 result:0 if_else result:1
111 :if_3 result:1 if_else result:1
*/
不难看出,对结果来说,我们可以对输入取反后对输出取反使得结果相同(德摩根律)
对abc取!后对return取!则等价:01111111->11111110->00000001
!(a && b && c) = !a || !b || !c
a && b && c = !(!a || !b || !c)
//那么使
int if_else(int a, int b, int c){
if(!a)
return 0;
else if(!b)
return 0;
else if(!c)
return 0;
else return 1;
}
/*
000 :if_3 result:0 if_else result:0
001 :if_3 result:0 if_else result:0
010 :if_3 result:0 if_else result:0
011 :if_3 result:0 if_else result:0
100 :if_3 result:0 if_else result:0
101 :if_3 result:0 if_else result:0
110 :if_3 result:0 if_else result:0
111 :if_3 result:1 if_else result:1
*/
//两函数结果等价
容易看出,这两种结构的逻辑过程是不完全等价的(虽然可以强行使结果等价),默认采用前两种结果不等的形式讨论:
嵌套if的逻辑是
a && b && c == 1(依次顺序执行)
a || b || c == 0(执行到0后不再执行后面语句)
if-else 级联的逻辑是
a || b || c == 1(执行到1后不再执行后面语句)
a && b && c == 0(依次顺序执行)
P.S. 这里回答一下有些人会提出的问题:if_3 和 if_else 函数的返回值是根据什么确定的?可不可以取反
int if_3(int a,int b,int c){
if(a)
if(b)
if(c) return 0;
return 1;
}
int if_else(int a, int b, int c){
if(a)
return 0;
else if(b)
return 0;
else if(c)
return 0;
else return 1;
}
在我们当前语法层面来说当然是可以的,其相当于对函数的结果取反。但在递归算法中,返回值往往是一种固定搭配,有着特定的逻辑含义。(前文递归基处其实已经提到了,后面相应位置也还会再提到)。
四、从算法层面区分
那么将这两种结构放到算法中进行理解。
Status PreOrder(BiTree T, Status(* Visit)(TElemType e)){
if(T){
if(Visit(T->data)) // 此处 Visit 成功则 return 1
if(PreOrder(T->lchild,Visit))
if(PreOrder(T->Rchild,Visit)) return 1;
return 0
} else return 1;
}
遍历算法使用嵌套 if 的思路是:
a && b && c == 1
:节点存在则遍历依次所有节点,如果节点不存在则根据基线条件return 1
回溯至上级(b 和 c 为递归函数);
a || b || c == 0
:在文章开头提到过,作用就是为函数报错;因为该逻辑是 abc 三者任意为0则返回0;一旦一个0被返回,那么所有上级函数的 if 判断中都将存在0,直至退出递归(在先序遍历二叉链表的例子中,bc 是递归函数,本质也是执行 a ,所以一旦 a 执行失败则函数将直接报错)。
Status descendent(int L[], int R[], int u, int v){
if(u && v){
if(L[v]==u || R[v]==u)
return 1;
else if(descendent(L, R, u, L[v]))
return 1;
else return descendent(L, R, u, R[v]);
}else return 0;
}
判断子孙节点算法使用 else-if 级联的思路是:
a || b || c == 1
:只要 abc 出现1,那么就直接返回1,同上,递归将直接退出;具体表现为只要我判断到了u == v
,那么就退出遍历;
a && b && c == 0
:节点存在则遍历依次所有节点,如果节点不存在则根据基线条件return 0
回溯至上级(b 和 c 为递归函数);
你是否已经发现了一些共同点。作为遍历的算法,我们并不特别关心函数的具体返回值是什么,所以上述的两种逻辑可以总结成:
在多层嵌套的 if 结构和 else-if 级联结构中:
a && b && c == flag 可以进行递归遍历
a || b || c == flag 可以直接退出递归
回顾上面 P.S. 中的内容,在此处可以很明显看到:
嵌套 if 中:a && b && c == flag;flag = 1是一种 固定搭配
else-if 级联中:a && b && c == flag;flag = 0是一种 固定搭配
但如果你想逃离这种固定搭配,那就对条件取反,在对结果取反。(那么在某种程度上好像就逃脱这种固定搭配了,但没什么人会这样干)
五、尝试
那么尝试着将上面的两种算法用另一种逻辑结构表达吧。
// if
Status PreOrder(BiTree T, Status(* Visit)(TElemType e)){
if(T){
if(Visit(T->data)) // 此处 Visit 成功则 return 1
if(PreOrder(T->lchild,Visit))
if(PreOrder(T->Rchild,Visit)) return 1;
return 0;
} else return 1;
}
// 返回值1为顺利遍历,返回值0为出现错误
Status PreOrder(BiTree T, Status(* Visit)(TElemType e)){
if(T){
if(!Visit(T->data)) // 此处 Visit 成功则 return 1
return 1;
else if(PreOrder(T->lchild,Visit))
return 1;
else return PreOrder(T->Rchild,Visit);
}else return 0;
}
// 返回值0为顺利遍历,返回值1为出现错误
// else-if
Status descendent(int L[], int R[], int u, int v){
if(u && v){
if(L[v]==u || R[v]==u)
return 1;
else if(descendent(L, R, u, L[v]))
return 1;
else return descendent(L, R, u, R[v]);
}else return 0;
}
// 返回值1为找到,0为没找到
// if
Status descendent(int L[], int R[], int u, int v){
if(u && v){
if(L[v]!=u && R[v]!=u)
if(descendent(L, R, u, L[v]))
if(descendent(L, R, u, R[v])) return 1;
return 0;
} else return 1;
}
// 返回值0为找到,1为没找到