DS博客作业03--树

| 这个作业属于哪个班级 | 数据结构--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业03--树 |
| 这个作业的目标 | 学习树结构设计及运算操作 |
| 姓名 | 章审 |

0.PTA得分截图

1. 本周学习总结

1. 1二叉树结构

1.1.1二叉树的两种存储结构

二叉树的顺序存储结构
二叉树的顺序存储结构就是用一组地址连续的存储单元来存放二叉树的数据元素,因此必须确定好树中各数据元素的存放次序,使得各数据元素在这个存放次序中的相互位置能反映出数据元素之间的逻辑关系。
由二叉树的性质可知,对于完全二叉树和满二叉树,树中结点的层序编号可以唯一地反映出结点之间的逻辑关系,所以可以用一维数组按从上到下、从左到右的顺序存储树中的所有结点值,通过数组元素的下标关系反映完全二叉树或满二叉树中结点之间的逻辑关系。
下图1所示的是一棵一般的二叉树,添加一些虚结点使其成为一棵完全二叉树的结果如图2所示,再对所有结点按层序编号,然后仅保留实际存在的结点。接着把各结点值按编号存储到一维数组中,在二叉树中人为增添的结点(空结点)在数组中所对应的元素值为一特殊值,例如“#”字符,如图3所示。
图一:

图二:

图三:

也就是说,一般二叉树采用顺序存储结构后,二叉树中各结点的编号与等高度的完全二叉树中位置上结点的编号相同,这样对于一个编号(下标)为i的结点,如果有双亲,其双亲结点的编号(下标)为Li/2」;如果它有左孩子,其左孩子结点的编号(下标)为2i;如果它有右孩子,其右孩子结点的编号(下标)为2i+1.
显然,完全二叉树或满二叉树采用顺序存储结构比较合适,既能够最大可能地节省存储空间,又可以利用数组元素的下标确定结点在二叉树中的位置以及结点之间的关系。对于一般二叉树,如果它接近于完全二叉树形态,需要增加的空结点个数不多,也可以采用顺序存储结构。如果需要增加很多空结点才能将一棵二叉树改造成为一棵完全二叉树,采用顺序存储结构会造成空间的大量浪费。最坏情况是右单支树(除叶子结点外每个结点只有一个右孩子),一棵高度为h的右单支树,只有h个结点,却需要分配2-1个存储单元。
在顺序存储结构中,查找一个结点的孩子、双亲结点都很方便,编号(下标)为i的结点的层次log2 (i+1).
由于二叉树顺序存储结构具有顺序存储结构的固有缺陷,使得二叉树的插入、删除等运算十分不方便。
* 二叉树的链式存储结构
二叉树的链式存储结构是指用一个链表来存储一棵二叉树,二叉树中的每一个结点用链表中的一个结点来存储。二叉树链式存储结构中结点的标准存储结构如下:

其中,data表示值域,用于存储对应的数据元素,lchild和rchild分别表示左指针域和右指针域,分别用于存储左孩子结点和右孩子结点的存储地址。这种链式存储结构通常简称为二叉链(binary linked list),。二叉链中通过根结点指针b来唯一标识整个存储结构,称为二叉树b。
二叉链中结点类型BTNode的声明如下:

typedef struct node{
ElemType data;         //数据元素
struct node * Ichild;  //指向左孩子节点
struct node * rchild;  //指向有孩子节点
} BTNode;

例如,图四所示的二叉树对应的二叉链存储结构如图五所示。

二叉链存储结构的优点是对于一般的二叉树比较节省存储空间,在二叉链中访问一个结点的孩子很方便,但访问一个结点的双亲结点需要扫描所有结点。
1. 1. 2 二叉树的构造
假设二叉树中的每个结点值为单个字符,而且所有结点值均不相同(本节好句的算法均基于这种假设),同一棵二叉树具有唯一先序序列、中序序列和后序序列,但不同的二叉树可能具有相同的先序序列、中序序列和后序序列。
例如,如图六所示的5棵二叉树,先序序列都为ABC。如图七所示的5棵二叉树,中序序列都为ACB,如图八所示的5棵二叉树,后序序列都为CBA。
图六:

图七:

图八:

