DS博客作业03--树

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

0.PTA得分截图

1.本周学习总结学习总结


树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。
上图是使用树结构存储的集合 {A,B,C,D,E,F,G,H,I,J,K,L,M} 的示意图。对于数据 A 来说,和数据 B、C、D 有关系;对于数据 B 来说,和 E、F 有关系。这就是“一对多”的关系。
将具有“一对多”关系的集合中的数据元素按照图的形式进行存储,整个存储形状在逻辑结构上看,类似于实际生活中倒着的树,所以称这种存储结构为“树型”存储结构。

1.1 二叉树结构

定义:二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。

二叉树性质

  1. 二叉树中,第 i 层最多有 2i-1 个结点。
  2. 如果二叉树的深度为 K,那么此二叉树最多有 2K-1 个结点。
  3. 深度为k的二叉树至多有2^k-1个结点;对任何一棵二叉树,如果其终端结点(即叶子节点)数为n0,度为2的结点数为n2,则n0=n2+1。

特殊的两种二叉树:

  • 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,即节点总数为2^k-1
  • 完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,要注意的是满二叉树是一种特殊的完全二叉树。

1.1.1 二叉树的2种存储结构

树的顺序存储

按照顺序存储结构的定义,用一组地址连续的存储单元以此自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在如上定义的一维数组中下标为i-1的分量中。对于一般二叉树,则应将其每个结点与完全二叉树上的结点相对照,存储在一维数组的相应分量中,空缺表示不存在此结点。由此可见,这种顺序存储结构仅适用于完全二叉树,因为,在最坏的情况下,一个深度为k且只有k个节点的单支树(树中不存在度为2的结点)却需要长度为2k-1的一维数组。

树的链式存储

二叉树的链式存储结构(简称二叉链表)是指用一个链表来存储一棵二叉树,二叉树中每一个结点用链表中的一个链结点来存储


  • 指向左孩子节点的指针(Lchild);
  • 节点存储的数据(data);
  • 指向右孩子节点的指针(Rchild);

顺序存储结构和链式存储结构的优缺点:

  • 顺序存储时,相邻数据元素的存放地址也相邻(逻辑与物理统一);要求内存中可用存储单元的地址必须是连续的。
    优点:存储密度大(=1),存储空间利用率高。
    缺点:插入或删除元素时不方便。
  • 链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
    优点:插入或删除元素时很方便,使用灵活。
    缺点:存储密度小(<1),存储空间利用率低。

使用情况

  • 顺序表适宜于做查找这样的静态操作;
  • 链表宜于做插入、删除这样的动态操作。
  • 若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;
  • 若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。

1.1.2 二叉树的构造

二叉树创建
//先序
BTree CreateBT(string str,int&i)
{
    if(i>=len-1)
        return NULL;
    if(str[i]=='#')
        return NULL;
        
     BTree bt=new BTnode;
     bt->data=str[i];
     bt->lchild=CreateBT(str,++i);
     bt->rchild=CreateBT(str,++i);
}
//中序
BTree CreateBT(string str,int&i)
{
    if(i>=len-1)
        return NULL;
    if(str[i]=='#')
        return NULL;       
     BTree bt=new BTnode;   
     bt->lchild=CreateBT(str,++i);
     bt->data=str[i];
     bt->rchild=CreateBT(str,++i);
}
//后序
BTree CreateBT(string str,int&i)
{
    if(i>=len-1)
        return NULL;
    if(str[i]=='#')
        return NULL;       
     BTree bt=new BTnode;   
     bt->lchild=CreateBT(str,++i);     
     bt->rchild=CreateBT(str,++i);
     bt->data=str[i];
}

CreateBT1(char *pre,char *in,int n):由先序序列pre和中序序列in构造二叉树。
CreateBT2(char *post,char *in,int n):由中序序列in和后序序列post构造二叉树。

