数据结构PTA第四次练习题 二叉树删除子树

编写程序对给定二叉树执行若干次删除子树操作,输出每次删除子树后剩余二叉树的中根序列。二叉树结点的数据域值为不等于0的整数。每次删除操作是在上一次删除操作后剩下的二叉树上执行。

输入格式:

输入第1行为一组用空格间隔的整数,表示带空指针信息的二叉树先根序列,其中空指针信息用0表示。例如1 5 8 0 0 0 6 0 0表示如下图的二叉树。第2行为整数m,表示要进行的删除操作次数。接下来m行,每行一个不等于0的整数K,表示要删除以K为根的子树。m不超过100,二叉树结点个数不超过5000。输入数据保证各结点数据值互不相等,且删除子树后二叉树不为空。

PA567.jpg
输出格式:

输出为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;
}
第一次实验: 题目1 单链表相关算法的实验验证。 [实验目的] 验证单链表及其上的基本操作。 [实验内容及要求] 1、 定义单链表类。 2、 实验验证如下算法的正确性、各种功能及指标: 1)创建单链表; 2)插入操作:分别在当前结点后、表头、表尾插入值为x的结点; 3)删除操作:分别删除表头结点、表尾结点和当前结点的后继结点; 4)存取操作:分别存取当前结点的值和单链表第k个结点的值; 5)查找操作:查找值为x的元素在单链表的位置(下标)。 题目2 分别给出堆栈、队列相关算法的实验验证。 [实验目的] 验证堆栈、队列及其上的基本操作。 [实验内容及要求](以队列为例) 1、 定义队列类。 2、 实验验证如下算法的正确性、各种功能及指标: 1)创建队列; 2)插入操作:向队尾插入值为x的元素; 3)删除操作删除队首元素; 4)存取操作:读取队首元素。 第二次实验 题目1 二叉树相关算法的实验验证。 [实验目的] 验证二叉树的链接存储结构及其上的基本操作。 [实验内容及要求] 1、 定义链接存储的二叉树类。 2、 实验验证如下算法的正确性、各种功能及指标: 1)创建一棵二叉树,并对其初始化; 2)先根、根、后根遍历二叉树(递归算法); 3)在二叉树搜索给定结点的父结点; 4)搜索二叉树符合数据域条件的结点; 5)从二叉树删除给定结点及其左右子树。 题目2 树和森林的遍历算法的实验验证。 [实验目的] 验证树和森林的遍历算法。 [实验内容及要求] 1、 定义左儿子—右兄弟链接存储的树类和森林类。 2、 实验验证如下算法的正确性、各种功能及指标: 1)创建树和森林; 2)树和森林的先根遍历的递归和迭代算法; 3)树和森林的后根遍历的递归和迭代算法; 4)树和森林的层次遍历算法。 题目3 二叉查找树的验证实验。 [实验目的] 验证二叉查找树及其相关操作。 [实验内容及要求] 1、 定义二叉查找树的类。 2、 实验验证如下算法的正确性、各种功能及指标: 1)实现二叉查找树结构; 2) 实现二叉查找树的查找、插入和删除算法; 第三次实验 题目1 邻接表存储的图相关算法的实验验证。 [实验目的] 验证邻接表存的图及其上的基本操作。 [实验内容及要求] 1、 定义邻接表存储的图类。 2、 实验验证如下算法的正确性、各种功能及指标: 1)创建一个邻接表存储的图; 2)返回图指定边的权值; 3)返回图某顶点的第一个邻接顶点; 4)返回图某顶点关于另一个顶点的下一个邻接顶点的序号; 5)插入操作:向图插入一个顶点,插入一条边; 6)删除操作:从图删除一个顶点,删除一条边。 题目2 图的遍历算法的实验验证。 [实验目的] 验证图的遍历算法。 [实验内容及要求] 1、 定义邻接表存储的图。 2、 实验验证如下算法的正确性、各种功能及指标: 1)创建一个图; 2)图的深度优先遍历的递归算法; 3)图的深度优先遍历的迭代算法; 4)图的广度优先遍历算法第四次实验 折半插入排序,堆排序,快速排序 请阅读说明文档
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值