题目链接
题目概述
-
给出一棵完全二叉树的层次遍历序列,打印从根节点到叶子结点的所有路径,作为右孩子的结点优先被打印;
-
判断这棵二叉树是不是一个堆,如果是堆,判断是大顶堆还是小顶堆.
解题思路
打印路径
-
打印路径应该使用DFS,从根节点开始递归向下,如果右孩子存在,那么先转向右孩子,重复这样的递归,直到右孩子为空,这样就得到了第一个遍历的路径,然后这个抵达递归基,返回上一级调用的地方,转向左子树,执行相同的动作,按照根->右孩子->左孩子的顺序进行遍历.可以用一个全局的数组保存此前已经访问的结点,在函数的结尾要有把当前遍历的结点弹出的操作,这样可以保证程序稳定的执行.
可以通过这个打印这个数组的过程来处理判断这个是不是一个堆,是大顶堆还是小顶堆.这样判断不可行,需要单独写一个程序来判断这个堆是大顶堆还是小顶堆. -
经过上面的分析,觉得好像用
value,lc,rc
三元组表示的结点构成的数更容易实现,但是题目中给出的是以\(level\quad order\quad traversal\)表示的完全二叉树的序列,好像用数组做更方便些,但是如果用数组的话,完全二叉树的根结点与左右孩子之间存在这这样的关系(索引从1开始到n):$$index(lc) = 2* index(root), index(rc) = 2* index(root) + 1$$.边界条件特别重要,由其是要注意处理那种结点只有一个左孩子的情形. -
打印从根节点到叶子结点的程序如下:
const int N = 10005;
int buf[N];
int ans[N];
int n;
/**
* root是当前结点在数组中的索引;
* index是保存路径的数组的元素的数目,也是当前结点要存放的在数组的索引.
*/
void dfs(int root, int& index){
//抵达叶子结点,打印路径.
if( root > n ){
cout << ans[0];
for (int i = 1; i < index; i++){
cout << " " << ans[i];
}
cout << endl;
// --index;
return;
}
//存储当前接结点
ans[index++] = buf[root];
//右孩子不存在,左孩子可能存在,也可能不存在.
//如果左右孩子都不存在,那么只要在选择左孩子或右孩子深入一层就可以打印这条路径,但是不能全选左右孩子,否则这条路径会打印两遍.
//如果左孩子存在,右孩子不存在,那么必须左转深入,然后情况转移到上面那种情况.
//综上,这一处必须想左转,才能打印出正确的结果.
if( 2 * root + 1 > n)
dfs(2 * root, index);
//中间过程的结点,左右孩子都存在,先右转,执行完后后再左转,确保右孩子先于左孩子打印.
else{
dfs(2 * root + 1, index);
dfs(2 * root, index);
}
//程序执行到此处,表示以这个结点为中间结点(包含末尾结点的情况)的路径已经处理完毕,要回到这个结点的父节点,所以需要将这个结点弹出,也就是回溯.
--index;
}
我觉得主要有两点需要注意:
-
使用index来计数以及表示回溯的动作;
-
要区分接结点有两个孩子,有一个孩子,以及没有孩子的情况.
-
有两个孩子的是一般的递归情形,先右孩子,再左孩子;
-
有一个孩子的话,一定是左孩子(完全二叉树),所以必须左转深入;
-
没有孩子的话,向左向右都可以,但是只能向左或向右选择其一深入.
-
判断是否为堆
- 判断是否为堆,以及是大顶堆还是小顶堆,我是根据堆的性质,写了一个冗长的递归程序:
//flag为false表示不是堆;
bool flag = true;
//m为1表示大顶堆,m为-1表示小顶堆,m为0表示暂未确定
int m = 0;
void isheap(int root){
//当前结点是叶子结点,返回
if( 2*root > n)
return;
//已经判断出不是堆,返回
if (!flag){
// m = 0;
return;
}
//当前的结点只有左孩子,没有右孩子.
if( 2 * root == n && 2*root + 1 > n){
//此前判断是大顶堆(小顶堆),但是这个局部不满足
if( m == 1 && buf[root] < buf[2*root]){
flag = false;
// return;
}else if (m == -1 && buf[root] > buf[2*root]){
flag = false;
// return;
}
//此前尚未确定,根据这个局部的情况,来判断是大顶堆还是小顶堆,这个是一种边界情况.
if( m == 0 && (buf[root] >= buf[2 * root] ))
m = 1;
else if( m == 0 && (buf[root] < buf[2 * root]))
m = -1;
return;
}else{ //一般结点,左右孩子都存在
//此前已确定是大顶堆(小顶堆),但是这个结点的局部不满足.
if( m == 1 && (buf[root] < buf[2*root] || buf[root] < buf[2*root+1])){
flag = false;
return;
}else if (m == -1 && (buf[root] > buf[2 * root] || buf[root] > buf[2 * root + 1])){
flag = false;
return;
}
//此前尚未确定,根据局部的情况判断是大顶堆还是小顶堆.
if( m == 0 && (buf[root] > buf[2 * root] && buf[root] > buf[2*root+1]))
m = 1;
else if( m == 0 && (buf[root] < buf[2 * root] && buf[root] < buf[2*root + 1]))
m = -1;
//向左向右转向深入判断.
isheap(2 * root);
isheap(2 * root + 1);
}
}
唯一要注意的是处理当前结点只有左孩子,没有右孩子的并且此前尚未确定是大顶堆还是小顶堆,最开始时没有考虑到这个情况,提交有一个测试点没通过,后面改正之后全部通过了.
-
反思
虽然根据利用完全二叉树实现的判断是不是堆,以及是大顶堆还是小顶堆的递归程序,虽然可行,但是总觉得好像太冗长了,就像上面那样,对于有些边界情况,因为没有考虑到,可能没法处理.那个递归的程序我认为是仅仅将性质转换为对应的程序语句,应该有更简洁的等价的迭代写法吧.
/**
* 从全树的根节点的左孩子开始,Min和Max用于标志堆的特性;
* 如果在某个局部buf[i/2] < buf[i]则说明这个完全二叉树不满足大顶堆,
* 如果在某个局部buf[i/2] > buf[i]则说明这个完全二叉树不满足小顶堆.
* 如果Min和Max都为false,则说明这个完全二叉树不是堆;
* 当且仅当这个完全二叉树的所有元素都相同时,Min和Max才全部是true,
* 否则Min和Max有且仅有一个为true(当这个完全二叉树是堆的时候).
*/
bool Min = true;
bool Max = true;
for (int i = 2; i <= n; i++){
if( buf[i/2] < buf[i])
Max = false;
if( buf[i/2] > buf[i])
Min = false;
}
if( Min ){
cout << "Min Heap" << endl;
}else if( Max ){
cout << "Max Heap" << endl;
}else if (!Min && !Max){
cout << "Not Heap" << endl;
}
当然也可以在Min和Max都为false时提前终止.