BTNode *CreateBT1(char *pre,char *in,int n)
{       //由先序序列pre和中序序列in构造二叉树
    BTNode *s;
    char *p;
    int k;
    if (n<=0) return NULL;
    s=(BTNode *)malloc(sizeof(BTNode));     //创建二叉树节点*s
    s->data=*pre;
    for (p=in;p<in+n;p++)          //在中序序列中找等于*pre的位置k
        if (*p==*pre)
            break;
    k=p-in;                        //k为根结点在in中的下标 
    s->lchild=CreateBT1(pre+1,in,k);       //递归构造左子树 
    s->rchild=CreateBT1(pre+k+1,p+1,n-k-1);//递归构造右子树
    return s;
}
BTNode *CreateBT2(char *post,char *in,int n)
{      //由后序序列post和中序序列in构造二叉树
    BTNode *b; char r, *p; int k;
    if (n<=0) return NULL;
    r = *(post+n-1);                      //取根结点值
    b=(BTNode *)malloc(sizeof(BTNode));   //创建二叉树节点*b
    b->data=r;
    for(p=in;p<in+n;p++)                  //在in中查找根结点 
        if(*p == r)  break;
    k = p-in;                             //k为根结点在in中的下标 
    b->lchild=CreateBT2(post,in,k);       //递归构造左子树 
    b->rchild=CreateBT2(post+k,p+1,n-k-1);//递归构造右子树
    return b;
}

1.1.3 二叉树的遍历

    (1)先序遍历:先访问根节点,再访问左子树,最后访问右子树。

     (2)  后序遍历:先左子树,再右子树,最后根节点。

    (3)中序遍历:先左子树,再根节点,最后右子树。

    (4)层序遍历:每一层从左到右访问每一个节点。

如:

先序遍历:FCADBEHGM

后序遍历:ABDCHMGEF

中序遍历:ACBDFHEMG

层序遍历:FCEADHGBM

递归实现,递归实现难理解但是很容易实现,先序遍历先操作根节点,之后递归先左后右。中序遍历先递归左子树,再操作根节点,再递归右子树。后序遍历先递归左子树再递归右子树之后再操作根节点。这样实现起来就很简单了。

//遍历实现先序
//根左右
void PreOrder(Tree T)
{
	if(T)
	{
		printf("%d",T->Element);//输出根节点,可以是其他操作
		PreOrder(T->Left);
		PreOrder(T->Right);
	}
}
//遍历实现中序
//左根右
void PreOrder(Tree T)
{
	if(T)
	{
		
		PreOrder(T->Left);
		printf("%d",T->Element);//输出根节点,可以是其他操作
		PreOrder(T->Right);
	}
}
//遍历实现后序
//左右根
void PreOrder(Tree T)
{
	if(T)
	{
		
		PreOrder(T->Left);
		
		PreOrder(T->Right);
		printf("%d",T->Element);//输出根节点,可以是其他操作
	}
}

层次遍历

例如,层次遍历图中的二叉树:
首先,根结点 1 入队;
根结点 1 出队,出队的同时,将左孩子 2 和右孩子 3 分别入队;
队头结点 2 出队,出队的同时,将结点 2 的左孩子 4 和右孩子 5 依次入队;
队头结点 3 出队,出队的同时,将结点 3 的左孩子 6 和右孩子 7 依次入队;
不断地循环,直至队列内为空。

/* 
执行过程:
1 根节点入栈
2 根节点出栈,访问根节点
3 根节点的左右子节点入栈
4 重复2、3操作
*/
void levelOrder(BTree tree)
{
    queue<BTree> qTree;
    if (tree)
    {
        qTree.push(tree);
    }
    while (!qTree.empty())
    {
        // 根节点出队,访问根节点
        BTree curTree = qTree.front();
        qTree.pop();
        visit(curTree);

        // 左子节点不为空,入队
        if (curTree->left)
            qTree.push(curTree->left);
        // 右子节点不为空,入队
        if (curTree->right)
            qTree.push(curTree->right);

    }
}

1.1.4 线索二叉树

