DS博客作业03--树

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

0.PTA得分截图

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

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

1.1 二叉树结构

1.1.1 二叉树的2种存储结构

顺序存储

借用数组将二叉树中的数据元素存储起来。此方式只适用于完全二叉树,如果想存储普通二叉树,需要将普通二叉树转化为完全二叉树。

使用数组存储完全二叉树时,从数组的起始地址开始,按层次顺序从左往右依次存储完全二叉树中的结点。当提取时,根据完全二叉树的第 2 条性质,可以将二叉树进行还原。

数组中存储为:

根据完全二叉树的第 2 条性质就可以根据数组中的数据重新搭建二叉树的结构。

如果普通二叉树也采取顺序存储的方式,就需要将其转化成完全二叉树,然后再存储,例如:

转化前 ~~~~~~~~~~~~转化后

链式存储

结点结构代码表示:

typedef struct BiTNode
{
  TElemType data;  //数据域
  struct BiTNode *lchild, *rchild;  //左右孩子指针
}BiTNode, *BiTree;

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

1.1.2 二叉树的构造

最常用方法

void CreateBiTree(string str, BiTree& T, int index)
{
	if (index > str.size()-1)
	{
        T = NULL;
		return;
	}
	if (str[index] == '#')
	{
		T = NULL;
	}
	else
	{
		T = new BiTNode;
		T->data = str[index];
		CreateBiTree(str, T->lchild, 2 * index);
		CreateBiTree(str, T->rchild, 2 * index + 1);
	}
}

先序遍历建二叉树

BTree CreatTree(string str, int &i)
{
	BTree bt;
	if (i > len - 1) return NULL;
	if (str[i] == '#') return NULL;
	bt = new BTNode;
	bt->data = str[i];
	bt->lchild = CreatTree(str, ++i);
	bt->rchild = CreatTree(str, ++i);
	return bt;
}

已知二叉树先序遍历序列和中序遍历序列建二插树

