编写程序对给定二叉树执行若干次删除子树操作,输出每次删除子树后剩余二叉树的中根序列。二叉树结点的数据域值为不等于0的整数。每次删除操作是在上一次删除操作后剩下的二叉树上执行。
输入格式:
输入第1行为一组用空格间隔的整数,表示带空指针信息的二叉树先根序列,其中空指针信息用0表示。例如1 5 8 0 0 0 6 0 0表示如下图的二叉树。第2行为整数m,表示要进行的删除操作次数。接下来m行,每行一个不等于0的整数K,表示要删除以K为根的子树。m不超过100,二叉树结点个数不超过5000。输入数据保证各结点数据值互不相等,且删除子树后二叉树不为空。
输出格式:
输出为m行,每行为一组整数,表示执行删除操作后剩余二叉树的中根序列(中根序列中每个整数后一个空格)。若要删除的子树不在当前二叉树中,则该行输出0(0后无空格)。
输入样例:
1 5 8 0 0 0 6 0 0
3
5
8
6
输出样例:
1 6
0
1
题解:
这题主要就是构建出一颗二叉树,然后进行遍历操作,而构建二叉树可以利用数组或结构体来实现,这里我都一一讲一下,并且说一下各自的利弊。
完全二叉树的思路
首先是用存储完全二叉树的思路去构建二叉树。node 表示当前节点,如果根节点从 0 开始算的话,那么它的左孩子就是 2*node+1,右孩子就是 2*node+2,参考下图理解一下,随便任意一个节点的编号乘以 2 再加 1就是左孩子的编号,加2就是右孩子的编号。
所以我们就可以利用一个数组,数组的下标就对应节点的编号,这种做法的好处是代码简单,容易理解,代码如下。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
int data[N], ans[N], len;
// data 存储节点的数据
// ans 储存题目要求输出的中序遍历
// len 记录ans数组中节点的个数
void build_tree(int node) {
int lnode = 2*node + 1; //计算左孩子节点的下标
int rnode = 2*node + 2; //计算右孩子节点的下标
int x;
cin >> x; //读入当前节点数据
data[node] = x;
if(x == 0) return ; //如果x == 0是结束标志,返回
build_tree(lnode); //构建当前节点的左子树
build_tree(rnode); //构建当前节点的右子树
}
bool solve(int node, int x) {
if(data[node] == 0) return false; //data[node] == 0是结束标志,返回false
if(data[node] == x) { //找到了需要切掉的子树的根,将data[node]修改为0
data[node] = 0; //之后递归到这里就会直接返回,相当于切掉了该子树
return true; //找到了要切掉的子树,返回true
}
bool flag = false;
int lnode = 2*node + 1;
int rnode = 2*node + 2;
flag |= solve(lnode, x); //等价于flag = flag|solve(lnode,x)
ans[len++] = data[node]; //记录中序遍历
flag |= solve(rnode, x); // ‘|’是或运算符,如flag = true|false,则flag = true
return flag; //返回有没有找到需要切掉的子树
}
int main() {
build_tree(0); //构建二叉树
int t;
cin >> t;
while(t--) {
int x;
cin >> x;
len = 0;
if(!solve(0, x)) cout << 0 << endl; //没有找到,输出0,行末无空格
else {
for(int i=0; i < len; i++) //找到了要切的子树,输出中序遍历,行末有空格
cout << ans[i] << ' ';
cout << endl; //记得换行
}
}
return 0;
}
上述代码写了两个函数,一个是构建二叉树,一个是对二叉树进行中序遍历,不过要注意的就是在中序遍历时,如果遍历到了要切掉的子树的根时,也要返回,因为被切掉的子树不需要遍历。OK,提交代码后完美地来了段错误。
不难发现,这题的节点个数保证不超过5000个,也就是说最坏情况下,我们需要的data数组大小是2^5000。所以打算用完全二叉树的思路水过这道题的计划失败,老老实实写个链式存储。
链式存储实现二叉树
我们要用结构体去定义一个节点,一个节点有三个属性,数据 data,指向左孩子的指针 lson,指向右孩子的指针 rson。其实就是数据结构课上教的实现方法,代码如下。
#include<bits/stdc++.h>
using namespace std;
const int N = 5e3+5;
int ans[N], len;
struct Node {
int data; //记录当前节点数据
Node *lson; //左孩子指针
Node *rson; //右孩子指针
};
Node * build_tree() {
int x;
cin >> x;
if(x == 0) return NULL; //结束标志,返回NULL
Node *p = new Node(); //申请当前节点内存空间
p->data = x;
p->lson = build_tree(); //构建左子树
p->rson = build_tree(); //构建右子树
return p;
}
void delete_tree(Node *p) {
if(p == NULL) return ;
delete_tree(p->lson); //删除左子树
delete_tree(p->rson); //删除右子树
delete p; //释放当前节点
}
bool solve(Node *p, int x) {
if(p == NULL) return false; //找不到要切的子树,返回false
if(p->data == x) { //找到了要切的子树
delete_tree(p); //调用函数删除该子树
return true; //返回true表示找到了该子树
}
bool flag = false;
int t = -1;
if(p->lson != NULL) t = p->lson->data; //记录下左子树的根的数据
flag |= solve(p->lson, x);
if(t == x) p->lson = NULL; //如果左子树为要切的子树,则将p->lson赋值为NULL,不然就变成野指针了
ans[len++] = p->data;
t = -1;
if(p->rson != NULL) t = p->rson->data;
flag |= solve(p->rson, x);
if(t == x) p->rson = NULL;
return flag;
}
int main() {
Node *head = build_tree(); //构建二叉树,并记录根节点地址
int t;
cin >> t;
while(t--) {
int x;
cin >> x;
len = 0;
if(!solve(head, x)) cout << 0 << endl; //没有找到,输出0,行末无空格
else {
for(int i=0; i < len; i++) //找到了要切的子树,输出中序遍历,行末有空格
cout << ans[i] << ' ';
cout << endl;
}
}
delete_tree(head); //记得释放申请的所有空间
return 0;
}
主要思路还是不变的,在中序遍历时如果找到了要切除的子树,则调用delete_tree函数释放掉该子树,但是有一点很麻烦的就是释放掉该子树后,原先指向该子树的指针没有修改成NULL,这样就会导致段错误。所以可以发现我在递归调用左右子树的时候加了好几段可读性很差的代码。
利用内存池去简化代码
上面代码我觉得主要难写的是释放要切除的子树后,还要想办法去把指向这个子树的指针修改为NULL,如果换成一道复杂的题,这个bug可能就要找好久了。还有就是动态分配的内存需要释放掉就很烦,所以内存池就是解决这些问题的。内存池可以理解成我们事先申请一整块内存,然后需要用到一个节点的时候我们就分一点给它,这样我们在写代码的时候就不需要去考虑管理这些内存了,直接在程序最后结束的时候一次性全部释放掉就行了。可能还有点抽象,看下面的代码和注释理解一下。
const int N = 5e3+5;
struct Node {
int data;
Node *lson;
Node *rson;
}node[N]; //申请一个节点数组作为内存池
Node * newnode() { //这个函数每次调用,就从内存池中拿一个新的节点并返回。
static int cnt = -1; //静态变量,记录最后一次申请的节点的下标
return &node[++cnt]; //返回一块内存
}
/* 看不懂上面的静态变量的可以看一下下面这个,两个等效的
int cnt = -1;
Node * newnode() {
cnt = cnt + 1;
return &node[cnt];
}
*/
有了上面的代码,我们就可以不用去管理内存了,在做切除子树操作时,只需要把 data 标记为 0, 代表结束的标志,而不用真正地去释放这颗子树的内存。整体的代码就像完全二叉树的思路里的代码一样了,代码如下。
#include<bits/stdc++.h>
using namespace std;
const int N = 5e3+5;
int ans[N], len;
struct Node {
int data;
Node *lson;
Node *rson;
}node[N]; //申请一个节点数组作为内存池
Node * newnode() { //从内存池中申请一块新的内存
static int cnt = -1;
return &node[++cnt];
}
Node * build_tree() { //构建二叉树
int x;
cin >> x;
if(x == 0) return NULL;
Node *p = newnode(); //申请新节点
p->data = x;
p->lson = build_tree(); //构建左子树
p->rson = build_tree(); //构建右子树
return p; //返回当前构建好的这棵树
}
bool solve(Node *p, int x) {
if(p == NULL || p->data == 0) return false; //当前树为空树或这棵树被标记为被切掉了
if(p->data == x) { //找到了要切掉的子树的树根
p->data = 0; //标记一下这棵树被切掉了
return true; //返回true表示找到了要切的树
}
//中序遍历
bool flag = false;
flag |= solve(p->lson, x); //到左子树中找
ans[len++] = p->data;
flag |= solve(p->rson, x); //到右子树中找
return flag;
}
int main() {
Node *head = build_tree(); //构建二叉树,并记录根的地址
int t;
cin >> t;
while(t--) {
int x;
cin >> x;
len = 0;
if(!solve(head, x)) cout << 0 << endl; //没有找到要切除的子树,输出0,行末无空格
else {
for(int i=0; i < len; i++) //输出切除子树后的中序遍历序列,行末有空格
cout << ans[i] << ' ';
cout << endl; //记得换行
}
}
//在程序结束的时候,申请来的节点数组node[]就会被自动释放了
return 0;
}