0.PTA得分截图

1.本周学习总结(5分)

学习总结,请结合树的图形展开分析。

1.1 二叉树结构

1.1.1 二叉树的2种存储结构

树的顺序存储和链式存储结构,并分析优缺点。

树的顺序存储结构

完全二叉树:按从上至下、从左到右顺序存储,n个结点的完全二叉树的结点父子关系

非根节点(序号i>1)的父结点的序号是 [i/2]
结点(序号为i)的左孩子结点的序号是2i([2i<=n] ,否则没有左孩子)
结点(序号为i)的右孩子结点的序号是2i+1([2i+1<=n] ,否则没有右孩子)

而一般树用顺序存储结构,较容易造成空间上的浪费。

树的链式存储结构

每个结点的结构的代码定义

typedef struct TreeNode *BinTree;
struct TreeNode{
  ElementType Data;
  BinTree Left;
  BinTree Right;
}

结构:

如图,从一个头结点开始,左孩子的指针指向一个子树,右孩子的一个指针指向一个子树,从而构成整棵树。
这就是利用链表的结构,来存储的树。

1.1.2 二叉树的构造

总结二叉树的几种构造方法。分析你对这些构造方法的看法。务必介绍如何通过先序遍历序列和中序遍历序列、后序遍历序列和中序遍历序列构造二叉树。

如图二叉树,前中后三种遍历都是利用递归的结构来进行遍历的,而先序遍历是先访问根节点,之后再访问其左子树跟右子树,同理中序遍历是先左子树
然后再进行根节点的访问跟右子树的访问,而后序则是先左右子树,后根节点。
而转换成构造,无非也是一种遍历,先创建一个树结点,然后看序列来创建左子树或者右子树,或者给根节点赋值。

先序遍历序列

代码实现

BTree CreatTree(char* str)//创建二叉树
{
	if (str[i] == '#' || !str[i])
	{
		i++;
		return NULL;
	}
	BTree T;
	T = new TNode;
	T->data = str[i++];
	T->lchild = CreatTree(str);
	T->rchild = CreatTree(str);
	return T;
}

如果,则先创建根节点a,然后赋值,之后进入左子树的构造,在左子树里重复该模式,知道遇到#,构建右子树。

中序遍历序列

中序遍历是先进入左子树的构建,一直到#,然后才开始给根节点赋值,所以a就会在原来c的位置,进入右子树的构建,还是会优先构建左子树,所以c会出现在e的位置。

后序遍历序列

后序遍历如上所述,则先进入左右子树的构建,则赋值a后,因为原来的b有右子树,所以b结点并不会出现在原来的位置,而是会先进行左右子树的遍历,直到到达原来的g的位置,因为g的左右并无节点了,然后赋值后往上赋值,到原来的d的位置,但却不进行赋值,先构建右子树。

  • 如此便是前中后三种序列的构建,实际的应用中,只需更改下赋值跟递归的顺序,差别其实不大。

1.1.3 二叉树的遍历

总结二叉树的4种遍历方式,如何实现。
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问依次且仅被访问一次。
四种遍历方式分别为:先序遍历、中序遍历、后序遍历、层序遍历。

先序遍历

前序遍历首先访问根结点然后遍历左子树,最后遍历右子树。在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。
若二叉树为空则结束返回,否则:
(1)访问根结点。
(2)前序遍历左子树。
(3)前序遍历右子树 。
已知后序遍历和中序遍历,就能确定前序遍历。

void preorder(BinTree BT)
{
    if (!BT)
        return;
    if (!BT->Left && !BT->Right)
        printf(" %c", BT->Data);
   preorder(BT->Left);
   preorder(BT->Right);
}

中序遍历