显然,仅由先序序列、中序序列和后序序列中的任何一个无法确定这棵二叉树的树形。但是,如果同时知道了一棵二叉树的先序序列和中序序列,或者同时知道了中序序列和后序序列,就能确定这棵二叉树。
例如,先序序列是ABC,而中序序列是ACB的二叉树必定是图六(c)。
类似地,中序序列是ACB,而后序序列是CBA的二叉树必定是图七(c)
但是,同时知道先序序列和后序序列仍不能确定二叉树的树形,例如图六和图八中除第一棵以外的4棵二叉树的先序序列都是ABC,后序序列都是CBA.
定理1:任何n(n>=0)个不同结点的二叉树,都可由它的中序序列和先序序列唯一地确定。
定理2:任何n(n>=0)个不同结点的二叉树,都可由它的中序序列和后序序列唯一地确定。
实际上,先序序列或后序序列的作用是确定一棵二叉树的根结点(其第一个或最后一个元素即为根结点),中序序列的作用是确定左、右子树的中序序列(包含确定其含的结点个数),进而可以确定左、右子树的先序序列。再递归构造左、右子树。

1. 1. 3二叉树的遍历
由二叉树的先序、中序和后序3种遍历过程直接得到以下3种递归算法:

void PreOrder(BTNode *b)     //先序遍历递归算法
{
if (b!=NULL)                 
{
printf("%c ", b -> data);    //访问根结点
PreOrder(b -> Ichild);       //先序遍历左子树
PreOrder(b-> rchild);        //先序遍历右子树

}
}
void InOrder(BTNode *b)      //中序遍历递归算法
{                            
if (b!=NULL) 
{
InOrder(b -> lchild);        //中序遍历左于树
printf("%c ", b-> data);     //访问根结点
InOrder(b -> rchild);        //中序遍历右子树
}
}
void PostOrder(BTNode * b)    //后序追历递归算法
{
if (b!=NULL)
{
PostOrder(b →>Ichild);        //后序遍历左子树
PostOrder(b →> rchild);       //后序遍历右子树
printf("%c ",b→> data);       //访问根结点
}
}

从上面可以看出,3种递归算法虽然简单,但执行过程是十分复杂的。一般情况下,递归调用从哪里开始,执行最后一定要返回到这个调用的地方。
递归算法在执行中需要多次调用自身。例如,对于图五所示的二叉链,先序遍历算法PreOrder(A)的执行过程如图九所示。为了简便,其中参数A示结点A的地址,其余类同。图中的实箭头表示调用步(对应递归的分解),虚箭头表示返回步(对应递归的求值)。
图九

在进行层次遍历时,对某个结点访问完之后,再按照它的左、右孩子顺 扫序进行同样的处理,这样一层一层进行。先访问结点的左、右孩子也要先访问,这样与队列的特征相吻合。因此层次遍历算法采用一个环形队列qu来实现。
算法中的环形队列采用顺序队存储结构,其类型声明如下:

typedef struct{
BTNode * data[MaxSize];  //存放队中元素
int front, rear;         //队头和队尾指针
}SqQueue;               //顺序队类型

层次遍历过程是先将根结点进队,在队不空时循环:从队列中出列一个结点p,访问它;若它有左孩子结点,将左孩子结点进队;若它有右孩子结点,将右孩子结点进队。如此操作直到队空为止。对应的算法如下:

void LevelOrder(BTNode * b)
{
BTNode * p: 
SqQueue * qu;                 //定义环形队列指针
InitQueue(qu);                //初始化队列
enQueue(qu, b);               //根结点指针进入队列
while (! QueueEmpty(qu))      //队不为空循环
{
deQueue(qu,p);                //出队结点P
printf("%c ",p→> data);       //访问结点p
if (p->lchild!-NULL)          //有左孩子时将其进队
enQueue(qu, p→> Ichild);
if (p→>rchild!=NULL)          //有右孩子时将其进队
enQueue(qu, p→> rchild);
}
}