线索二叉树如何设计?
线索二叉树有三种类型,分别为先序、中序、后序。
在普通二叉树中,我们想要获取某个结点在某种遍历次序下的直接前驱或后继,每次都需要遍历获取到遍历次序之后才能知道。而在线索二叉树中,我们只需要遍历一次(创造线索二叉树时的遍历),之后,线索二叉树就能“记住”每个结点的直接前驱和后继了,以后都不需要再通过遍历次序获取前驱或后继了。
按照某种遍历方式,把普通二叉树变为线索二叉树的过程被称为二叉树的线索化。

将标志位为 1 的指针,按照中序遍历序列,使其指向前驱或后继:

其中,结点 D 没有直接前驱,结点 F 没有直接后继,故指针为 NULL。
这个解决了拥有 n 个结点的二叉树存在 n+1 个空指针域所造成的浪费,解决方式是给每个结点的指针增加一个标志位,以此来利用空指针域。标志位中存储的是 0 或 1 的布尔值,与浪费的空指针域相比,是相对比较划算的。而且使二叉树具有了一种新特性——二叉树中能保存在某种遍历次序下的结点之间的前驱和后继关系。
线索化的实质是在按照某种遍历次序进行遍历二叉树的过程中修改结点的空指针,使其指向其在该遍历次序下的直接前驱或直接后继的过程。线索二叉树就是为了加快查找结点前驱和后继的速度。

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

中序线索二叉树特点:

  • 结点的后继:遍历其右子树时访问的第一个结点,即右子树中最左下的结点;
  • 结点的前驱:若没有左孩子其左标志为 1 ,则左链为线索,指向其前驱 ;否则,遍历左子树时最后一个访问的结点(左子树中最右下的结点)位其前驱。

//二叉树的中序线索化
void Inthread(BiTree root)
{	if(root!=NULL)
	{
		Inthread(root->LChild);/*线索化左子树*/
		if(root->LChild == NULL)
		{
			root->LChild = pre;
			root->Ltag = 1;		/*置前驱线索*/
		}
		if(pre != NULL && pre->RChild == NULL)
		{
			pre->RChild = root;
			pre->Rtag = 1;		/*置后继线索*/
		}
		pre = root;		/*记录当前访问结点,将成为下一个访问节点的前驱*/
		Inthread(root->LChild);/*线索化右子树*/
	}
}




//中序线索二叉树找前驱
BiThrTree InPre(BiThrThee p)
{
	if(p->Ltag == 1)
		pre = p->LChild;
	else{
		for(q=p->LChild;q->Rtag==0;q=q->RChild)
			pre = q;
	}
	return pre;
}

//中序线索二叉树找后继
BiThrTree InNext(BiThrThee p)
{
	if(p->Rtag == 1)
		next = p->RChild;
	else{
		for(q=p->RChild;q->Ltag==0;q=q->LChild)
			next = q;
	}
	return next;
}

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

构造一颗表达式树的算法:该算法描述的是将一颗后缀表达式转换成表达式树的方法。
我们每次读入一个符号,如果是操作数,我们建立一个单节点树(该节点是没有左右子树的),并将指向这棵树的指针入栈;如果读到的符号是操作符,那么从栈中弹出两个子树,分别作为该操作符的左右子树,将形成的这颗新树压入栈中。接着继续读取符号,重复上面的步骤,直到读取结束为止。这时候,栈中只剩一个元素,该元素就是这颗表达式树的根节点。
表达式树的叶节点是操作数,其他节点是操作符。假设所有的运算符都是双目运算符,那么刚好形成一颗二叉树。我们可以通过递归计算左子树和右子树的值,从而得到整个表达式树的值。