中序遍历首先遍历左子树,然后访问根结点,最后遍历右子树。若二叉树为空则结束返回,否则:
(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树
已知前序遍历和后序遍历,不能确定中序遍历。

void inorder(BinTree BT)
{
    if (!BT)
        return;
    
    inorder(BT->Left);
    if (!BT->Left && !BT->Right)
        printf(" %c", BT->Data);
    inorder(BT->Right);
}

后序遍历

后序遍历是二叉树遍历的一种。后序遍历指在访问根结点、遍历左子树与遍历右子树三者中,首先遍历左子树,
然后遍历右子树,最后遍历访问根结点,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后遍历根结点。

(1)若二叉树为空,结束
(2)后序遍历左子树
(3)后序遍历右子树
(4)访问根结点

void postorder(BinTree BT)
{
    if (!BT)
        return;
    
    postorder(BT->Left);
    postorder(BT->Right);
    if (!BT->Left && !BT->Right)
        printf(" %c", BT->Data);
}

层序遍历

层序遍历,主要运用队列的结构,来对一层一层的结点先后进行遍历,如果是同一层的结点,就先入队,然后读完上一层后,
就继续出队下一层的结点,然后再将下一层结点的左子树跟右子树的结点入队,再输出,以便能够达到保存一层层的结点进行输出

图的思维过程:
A入队
访问队首A,左儿子不为空,B入队,右儿子不为空,C入队,A出队
访问队首B,左右儿子为空,不用操作,B出队
访问队首C,同步骤2
访问队首D,同步骤3
访问队首E,同步骤2
访问队首F,同步骤3
访问队首G,同步骤3
遍历结果 ABCDEFG

void LevelTraversal(BinTree T)
{
    if (!T)
    {
        cout << "NULL";
        return;
    }
    BinTree p;
    queue<BinTree>q;
    q.push(T);
    int flag = 1;
    while (!(q.empty()))
    {
        p = q.front();//头结点
        q.pop();//弹出头结点
        if (flag)//访问结点
        {   
            cout << p->Data;
            flag = 0;
        }
        else if (!flag)
            cout << " " << p->Data;
        if (p->Left != NULL)
            q.push(p->Left);
        if (p->Right != NULL)
            q.push(p->Right);
    }
}

1.1.4 线索二叉树

线索二叉树如何设计?

按照某种遍历方式对二叉树进行遍历,可以把二叉树中所有结点排序为一个线性序列。在该序列中,除第一个结点外每个结点有且仅有一个直接前驱结点;
除最后一个结点外每一个结点有且仅有一个直接后继结点。这些指向直接前驱结点和指向直接后续结点的指针被称为线索(Thread),加了线索的二叉树称为线索二叉树。
建立线索二叉树,或者说对二叉树线索化,实质上就是遍历一颗二叉树。在遍历过程中,访问结点的场所是检查当前的左,右指针域是否为空,将它们改为指向前驱结点或后续结点的线索。
为实现这一过程,设指针pre始终指向刚刚访问的结点,即若指针p指向当前结点,则pre指向它的前驱,以便设线索。
另外,在对一颗二叉树加线索时,必须首先申请一个头结点,建立头结点与二叉树的跟结点的指向关系,对二叉树线索化后,还需建立最后一个结点与头结点之间的线索。

iThrNodeType *pre;

BiThrTree InOrderThr(BiThrTree T)

{   /*中序遍历二叉树T,并将其中序线索化,pre为全局变量*/

    BiThrTree head;

    head=(BitThrNodeType *)malloc(sizeof(BiThrType));/*设申请头结点成功*/

    head->ltag=0;head->rtag=1;/*建立头结点*/

    head->rchild=head;/*右指针回指*/

    if(!T)head->lchild=head;/*若二叉树为空,则左指针回指*/

    else{head->lchild=T;pre=head;

            InThreading(T);/*中序遍历进行中序线索化*/

            pre->rchild=head;

            pre->rtag=1;/*最后一个结点线索化*/

            head->rchild=pre;

            };

     return head;

}

void InThreading(BiThrTree p)

{/*通过中序遍历进行中序线索化*/

     if(p)

       {InThreading(p->lchild);/*左子树线索化*/

         if(p->lchild==NULL)/*前驱线索*/

            {p->ltag=1;

             p->lchild=pre;

            }

          if(p->lchild==NULL)/*后续线索*/

            {p->rtag=1;

             p->rchild=pre;

            }

          pre=p;

          InThreading(p->rchild);/*右子树线索化*/

       }

}

中序线索二叉树特点?如何在中序线索二叉树查找前驱和后继?

左指针为空时指向的便是前驱,右指针为空时指向的便是后继

1.1.5 二叉树的应用--表达式树

介绍表达式树如何构造

  1. 依次读取表达式;
  2. 如果是操作数,则将该操作数压入栈中;
  3. 如果是操作符,则弹出栈中的两个操作数,第一个弹出的操作数作为右孩子,第二个弹出的操作数作为左孩子;然后再将该操作符压入栈中。
    这样下去,就可以建立一颗完整的表达式树。

如何计算表达式树
从操作数的栈中弹出两个数,再从操作符的栈中弹出一个符号,进行计算,得出的数再压入数栈中
如此循环,直至栈中清空,得出的最后一个数即为该表达式计算出来的结果。

1.2 多叉树结构

1.2.1 多叉树结构

主要介绍孩子兄弟链结构
孩子兄弟链表示法树的一种存储方式,每个结点由三部分组成:存储数据元素值的数据部分、指向它的第一个子结点的指针、指向它的兄弟结点的指针。

typedef struct node
{
    char data;
    struct node *sublist; //孩子链指针
    struct node *link;//兄弟链指针
}BTNode;


如此,以父节点指向第一个子节点,该层的其他结点存储为链表结构,即第一个子节点指向后续的结点。
这样存储方便了树的结构的构造,在多叉树的应用中,不需要在结构体中添加太多指向后继结点的指针元素。
但其劣势也很明显,不方便找到父亲结点。

1.2.2 多叉树遍历

介绍先序遍历做法
二叉树的先序遍历是优先输出根节点,后继续递归进入下一层的左孩子。
多叉树的遍历也类似,
1.访问根节点,
2.进行第一个孩子的优先的递归遍历,后进行后面其余孩子的递归遍历。
两者皆用递归的结构,且原理类似。

1.3 哈夫曼树

1.3.1 哈夫曼树定义

什么是哈夫曼树?,哈夫曼树解决什么问题?

给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

霍夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。
树的路径长度是从树根到每一结点的路径长度之和,记为WPL=(W1L1+W2L2+W3L3+...+WnLn),N个权值Wi(i=1,2,...n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,...n)。
可以证明霍夫曼树的WPL是最小的。
这种结构被用于电报的编码中,让使用频率高的用短码,使用频率低的用长码,以优化整个报文编码。

1.3.2 哈夫曼树构建及哈夫曼编码

结合一组叶子节点的数据,介绍如何构造哈夫曼树及哈夫曼编码。

哈夫曼树的树结构为二叉树,采用自下而上的构建方式

如图数据,找到两个最小的权的数据进行合并成一个子树

重复以上,再找到两个最小数进行合并

第三次进行合并

构成了最小带权路径的哈夫曼树

用0标注左枝干,1标注右枝干
则编码即为,头结点,到各字母的所经过的边的值的合并。
A,B,C,D对应的哈夫曼编码分别为:111,10,110,0。

1.4 并查集

在计算机科学中,并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个用于此数据结构的操作:
Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
Union:将两个子集合并成同一个集合。
由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于建立单元素集合。有了这些方法,许多经典的划分问题可以被解决。
为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数。

结构声明如下

typedef struct node { 
int data;              //结点对应人的编号
int rank;              //结点秩:子树的高度,合并用
int parent;            //结点对应双亲下标
} UFSTree;             //并查集树的结点类型

初始化

void MAKE_SET(UFSTree t[],int n)//初始化并查集树
{
  int i;
  for(i=1;i<=n;i++)
  {
    t[i].data=0;  //数据为该人的编号
    t[i].rank=0;  //秩初始化为0
    t[i].parent=i;//双亲初始化指向自己
}

查找一个元素的集合

int FIND SET (UFSTree t[], int x) {    //在x所在的子树中查找集合编号
if (x!=t[x]. parent)                   //双亲不是自己
return(FIND-SET(t,t[x] . parent);      //递归在双亲中找x
else
return(x);                             //双亲是自己,返回x
}

合并两个元素各自所属的集合

void UNION(UFSTree t[], int x, int y)  //将x和y所在的子树合并
{
  x=FIND-SET(t,x);                         //套找*所在分离集合树的编号
  y=FIND SET(t,y);                         //查找y所在分离集合树的编号
  if (t[x].rank > t[y].rank)               //y结点的秩小于x结点的秩
    t[y].parent=x;                         //将y连到x结点上,*作为y的双亲结点
  else                                     //y结点的秩大于等于x结点的科
  {
  t[x].parent=y;                           //将x连到y结点上,y作为x的双亲结点
  if (t[x].rank==t[y]. rank)               //x和y结点的秩相同
  t[y].rank++;                             //y结点的秩增
  }
}

1.5.谈谈你对树的认识及学习体会。

2.PTA实验作业(4分)

此处请放置下面2题代码所在码云地址(markdown插入代码所在的链接)。如何上传VS代码到码云

2.1 输出二叉树每层节点

输出二叉树每层节点

2.1.1 解题思路及伪代码

主要是利用队列的结构,使树按照层次来进行输出

伪代码

if 该树为空
  输出NULL
创建树节点p , 队列q
h记录层数,next 保存队列进队的下一层的层数,n为遍历的该层的层数
for h =1 to 队列不为空
  输出h
  while 队列不为空且i<n
    出队一个树节点赋给p
    if p为有数据的结点
      输出p的值
      然后进行p的左右孩子的出队并且next++
      i++
  End While
  n = next 将下层结点的数保存的该层进行下一轮遍历
  next =0
end for


2.1.2 总结解题所用的知识点

所用的是树的构造还有树的层次遍历,队列的应用

2.2 目录树

2.2.1 解题思路及伪代码

2.2.2 总结解题所用的知识点

3.阅读代码(0--1分)

找1份优秀代码,理解代码功能,并讲出你所选代码优点及可以学习地方。主要找以下类型代码:

3.1 不同的二叉搜索树
可截图,或复制代码,需要用代码符号渲染。

int numTrees(int n) {
    int G[n + 1];
    memset(G, 0, sizeof(G));
    G[0] = G[1] = 1;
    for (int i = 2; i <= n; ++i) {
        for (int j = 1; j <= i; ++j) {
            G[i] += G[j - 1] * G[i - j];
        }
    }
    return G[n];
}

3.2 该题的设计思路及伪代码
请用图形方式展示解决方法。同时分析该题的算法时间复杂度和空间复杂度。
动态规划法:

以i作为根节点,0-i-1作为左子树的根,i+1 -n 作为右子树的根

G(n): 长度为 n 的序列能构成的不同二叉搜索树的个数。
F(i, n)F(i,n): 以 i 为根、序列长度为 n 的不同二叉搜索树个数 (1≤i≤n)。



所以从头开始算G[]的值直至算至G[n]。
伪代码

定义数组G[]储存不同长度的搜索树的种类数量
初始化G
for 2 to n
  for 1 to i
    根据公式计算G[I]的值
返回G[n]

3.3 分析该题目解题优势及难点。
难点主要是对公式的推理分析,类似于前面所学的斐波那系数都是从头开始计算的,这种都是依赖与数学
的方式,找出规律整合出表达式。解题也给我提供了一种新的思路,不一定要用穷举的方式来做出题目,还可以对数据的结构进行分析,然后整合出更高效的表达计算方式。

 posted on 2021-05-02 20:29    阅读(127)  评论(0编辑  收藏  举报