1.1.4线索二叉树
*对于具有n个结点的二叉树,当采用二叉链存储结构时,每个结点有两个指针域,总共有2n个指针域,又由于只有n-1个结点被有效指针域所指向(n个结点中只有根结点没有被有效指针域指向),则共有2n-(n-1)=n+1个空链域。
遍历二叉树的结果是一个结点的线性序列,可以利用这些空链域存放指向结点的前驱结点和后继结点的地址。其规定是当某结点的左指针为空时,令该指针指向这个线性序列中该结点的前驱结点;当某结点的右指针为空时,令该指针指向这个线性序列中该结点的后继结点,这样的指向该线性序列中的“前驱结点”和“后继结点”的指针称为线索(thread)。
创建线索的过程称为线索化。线索化的二叉树称为线索二叉树(threaded binary-tree)。
由于遍历方式不同,产生的遍历线性序列也不同,会得到相应的线索二叉树。一般有先序线索二叉树、中序线索二叉树和后序线索二叉树。创建线索二叉树的目的是提高该遍历过程的效率。
那么,在线索二叉树中如何区分左指针指向的是左孩子结点还是前驱结点,右指针指向的是右孩子结点还是后继结点呢?为此,在结点的存储结构上增加两个标志位来区分这两种情况:
左标志ltag=0 表示lchild指向左孩子节点
左标志ltag=1 表示lchild指向前驱节点
右标志ltag=0 表示lchild指向右孩子节点
右标志ltag=1 表示lchild指向后继节点
这样,每个节点的存储结构如下:

在某遍历方式的线索二叉树中,若开始结点p没有左孩子,将p结点的左指针改为线索,其左指针仍为空;若最后结点q没有右孩子,将q结点的右指针改为线索,其右指针仍为空。对于其他结点r,若它没有左孩子,将左指针改为指向前驱结点的非空线索;若它没有右孩子,将右指针改为指向后继结点的非空线索。
为了实现线索化二叉树,将前面二叉树结点的类型声明修改如下:

typedef struct node
{ ElemType data;      //结点数据域
int ltag, rtag;        //增加的线索标记
struct node * Ichild;  //左孩子或线索指针
struct node  rchild;   //右孩子或线索指针
} TBTNode;             //线索二叉树中的结点类型

中序线索二叉树的算法如下:

TBTNode * pre;             //全局变量
void Thread(TBTNode * &p)  //对二叉树p进行中序线索化
{
if (p!=NULL)
{
Thread(p->lchild);         //左子树线索化
if (p->Ichild=NULL)        //左孩子不存在,进行前驱结点线索化
{
p-> Ichild=pre;            //建立当前结点的前驱结点线索
p-> ltag =1;
}
else                       //p结点的左子树已线索化
p->ltag=0;
if (pre->rchild=NULL)      //对pre的后继结点线索化
{
pre->rchild=p;             //建立前驱结点的后继结点线索
pre->rtag=1;
}
else
pre->rtag=0;
pre=p;
Thread(p->rchild);          //右子树线索化
}
}

TBTNode CreateThread(TBTNode b)            //中序线索化二叉树
{
TBTNode * root;
root =(TBTNode *)malloc( sizeof(TBTNode)); //创建头结点
root->Itag = 0; root->rtag=1;
root->rchild =b;
if (b=NULL)                                //空二支树
root -> 1child=root;
else
{
root->lchild=b; 
pre=root;                //pre是结点p的前驱结点,供加线索用
Thread(b);               //中序遍历线索化二叉树
pre->rchild =root;       //最后处理,加入指向头结点的线索
pre->rtag=1;
root->rchild =pre;       //头结点右线索化
}
return root;
}
  • 中序线索二叉树特点:
    中序线索二叉树任一个节点后续
    节点有右孩子,则为右子树最左孩子节点;
    节点无右孩子,则为后继线索指针指向节点;
    中序线索二叉树任一个节点前驱
    节点有左孩子,则为左子树最右孩子节点;
    节点无左孩子,则为前驱线索指针指向节点;
    1.1.5二叉树的应用-表达式树
    伪代码
void InitExpTree(BTree& T,string str)//建二叉表达树
{
  定义栈symbol来保存运算符;
  定义栈number来保存运算数;
  函数Precede()用于比较两运算符的优先级;

  While(遍历表达式)
     if(str[i]为运算数)
        构建结点node保存运算数,并进运算数栈; 
     else//为运算符
        若优先级>栈顶运算符,则入运算栈;
        若优先级<栈顶运算符,则栈顶运算符出栈,树根栈弹出两个结点进行建树,新生成的树根入树根栈;
        若优先级==栈顶运算符,则为左右括号匹配,弹出栈顶的左括号;
     end if
  end While
  While(运算符栈不为空)
      栈顶运算符出栈,树根栈弹出两个结点进行建树,新生成的树根入树根栈;
  end while
}