这就是一颗表达式树,在这棵树中,只有叶节点是操作数,其他节点都是操作符。前序遍历这棵树将会得到这样一个表达式:++abcde;(这样的表达式,我们称之为前缀表达式,操作符位于操作数之前。),中序遍历这棵树,得到的表达式是这样的:a+bc+de;(中缀表达式是我们最熟悉的表达式,但是有时候中序遍历产生的表达式是不符合预期的,一般我们可以通过递归产生一个带括号的左表达式,然后打印根,最后通过递归产生一个带括号的右表达式。)后序遍历会得到的表达式是这样的:abc+de+;(后缀表达式计算起来是非常简单的:我们常用的遍历表达式树的方式是中序遍历和后序遍历。这样可以得到人们喜欢使用的中缀表达式和计算机喜欢的后缀表达式。

PTree CreatExpTree()
{
	char data;
	PTree T;
	Pstack P = CreatStack();
 
	cout << "请输入后缀表达式:(换行输入ctrl+z结束输入)";
	while (cin >> data)
	{
		if ('a'<= data && 'z' >= data)
		{
			T = (PTree)malloc(sizeof(Tree));
			T->data = data;
			T->left = NULL;
			T->right = NULL;
			Push(P, T);
		}
		else
		{
			T = (PTree)malloc(sizeof(Tree));
			T->data = data;
			T->right = Pop(P);
			T->left = Pop(P);
			Push(P, T);
		}
	}
	return Pop(P);    //返回树的根节点
}

1.2 多叉树结构

1.2.1 多叉树结构

孩子兄弟链结构

typedef struct tnode
  {
      ElemType data;
      struct tnode * fistChild;
      struct tnode * brother;
  }TSBNode, * TSB;    // Tree Son Brother

1.2.2 多叉树遍历

先序遍历做法

1.3 哈夫曼树

1.3.1 哈夫曼树定义

什么是哈夫曼树?

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。在图中,因为结点 a 的权值最大,所以理应直接作为根结点的孩子结点。
路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径。图中,从根结点到结点 a 之间的通路就是一条路径。

哈弗曼树相关知识点:

路径长度:在一条路径中,每经过一个结点,路径长度都要加 1 。例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为 i - 1 。图中从根结点到结点 c 的路径长度为 3。
结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权。例如,图中结点 a 的权为 7,结点 b 的权为 5。
结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。例如,图中结点 b 的带权路径长度为 2 * 5 = 10 。
树的带权路径长度为树中所有叶子结点的带权路径长度之和。通常记作 “WPL” 。例如图中所示的这颗树的带权路径长度为:
WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3(WPL=求和(wi li)其中wi是第i个节点的权值(value)。li是第i个节点的长(深)度.)(其中WPL表示计算出的权值。)

1.3.2 哈夫曼树的结构体

//哈夫曼树结点结构
typedef struct {
    int weight;//结点权重
    int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
}HTNode, *HuffmanTree;

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

构造哈夫曼树

初始时候各个数直都是一个单节点森林!然后进行排序。

放入优先队列(自己排序也行)每次取两个最小权值顶点,构造父节点(value=left.value+right.value)
如果队列为空,那么返回节点,并且这个节点为根节点root。

否则继续加入队列进行排序。重复上述操作,直到队列为空。

在计算带权路径长度的时候,需要重新计算树的高度(从下往上),因为哈夫曼树是从下往上构造的,所以对于高度不太好维护,可以构造好然后计算高度。
比如上述的WPL为:23+33+62+82+92=(2+3)3+(6+8+9)*2=61.

哈弗曼编码

统计字符出现的个数,然后进行构建哈夫曼树;
后序遍历哈夫曼树,左0右1,对每个叶子节点

注意:在建立不等长编码时,必须是任何一个字符的编码不能是另一个字符编码的前缀,这样才能保证译码的唯一性。

任何一个字符的huffman编码都不可能是另一个字符的huffman编码的前缀。

1.4 并查集

  • 什么是并查集?
    并查集(Union Find)是一种用于管理分组的数据结构。它具备两个操作:(1)查询元素a和元素b是否为同一组 (2) 将元素a和b合并为同一组。

注意:并查集不能将在同一组的元素拆分为两组。

  • 并查集解决什么问题?
    并查集可以进行集合合并的操作(并)
    并查集可以查找元素在哪个集合中(查)
    并查集维护的是一堆集合(集)
  • 并查集的结构体、查找、合并操作如何实现?
int node[i]; //每个节点
 
//初始化n个节点
void Init(int n){
    for(int i = 0; i < n; i++){
        node[i] = i;
    }
}
//查找当前元素所在树的根节点(代表元素)
int find(int x){
    if(x == node[x])
        return x;
    return find(node[x]);
}
//合并元素x, y所处的集合
void Unite(int x, int y){
    //查找到x,y的根节点
    x = find(x);
    y = find(y);
    if(x == y) 
        return ;
    //将x的根节点与y的根节点相连
    node[x] = y;
}

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

树的知识点很多,难度也不小,一方面要对树的构造遍历等在知识层面上有一定了解,又要能将这些繁琐的知识能运用到代码中,能写的出来。尽管大部分知识都略知道,在解题的时候还是会常常写不出代码,会多多练习哒。

2.PTA实验作业

2.1 二叉树

二叉树叶子结点带权路径长度和

2.1.1 解题思路及伪代码

创建树CreateBT(string str,int i){       //i为传入的字符的下标
    if  下标越界||字符为‘#’||树为空 then
        return NULL;
    end if
    bt->lchild=递归调用建树函数,下标为2*i;
    bt->rchild=递归调用建树函数,下标为2*i+1;
}
求带权路径长度和void GetWpl(BinTree bt,int h,int &wpl ){   //h为层数,根节点为0层
    if 树为空 then 
        return ;
    end if
    if 该结点是叶子节点 then
        路径长wpl=叶子节点的权重*层数;
    end if
    递归调用GetWpl函数,查找左孩子的叶子节点,层数+1;
    递归调用GetWpl函数,查找右孩子的叶子节点,层数+1;
}

2.1.2 总结解题所用的知识点

1.利用了queue函数
2.递归的方法

2.2 目录树


2.2.1 解题思路及伪代码

typedef struct node*BTree;
typedef struct node
{
string name;
BTree Catalog;
BTree Brother;
Bool flag;
}BTnode;
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 总结解题所用的知识点

1.目录插入问题
2.用了孩子兄弟链结构

3.阅读代码

3.1 题目及解题代码



class Solution {
    int sum = 0;
    public TreeNode bstToGst(TreeNode root) {
        if(root != null){
            bstToGst(root.right);
            sum = sum + root.val;
            root.val = sum;
            bstToGst(root.left);
        }
        return root;
    }
}

3.2 该题的设计思路

本题中要求我们将每个节点的值修改为原来的节点值加上所有大于它的节点值之和。这样我们只需要反序中序遍历该二叉搜索树,记录过程中的节点值之和,并不断更新当前遍历到的节点的节点值,即可得到题目要求的累加树。如:

观察累加前中序遍历与累加后中序遍历,我们会发现,其实后者就是前者的一个从后的累加结果。那问题就迎刃而解了,我们只需反向中序遍历即可,并把每次的节点值进行累加,就能得到最终的累加树。而且这样保证了我们对每个节点只访问了一次。

复杂度分析

时间复杂度:O(n),其中n是二叉搜索树的节点数,每一个节点恰好被遍历一次。

空间复杂度:O(n),为递归过程中栈的开销,平均情况下为O(logn),最坏情况下树呈现链状,为O(n)。

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

1.不易想到反序中序遍历的解题思路
2.需熟知二叉树性质:
二叉搜索树是一棵空树,或者是具有下列性质的二叉树:

若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;

它的左、右子树也分别为二叉搜索树。

由这样的性质我们可以发现,二叉搜索树的中序遍历是一个单调递增的有序序列。如果我们反序地中序遍历该二叉搜索树,即可得到一个单调递减的有序序列。

posted @ 2021-05-05 22:24  唐yuy  阅读(63)  评论(0编辑  收藏  举报
levels of contents