问题描述:
1.给定一棵二叉树,要求分层遍历二叉树(从上到下按层次访问二叉树,每一层单独输出一行),访问顺序为由左至右。
2.打印二叉树某层次节点(从左至右),其中根节点为第0层,函数原型为int PrintNodeAtLevel(Node *root , int level),成功返回1,失败返回0
思路与解答:
二叉树由于本身结构的递归性,常常用递归来解决。这两个问题其实是一个问题,只要解决了问题二,然后按不同层遍历一遍就可以得到问题一的结果。
那么为什么不直接用类似于先序、中序、后序遍历的递归算法直接求解问题一呢?这是因为层序遍历一棵树本身不能直接由递归算法解出,可以用反证法证明。如果能实现对 A 节点的层序递归,在对 A 节点处理的过程中,应该递归的对两个儿子 B 和 C 分别调用了层序遍历。在这种情况下,我们无法让 B 和 C 的同一个层级的儿子在集中的时间中被遍历到,换言之,B 的第一层儿子在对 B 的调用中被遍历,而 C 的第一层儿子,则在对 C 的调用中遍历,这是分离开的。不成立,得证。
//方法:输出从根节点开始算起的第k层相当于输出从根节点左右孩子算起的k-1层
void printNodeAtLevel(Node *root , int level)
{
if(root == NULL || level<0)
return ;//失败
if(level == 0)//叶子节点
cout<<root->data<<" ";
if(root->pLChild)
printNodeAtLevel(root->pLChild , level-1);
if(root->pRChild)
printNodeAtLevel(root->pRChild,level-1);
}
下面如果知道该二叉树的深度n,可以直接经过n次调用得到结果:
void printNodeByLevel(Node *root , int nDepth)
{
for(int i=0 ; i<nDepth ; i++){
printNodeAtLevel(root , i);
cout<<endl;
}
}
如果事先不知道二叉树的深度,依旧可以用递归的思想求二叉树的深度,如下:
int Max(int a , int b)
{
return a>b?a:b;
}
int calcTreeDepth(Node *root)
{
if(!root)
return -1;//依本例定义,根节点为第0层,空树的深度为-1
int maxDepth = Max(calcTreeDepth(root->pLChild) , calcTreeDepth(root->pRChild));
return maxDepth+1;
}
但是求解深度需要遍历一次二叉树,与问题二岂不是同等复杂度的问题?能不能不求深度呢?可以,只需要访问二叉树某层次失败时候返回就行了。下面是代码:
int printNodeAtLevel(Node *root , int level)
{
if(!root || level<0)
return 0;//未打印节点
if( level == 0){
cout<<root->data<<" ";
return 1;//打印一个节点
}
/*
加上if语句和不加if语句:
加上if语句,不会递归到叶子的左右孩子,仅仅判断左右孩子如果都为空就返回了,搭配递归截止条件为叶子处返回。
不加if语句,将会递归到叶子的左右孩子,此时需要在递归截止条件进行判定是否为NULL进行判断或赋值。
*/
//if(root->pLChild)
//if(root->pRChild)仅当所有该层遍历结果为0时才返回0
return printNodeAtLevel(root->pRChild,level-1)+printNodeAtLevel(root->pLChild,level-1);
}
void printNodeByLevel(Node *root )
{
for(int i=0 ; ; i++){
if(printNodeAtLevel(root , i) == 0)//整层都未打印节点,返回
break;
cout<<endl;
}
}
事实上这种方法的弊端在于每一层的访问都需要重新从根节点开始,而我们在访问第k层的时候,只需要知道它的前面一层(k-1)层的节点信息就够了。依照这种思路,考虑设定一个数组来存放每一层的信息,一旦遍历完该层,就开始遍历下一层,而下一层的信息可以通过压入当前层(k-1层)左右孩子节点(k层)来构造,而不要重新从根节点开始。下面是代码:
void printNodeByLevel(Node *root)
{
vector<Node*> vec;
int cur , last;//设定该层的起始点和结束点
vec.push_back(root);
cur = 0;last = vec.size();
while(cur<vec.size()){//循环截止条件:没有更多层了
last = vec.size();
while(cur<last){//遍历一层
cout<<vec[cur]->data<<" ";
if(vec[cur]->pLChild)//有左孩子
vec.push_back(vec[cur]->pLChild);
if(vec[cur]->pRChild)//有右孩子
vec.push_back(vec[cur]->pRChild);
cur++;
}
cout<<endl;//该层访问结束,输出换行
}
}
扩展问题:
1.如果要求深度从下到上访问二叉树,每层的顺序仍然是从左到右,应该如何改进算法?(提示:可考虑左右节点的访问顺序)
答:比较容易想到的方法是先按照层序弄好一个待遍历数组,层与层之间考虑用一个标识NULL作为分割,这样只需要反向遍历这个数组就行了,不过要注意的是弄这个遍历数组一定要先压入右节点,再压入左节点。
代码如下:
void printNodeByLevel(Node *root)
{
vector<Node*> vec;
int cur , last;//设定该层的起始点和结束点
vec.push_back(root);
cur = 0;last = vec.size();
while(cur<vec.size()){//循环截止条件:没有更多层了
last = vec.size();
vec.push_back(NULL);//作为层间分割标识
while(cur<last){//遍历一层
if(vec[cur]->pRChild)//有右孩子
vec.push_back(vec[cur]->pRChild);
if(vec[cur]->pLChild)//有左孩子
vec.push_back(vec[cur]->pLChild);
cur++;
}
cur++;//跳过NULL标志位
}
vec.pop_back();//去掉最后一个NULL
//---逆序遍历数组
for(int i=vec.size()-1 ; i>=0 ; i--){
if(vec[i] == NULL){//碰到层间分割
cout<<endl;
continue;
}
cout<<vec[i]->data<<" ";
}
cout<<endl;
}
2.如果把问题一中的左右次序颠倒,改为从右到左,应该怎么改进? 答:只需要稍加修改,将“先压入右孩子再压入左孩子”的次序改为“先压入左孩子再压入右孩子”,其他均不变。
3.[百度面试题] 要求输出二叉树第m层第k个节点值(m,k均从0开始计数) 答:思路与本例非常相似,也是考虑第m层节点相当于考虑从根孩子节点出发的第m-1层节点,然后进行引用计数或使用全局变量。代码如下:
//寻找从根节点开始第m层的第k个节点也就是寻找从根节点的孩子节点开始的第m-1层第k个节点
int findKNode(Node *root , int m , int k , int &count)//找到root为根的子树第m层第k个节点值
{
if(m<0 || !root)
return 0;
if(m==0){//找到第m层
if(k == count){//找到第k个数
cout<<root->data<<endl;
return 1;
}
count++;//如果count是指针传入,注意*count++是count指针++后再解引用,正确做法是(*count)++
return 0;//未找到第k个数
}
if(findKNode(root->pLChild,m-1,k,count) || findKNode(root->pRChild,m-1,k,count))
return 1;
return 0;
}
int nCount = 0;
int findKNode(Node *root , int m , int k )//找到root为根的子树第m层第k个节点值
{
if(m<0 || !root)
return 0;
if(m==0){//找到第m层
if(k == nCount){//找到第k个数
cout<<root->data<<endl;
return 1;
}
nCount++;
return 0;//未找到第k个数
}
if(findKNode(root->pLChild,m-1,k) || findKNode(root->pRChild,m-1,k))//左子树和右子树的第m层是否找到第k个数
return 1;
return 0;
}