关于遍历二叉树递归算法的讨论

关于遍历二叉树递归算法的讨论

一、背景

为了后续有效地进行讨论,我们先引入两个算法:

// 首先是经典的先序遍历二叉树算法:
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;
}

对于我个人来说,初见这个算法时比较重要的理解点是(初见算法一定要剖析各部分的作用):

  1. 该函数的主流程由多层嵌套的 if 结构组成,这个结构的特点是:三个条件全部满足才能返回1,否则任意条件不满足都会返回0,并且不会继续执行后面的判断。
  2. 该函数的递归基是:结点是否存在,若不存在则return 1(可能有人会想为什么 return 1而不是0,返回值与第一点中的主流程是密切相关的,因为多层嵌套的 if 结构是顺序执行的类型,所以 return 1就可以不再递归,回溯至上一级,确保父节点在处理完左子树后能继续处理右子树)
  3. 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为没找到
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值