double EvaluateExTree(BTree T)//计算表达式树 
{
  定义变量sum保存每次运算结果;
  if(T->lchild==NULL&&T->rchild==NULL)//遍历到叶结点,找到进行第一次运算的运算数。
     return (T->data-'0');//记得返回时要将字符转为数据;
  lsum=EvaluateExTree(T->lchild);//lsum保存左值;
  rsum=EvaluateExTree(T->rchild);//rsum保存右值;
  switch(T->data)
  {
      lsum和rsum进行相对应的运算,若除数为0时,要exit(0)退出程序;
  }
  return sum;
}

代码

1.2多叉树结构

1.2.1多叉树结构

孩子兄弟链存储结构(child brother chain storage structure)是为每个结点设计3个域,即一个数据元素域、一个指向该结点的左边第一个孩子结点(长子)的指针域、一个指向亥结点的下一个兄弟结点的指针域。
兄弟链存储结构中结点的类型声明如下:

typedef struct tnode 
{ElemType data;         //结点的值
struct tnode * hp;      //指向兄弟
struct tnode * vp;      //指向孩子结点
}TSBNode;               //孩子兄弟链存储结构中的结点类型

孩子兄弟链存储结构如图十所示。
由于树的孩子兄弟链存储结构固定有两个指针域,并且这两个指针是有序的(即兄弟域和孩子域不能混淆),所以孩子兄弟链存储结构实际上是把该树转换为二叉树的存储结构。
孩子兄弟链存储结构的最大优点是可方便地实现树和二叉树的相互转换。孩子兄弟链存储结构的缺点和孩子链存储结构的缺点一样,就是从当前结点查找双亲结点比较麻烦,需要从树的根结点开始逐个结点比较查找。
图十:

1.2.2多叉树遍历

树的遍历(traversal)运算是指按某种方式访问树中的所有结点且每一个结点只被访问一次。树的遍历方式主要有先根遍历、后根遍历和层次遍历3种。注意,树的先根遍历和后根遍历过程都是递归的。
先根遍历(preorder traversal)的过程如下:
(1)访问根结点;
(2)按照从左到右的顺序先根遍历根结点的每一棵子树。

1.3哈夫曼树

1.3.1哈夫曼树定义

在n个带权叶子结点构成的所有二叉树中,带权路径长度WPL最小的二叉树称为哈夫曼树(Huffman tree)或最优二叉树。因为构造这种树的算法最早是由哈夫曼于1952年提出的,所以用他的名字命名。

1.3.2哈夫曼树的结构体

为了实现构造哈夫曼树的算法,设计哈夫曼树中的结点类型如下:

typedef struct
{
char data;       //结点值
double weight;   //权重
int parent;      //双亲结点
int lchild;      //左孩子结点
int rchild;      //右孩子结点
} HTNode;

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

给定n个权值,如何构造一棵含有n个带有给定权值的叶子结点的二叉树,使其带权路径长度WPL最小呢?哈夫曼最早给出了一个带有一般规律的算法,称为哈夫曼算法。哈夫曼算法如下:
(1)根据给定的n个权值(w1,w2, ...,wn),对应结点构成n棵二叉树的森林F=(T1, T2,··· ,Tn),其中每棵二叉树T, (1<=i<=n)中都只有一个带权值为wi;的根结点,其左、右子树均为空。
(2)在森林F中选取两棵结点的权值最小的子树分别作为左、右子树构造一棵新的二叉树,并且置新的二叉树的根结点的权值为其左、右子树上根的权值之和。
(3)在森林F中,用新得到的二叉树代替这两棵树。
(4)重复(2)和(3),直到F只含一棵树为止。这棵树便是哈夫曼树
例如,假设仍采用上例中给定的权值w-(1,3,5,7)来构造一棵哈夫曼树,按照上述算法,则图7.30给出了一棵哈夫曼树的构造过程,其中图十二就是最后生成的哈夫曼树它的带权路径长度为29。
图十二:

哈夫曼编码
规定哈夫曼树中的左分支为0、右分支为1,则从根结点到每个叶子结点所经过的分支对应的0和1组成的序列便是该结点对应字符的编码。这样的编码称为哈夫曼编码。
已上图为例,设1,3,5,7分别为abcd出现的次数,则abcd分别对应的哈夫曼编码为0000,0001,001,01

1.4并查集