node* BitryTree(char a[],char b[],int n){
    node *T;
    int i;
    if(!n)
        return NULL;即当先树遍历左子树为空时
    else{
        T = (node *)malloc(sizeof(struct node));
        T->data = a[0];
        for(i=0;i<n;i++)
        {
            if(a[0]==b[i])
                break;
        }
        T->left = BitryTree(a+1,b,i);//先遍历左子树
        T->right = BitryTree(a+1+i,b+i+1,n-i-1);
    }

看法:
最常用第一种建树方法,但主要依具体情节选择合适方法

1.1.3 二叉树的遍历

总结二叉树的4种遍历方式,如何实现。

递归先序遍历二叉树

 void PreOrderTraverse(BiTree & t)
 {
	 if(t!=NULL)
	 {
		 cout<<t->data;
         PreOrderTraverse(t->lchild);
         PreOrderTraverse(t->rchild);
	 }
 }

递归中序遍历二叉树

void InOrderTraverse(BiTree & t)
{
if(t!=NULL)
{

     InOrderTraverse(t->lchild);
	 cout<<t->data;
     InOrderTraverse(t->rchild);
 }

}

递归后序遍历二叉树

void InOrderTraverse(BiTree & t)
 {
	 if(t!=NULL)
	 {
		 
         InOrderTraverse(t->lchild);
         InOrderTraverse(t->rchild);
         cout<<t->data;
	 }
 }

递归层次遍历二叉树

#include <queue>
void LevelOrder(BTNode* b)
{
	BTNode* p;
	SqQueue* qu;//定义唤醒队列指针
	InitQueue(qu);//初始化队列
	enQueue(qu, b);
	while (!QueueEmply(qu))
	{
		deQueueEmpty(qu, p);
		printf("%c", p->data);
		if (p->lchild != NULL)
			enQueue(qu, p->lchild);
		if (p->rchild != NULL)
			enQueue(qu, p->rchild);
	}
	DestroyQueue(qu);	
}

1.1.4 线索二叉树

建立线索的规则:

(1)如果ptr->lchild为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为ptr的中序前驱;
(2)如果ptr->rchild为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为ptr的中序后继;

声明:

typedef struct node
{
	ElemType data;
	int ltag, rtag;  //增加的线索标记
	struct node* lchild;
	struct node* rchild;
}TBTNode;   //线索二叉树中的节点类型

对二叉树p进行中序线索化

TBTNode* pre;//全局变量
void Thread(TBTNode*& p)
{
	if (p != NULL)
	{
		Thread(p->lchild);//左子树线索化
		if (p->lchild == NULL)//左孩子不存在,进行前驱结点线索化
		{
			p->lchild = pre;//建立当前结点的前驱结点线索
			p->ltag = 1;
		}
		else//p结点的左子树已线索化
			p->ltag = 0;
		if (pre->rchild == NULL)//对pre的后继结点线索化
		{
			pre->rchild = p;//建立前驱结点的后继结点线索
			pre->rtag = 1;
		}
		else
			p->rtag = 0;
		pre = p;
		Thread(p->rchild);//右子树线索化
	}
}
TBTNode* CreateThread(TBTNode* b)//中序线索化二叉树
{
	TBTNode* root;
	root = (TBTNode*)malloc(sizeof(TBTNode));//创建头结点
	root->ltag = 0; root->rtag = 1;
	root->rchild = b;
	if (b == NULL)//空二叉树
		root->lchild = 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)//建表达式树
{
	stack<BTree> s;//存放数字
	stack<char> op;//存放运算符
	op.push('#');
	int i = 0;
	while (str[i])//树结点赋值
	{
		if (!In(str[i]))//数字
		{
			T = new BiTNode;
			T->data = str[i++];
			T->lchild = T->rchild = NULL;
			s.push(T);
		}
		else
		{
			switch (Precede(op.top(), str[i]))
			{
			case'<':
				op.push(str[i]);
				i++; break;
			case'=':
				op.pop();
				i++; break;
			case'>':
				T = new BiTNode;
				T->data = op.top();
				T->rchild = s.top();
				s.pop();
				T->lchild = s.top();
				s.pop();
				s.push(T);
				op.pop();
				break;
			}
		}
	}
	while (op.pop() != '#')
	{
		T = new BiTNode;
		T->data = op.top();
		op.pop();
		T->rchild = s.top();
		s.pop();
		T->lchild = s.top();
		s.pop();s.push(T);
	}
}

计算表达式树

double EvaluateExTree(BTree T)
{
	double a, b;
	if (T)
	{
		if (!T->lchild && !T->rchild)
			return T->data - '0';
		a = EvaluateExTree(T->lchild);
		b = EvaluateExTree(T->rchild);
		switch (T->data)
		{
		case'+': return a + b; break;
		case'-': return a - b; break;
		case'*': return a * b; break;
		case'/':
			if (b == 0)
			{
				cout << "divide 0 erroe!" << endl;
				exit(0);
			}
			return a / b; break;
		}
	}
}

1.2 多叉树结构

1.2.1 多叉树结构

主要介绍孩子兄弟链结构
孩子兄弟表示法就是既表示出每个结点的第一个孩子结点,也表示出每个结点的下一个兄弟结点。孩子兄弟表示法需要为每个结点设计三个域:一个数据元素域、一个指向该结点的第一个孩子结点的指针域、一个指向该结点的下一个兄弟结点的指针域。

声明:

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

1.2.2 多叉树遍历

void preorder(TR* T)
{
    if (T)
    {
        cout << T->data << " ";
        preorder(T->fir);
        preorder(T->sib);
    }
}

1.3 哈夫曼树
1.3.1 哈夫曼树定义
什么是哈夫曼树?,哈夫曼树解决什么问题?
哈夫曼树也叫最优二叉树哈夫曼树。给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
用途:
判别树:用于描述分类过程的二叉树。

如果每次输入量都很大,那么应该考虑程序运行的时间。

1.3.2 哈夫曼树的结构体

教材是顺序存储结构,也可以自己搜索资料研究哈夫曼的链式结构设计方式。

声明:

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

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

void CreateHT(HTNode ht[], int n)
{
	int i, k, lnode, rnode;
	double min1, min2;
	for (i = 1;i < 2 * n - 1;i++)//所有结点的相关域置初值-1
		ht[i].parent = ht[i].lchild = ht[i].rchild = -1;
	for (i = n;i <= 2 * n - 2;i++)//构造哈夫曼树的n-个分支结点
	{
		min1 = min2 = 32767;//lnode和rnode为最小权重的两个结点位置
		lnode = rnode = -1;
		for(k=0;k<=i-1;k++)//再ht[0...i-1]中找权值最小的两个结点
			if (ht[k].parent==-1)//只在尚未构造二叉树的结点中查找
			{
				if (ht[k].weight < min1)
				{
					min2 = min1; rnode = lnode;
					min1 = ht[k].weight;
					lnode = k;
				}
				else if (ht[k].weight < min2)
				{
					min2 = ht[k].weight;
					rnode = k;
				}
			}
		ht[i].weight = ht[lnode].weight + ht[rnode].weight;
		ht[i].lchild = lnode;
		ht[i].rchild = rnode;//ht[i]作为双亲结点
		ht[lnode].parent = i;
		ht[rnode].parent = i;
	}
}
void CreateHCode(HTNode ht[], HCode hcd[], int n)
{
	int i, j, k;
	HCode hc;
	for (i = 0;i < n;i++)
	{
		hc.start = n;
		k = i;
		j = ht[i].parent;
		while (j != -1)//循环至根结点
		{
			if (ht[j].lchild == k)
				hc.cd[hc.start--] == '1';//当前结点时的双亲结点的左孩子
			else
				hc.cd[hc.start--] == '0';//当前结点时的双亲结点的右孩子
			k = j; j = ht[j].parent;
		}
		hc.start++;//start指向哈夫曼编码最开始字符
		hcd[i] = hc;
	}
}

1.4 并查集

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。

优势:

并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多,如其求无向图的连通分量个数、最小公共祖先、带限制的作业排序,还有最完美的应用:实现Kruskar算法求最小生成树。

结构体

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

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

查找

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

合并:

void UnionSet(UFSTree t[], int x, int y)//两个元素各自所属的集合的合并
{//将x和y所在的子树合并
	x = FindSet(t, x);
	y = FindSet(t, y);//查找x和y所在的分离集合树的编号
	if (t[x].rank > t[y].rank)//y结点的秩小于x结点的秩
		t[y].parent = x;//将y连接到x结点上,x作为y的双亲结点
	else
	{
		t[x].parent = y;//将x连接到y结点上,y作为x的双亲结点
		if (t[x].parent == t[y].parent)//x和y的秩相同
			t[y].rank++;//y的秩加一
	}
}

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

树结构还是很有意思的,因为以前总是要写很多代码,现在到了树的结构,大多数都是直接用递归,代码量少(个别题目除外)。其实最主要的就是二叉树,不管是树还是森林,都可以转换成二叉树,在这个基础上进行操作,其中印象最深的的就是目录树了,虽然说操作起来有点麻烦,但是看到最终的结果还是十分有成就感的。但是结合之前知识点的题目又有点力不足的感觉,例如表达式树,究其原因应该是没有复习的锅了。树在生活中还是很多的,不仅仅是方便查找和存储数据,同时其中的哈夫曼树又可以用于数据的加密,涉及到一点密码学的内容,真的是很有意思了。

2.PTA实验作业(4分)

2.1 二叉树

输出二叉树每层节点

2.1.1 解题思路及伪代码

运用递归先序创造树结构,将#作为递归出口,然后层次遍历输出

2.1.2 总结解题所用的知识点

1.递归先序创造树结构
2.层次遍历

2.2 目录树

2.2.1 解题思路及伪代码

主要思路:

1.定义结构体node,包含1)名字name(2)指向下级目录指针ctl(3)指向下级文件指针file(4)指向同级目录或文件指针next(取决于它本身是目录还是文件)。

