第5章 树与二叉树学习小结

  前几章学习的基本都是线性的数据结构,就有顺序存储结构和链式存储结构,而这一章“树”结构是一类非线性数据结构,跟之前就有不同的点,但是,树的存储结构还是可以通过找到元素之间逻辑关系,采用类似线性表的方式,按照结点之间的逻辑关系放到线性存储中。

  这部分主要学习到二叉树的内容,二叉树有好几个性质,我想这些性质很重要,有时候在解决问题,它能够帮助理解这棵树比较抽象的结构层次,这是我在理解代码时候体会到的。二叉树存储结构跟遍历有很大的关系,遍历的结果是将非线性结构的树中结点排成一个线性序列。

这是二叉链表的存储表示:

typedef struct Bitnode
{
  int data;//结点数据域
  struct Bitnode *lchild,*rchild;//左右孩子指针
}Bitnode,*Bitree;

 

跟之前差不多一样,然后就最初始的建立二叉链表,用了递归方式进行创建和遍历,递归只用几行代码就把事情搞定,看起来很简单,老师很详细的讲解了递归的来龙去脉,让我们不只是停留在表面,而是要“知其然,更要知其所以然”,我想这样理解也更加深刻。二叉树另外一种存储方式-用结构体数组答题时候用的多,如下:

做PTA上题目时候,总能发现自己算法和编程的不足之处,有时需要优化改进。这次平台上的编程题目让我收获许多,其中发现它们都有相同的点,就是寻找一棵二叉树的根结点,也是后续完成各功能的关键。这里我记录一下解答Leaves List题目的过程:

 

7-1 List Leaves (30 分)

 

Given a tree, you are supposed to list all the leaves in the order of top down, and left to right.

Input Specification:

每个输入文件包含一个测试用例。对于每种情况,第一行给出正整数N(≤ 0),这是树中的节点的总数-并且因此节点编号从0到1。然后随后是N行,每行对应一个节点,并给出节点左右子节点的索引。如果孩子不存在,将在该位置放置“ - ”。任何一对孩子都被一个空间隔开。

Output Specification:

对于每个测试用例,按照自上而下和从左到右的顺序在一行中打印所有叶子的索引。任何相邻数字之间必须只有一个空格,并且该行末尾没有额外的空格。

样本输入:

8
1 -
- -
0 -
2 7
- -
- -
5 -
4 6

样本输出:

4 1 5
跟之前有结点名字不同,行序号作为结点名,在一行输入其左右孩子,节省了存储名字的空间,直接用数组下标记录。
用上面说到的结构,将一颗非线性二叉树存放到一个结构体数组中,因为根据题目内容特点,这样比较二叉链表好实现。
#include<iostream>
#include<queue>
using namespace std;

typedef struct //定义结构体数组,存放结点左右孩子
{
    int Left;
    int Right;
}Node;
View Code

下面这一步,感觉几个题目都用到了,在读取数据的同时,找到树的根结点:

int BuildTree(Node T[]) //建立二叉树 
{
    int i,N;
    bool check[100]={false};//check数组用于查找树的根节点 
    char x,y;
    cin>>N;
    
    if(N)//树结点个数不为0 ,之前没有这一步,虽然能够通过,但是程序不够严谨
    {
        for(i = 0; i < N; ++i)
        {
            cin>>x>>y; 
  
            if(x != '-')//若结点不为空,将节点索引放入左子树结点 
            {
                T[i].Left = x - '0';
                check[T[i].Left] = true;//记录此结点索引,在check数组将该位置置为true 
            }
            else
            {
                T[i].Left = -1;//若结点为空,将其置为-1 
            }
            
            if(y != '-')//同上,放入右子树 
            {
                T[i].Right = y - '0';
                check[T[i].Right] = true;
            }
            else
            {
                T[i].Right = -1;
            }
        }
        for(i = 0 ; i < N; ++i)//遍历check数组,除了根结点之外,其它元素为true或-1 
        {
            if(!check[i]) return i;//返回根结点下标 
        }
    }
    else return -1;// 若树为空,返回 -1
    
}
View Code

接下来,题目要求是输出叶结点,所以用了队列的特性,在出入队进行操作,第一次,我的代码是这样的:

void Leafnode(Node T[],int k)
{ queue
<int> q; int flag = 1; if(k == -1) return;//若树为空,返回 q.push(k);//将根结点下标入队 int temp; while(!q.empty()) { temp = q.front();//取队头元素 q.pop();//队头元素出队 if((T[temp].Left == -1) && (T[temp].Right == -1))//当此结点左右孩子都为空时,说明它是叶结点,输出 { if(flag)//用flag判断是否为要输出空格 { cout<<temp; flag = 0; } else cout<<" "<<temp; } if(T[temp].Left != -1)//左结点不为空时,入队 q.push(T[temp].Left); if(T[temp].Right != -1)//右结点不为空时,入队 q.push(T[temp].Right); } }

然后我看到最后面2个if语句,感觉可以简化一下,跟前面if合在一起判断,因为其实进来一个结点,可以先判断它是否为空,空则不进入语句,非空则执行if语句,就不用单独判断结点左和右孩子是否为空再入队,于是我修改代码如下:

void Leafnode(Node T[],int k)
{
    queue<int> q;
    int flag = 1;
    if(k == -1) return;//若树为空,返回 
    q.push(k);//将根结点下标入队 
    int temp;
    while(!q.empty())
    {
        temp = q.front();//取队头元素 
        q.pop();//队头元素出队 
        
        if(temp!=-1)//叶结点不为空时,执行以下语句 
        {    
                if((T[temp].Left == -1) && (T[temp].Right == -1))//若该结点的左右结点为空,输入叶结点 
            {
                //cout<<'\n'<<"ok"<<'\n';
                if(flag)//flag判断是否为第一个输出 
                {
                cout<<temp;//第一个输出前面不带空格 
                flag = 0;//同时将flag置为0 
                }
                else
                    cout<<" "<<temp;//输出元素前面带空格 
            
            }
            q.push(T[temp].Left);//此结点的左右孩子入队 
            q.push(T[temp].Right);
        }//执行下一轮循环 
    }
}

主函数:

int main()
{
    Node t[100];
    int k;
    k = BuildTree(t);
    Leafnode(t,k);//cout<<"ok";
    return 0;
}
View Code

另外,老师教打天梯赛《深入虎穴》那道题目,从开始分析,逻辑思维引入,一步步优化算法的数据结构,最后把控全局的框架思想,真让我意识到自己分析能力不够强大,还需努力了。那节课结束之后,我和几个同学都在很小的细节出错,因为题目有很多for循环语句,主要问题出现在起始下标,还有循环次数,这才感到编程先顾及全局,然后要注意每一部细节。仔细修正之后,最后成功解决。不过,我对老师开始提到的用单链表实现方式有点兴趣探究一下,所以之后尝试用单链表存储每个门之后通道序号,最后在Dev运行正确,但是提交之后只通过了3个测试点,另外3个不知道错在哪里。这是正确解题思路:https://www.cnblogs.com/chenzhenhong/p/10776941.html

#include<iostream>
#include<queue>
#include<list>
using namespace std;

typedef struct
{
    int doors;//门的数量
    list<int> l; //存放每个门后面通向门序号的单链表 
}node;

int input(node *a,int n)//读入n扇门的信息 ,并返回跟所在 门序号(下标) 
{    
    int i,j,value;
    bool *vi;
    vi=new bool[n+1];
    
    for(i=0;i<n+1;i++)
        vi[i]=false;
        
    for(i=1;i<n+1;i++)
    {
        cin>>a[i].doors;
        if(a[i].doors)//门后面有通道 
        {    
            for(j=0;j<a[i].doors;j++)
            {
                cin>>value;
                a->l.push_back(value);//将门序号存储到单链表中 
                vi[value]=true;
            }
        }
    
    }
    for(i=1;i<n+1;i++)//找出根结点所在下标(起点) 
    {
        if(!vi[i]) return i;
    }
}

int level(node *a,int r)//从a[r]开始对a数组进行层次遍历,并返回遍历最后一个结点的序号 
{
    queue<int> q;
    int f,i,t;
    q.push(r);
    
    while(!q.empty())
    {
        f=q.front();
        q.pop();
        
        if(a[f].doors) //t号门后面有通道门
        {
            for(i=0;i<a[f].doors;i++)
            {
                //cout<<t<<" ";
                t=a->l.front();//取链表头元素 
                a->l.pop_front();//去掉链表头元素
                q.push(t);// 链表头元素入队 
            }
        }
    }
    return f;
}

int main()
{
    node *a;//用于存储整棵树
    int n,root;
    cin>>n;
    a=new node[n+1];
    root=input(a,n);
    cout<<level(a,root);
    return 0;
}
View Code

 虽然没能成功,但这个过程理解了许多,这个星期做的题目比之前多了,编程打代码感觉挺好,就是还缺乏分析能力和逻辑转化为现实操作,希望之后的学习能够提高这方面缺陷。

从开始的二叉树,普通的树,再到森林,还有最优树(哈夫曼树),难点在于如何建立这些树,这与它的存储结构密切相关,所以,分析树的存储结构尤为重要,也是树的难点,只有树建立起来,之后操作才能够更好的进行。

posted @ 2019-04-26 21:45  Charzueus  阅读(496)  评论(2编辑  收藏  举报