什么是并查集
并查集支持查找一个元素所属的集合以及两个元素各自所属的集合的合并等运算。当给出两个元素的一个无序对(a,b)时,需要快速“合并”a和b分别所在的集合,这期间需要反复“查找”某元素所在的集合。“并”、“查”和“集”3个字由此而来。在这种数据类型中,n个不同的元素被分为若干组。每组是一个集合,这种集合叫分离集合,称之为并查集(disjoint-set)。
并查集的数据结构记录了一组分离的动态集合S={S1, S2,.., Sk}。每个动态集合S,(1<=i<=k)通过一个“代表”加以标识,该代表即为所代表的集合中的某个元素。对于集合Si,选取其中哪个元素作为代表是任意的。
优势:这样合并得到的分离集合树的高度不会超过log2n,是一个比较平衡的树,对应的查找与合并的时间复杂度也就稳定在O(log2n)了。
并查集的结构体如下:

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

并查集查找算法的实现:

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实验作业

2.1输出二叉树每层节点

https://gitee.com/z202021123020/zs777/commit/2f2a6449a79ac5826dd93d2b5d3115033cb8d621

2.1.1解题思路及伪代码

void Queue(BTree T, int n)
{
        利用队列将树的每个节点一个一个输入
	BTree p;
	p = T;
        定义节点个数和树的高度
        保存之前树的高度
	int flag;
	flag = 0;
	if (指针为空指针)
	{
		返回
	}
	while (p不为空指针且队列不为空)
	{
		记录层数
                如果层数改变,flag改变
		如果换行,打印
		先序遍历
		入队
		出队
		flag置零;
		i++;
	}
}

2.1.2总结解题所用的知识点

利用队列对树进行层次遍历,同时,在计算树的高度时,除了用log2(i)计算外,还可以在每层最后一个节点做标记,方便计算出树当前的高度;

2.2目录树

https://gitee.com/z202021123020/zs777/commit/0604639dd8ab3acb516af5ad257433339d565d8e

2.2.1解题思路及伪代码

void DealStr(string str, BTree bt)
{
while(str.size()>0)
{
     查找字符串中是否有’\’,并记录下位置pos
     if(没有)
         说明是文件,则进入InsertFile的函数
     else
         说明是目录,则进入Insertcatalog的函数里
         bt=bt->catalog;
         bt要跳到下一个目录的第一个子目录去,因为Insertcatalog的函数是插到bt->catalog里面去
         while(bt!NULL&&bt->name!=name)
            bt=bt->Brother;//找到刚刚插入的目录,然后下一步开始建立它的子目录                     
str.erase(0, pos + 1);把刚插进去的目录从字符串中去掉
}
}

2.2.2总结解题所用的知识点

string在查找字符串的位置时的应用,并且要将目录和文件夹区分,用兄弟孩子链表来组成目录树的结构体。

3.阅读代码

3.1题目即解题代码


解题代码

class Solution {
    public boolean isBalanced(TreeNode root) {
        if (root == null) {
            return true;
        } else {
            return Math.abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
        }
    }

    public int height(TreeNode root) {
        if (root == null) {
            return 0;
        } else {
            return Math.max(height(root.left), height(root.right)) + 1;
        }
    }
}

3.2 该题的设计思路及伪代码


定义函数height,用于计算二叉树中的任意一个节点p的高度,
有了计算节点高度的函数,即可判断二叉树是否平衡。具体做法类似于二叉树的前序遍历,即对于当前遍历到的节点,首先计算左右子树的高度,如果左右子树的高度差是否不超过 11,再分别递归地遍历左右子节点,并判断左子树和右子树是否平衡。这是一个自顶向下的递归的过程。
复杂度分析
时间复杂度:O(n^2),其中 n 是二叉树中的节点个数。
最坏情况下,二叉树是满二叉树,需要遍历二叉树中的所有节点,时间复杂度是 O(n)。
对于节点 p,如果它的高度是 d,则height(p)最多会被调用 d 次(即遍历到它的每一个祖先节点时)。对于平均的情况,一棵树的高度 h 满足 O(h)=O(log n),因为 d≤h,所以总时间复杂度为 O(nlogn)。对于最坏的情况,二叉树形成链式结构,高度为 O(n),此时总时间复杂度为 O(n^2)。
空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。

3.3 分析该题目解题优势及难点。

这道题有两种做法,一种是自顶向下的递归,一种是自底向上的递归。由于是自顶向下递归,因此对于同一个节点,函数 height 会被重复调用,导致时间复杂度较高。如果使用自底向上的做法,则对于每个节点,函数height 只会被调用一次。

posted @ 2021-05-05 22:16  51456  阅读(249)  评论(0编辑  收藏  举报