定义一个全局的指针ptr,指向上一个处理完毕的结点,比如一开始在输入“a\b\c”的中,ptr一开始指向root,从root开始处理a,处理完后ptr指向a,然后从ptr(即a)开始处理b,处理完后ptr指向b,再从ptr(即b)开始处理c。

2.处理一行数据时,字符串后带\的为目录,否则为文件。

3.假设插入一个目录类型结点s,从某一结点(设为x)开始,如果x的目录为空,直接插入;如果不为空,从x的指向的目录a开始,s与a比较,如果小于(按字典序排在前面),则在x和a之间插入s即可,否则,s继续跟a的next结点b比较,如果s小于b了,则在a与b之间插入s,以此类推……如果到链表遍历完毕都没有找到比s大的结点,说明s最大,需要放在最后,在遍历结束后直接让链表最后一个结点指向s即可。在s插入完毕后,ptr指向s,表明下一个结点的处理是从ptr结点开始的,如本段开头s的插入是从x结点开始的。

如果s跟x的目录重名了,则不需要插入s,直接令ptr指向x即可,然后开始下一个结点的处理(那就是从x,即ptr开始处理了)。

4.假设插入一个文件类型结点s,方法与上述插入目录结点时类似,只是插入文件结点时,是小于(即按字典序规则排在前面),等于(即重名)的时候插入,因为当

文件重名时,不能说只有一个文件,有n个文件重名那就是有n个文件,所以s仍然需要插入,这里跟目录的插入不同,重名的目录即使有多个也是只看成一个的,所以不需要重复插入了。

5.还要注意,在a和c之间插入b时,需要分情况处理,如果a与b同级,是a的next指向b;如果a比高一级,则a的ctl或者file指针指向b。

2.2.2 总结解题所用的知识点

1.孩子兄弟链
2.后序遍历目录树,对每个结点的子目录和子文件进行排序
3.先序遍历进行输出。

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

代码:

#include <iostream>
#include <cstdio>
using namespace std;

int main() {
    long long n, maxnum = -3500000000ll, maxlayer, cnt = 0, flag = 0;
    cin >> n;
    for(int layer = 0; ; layer++) { // 枚举每一层,习惯上从 0 开始
        long long sum = 0, a;
        //cout << "<<" << (1 << layer) << endl;
        for(int i = 0; i < (1 << layer); i++) { // 每一层的结点个数
            cin >> a;
            sum += a;
            if(++cnt >= n) {
                flag = 1;
                break;
            }
        }
        //cout << sum << endl;
        if(sum > maxnum)
            maxnum = sum, maxlayer = layer + 1;
        if(flag) break;
    }
    cout << maxlayer;
    return 0;
}

思路:

存储一个完全二叉树,可以使用一维数组存下所有结点。从上到下,从左到右,编号依次增加。可以发现,第一层有 1 个结点,第二层有 2 个,第三层有 4 个,第四层有 8 个……第 i 层有 [公式] 个结点。还可以发现,结点编号为 i 的话,它的左右子节点的编号分别是 2×i 和 2×i+1,不过这个和本题无关。

由于我们知道每一层的节点个数,可以直接输入数据后按层统计,甚至都可以不用数组把这些数字存下来。只要记录好读入的数字个数,在合适的时间退出循环就行。注意 << 符号是左移,1<<layer 的意思就是 [公式] 。如果读到的数字到了 n,那么就可以跳出循环,使用变量 flag 进行标记和判断。

本题有两个需要注意的地方

1.数据可能有小于 0 的,导致每层总和可能小于 0。
2.最多的一层可能有 100000-2^16 大约是 35000 个结点,所以要用 long long 防止总合爆 int。

posted @ 2021-05-02 20:13  年少不知头发贵  阅读(72)  评论(0编辑  收藏  举报