DS 数据结构-树
PTA
树思维导图
-
树的定义
n(n>=0)个结点的有限集,当n=0时,是空树,否则为非空树
空树
非空树 -
非空树的特点:
1、有且仅有一个特定的根结点,不允许存在多个根结点
2、除根结点以外的其余结点可分为m(m>0)个互不相交的有限集,其中每一个有限集本身还是一棵树,称为根的子树。子树的个数没有限制,但是一定不能有交集。
完全二叉树
满二插树
树的基本术语
度:树中某个结点的子树的个数称为该结点的度;树中所有结点的度中的最大值称为树的度。通常将度为m的树称为m次树。
- 分支结点与叶子结点
分支结点:树中度不为0的结点
叶子结点:度为0的结点
单分支结点:度为1的结点
双分支结点:度为2的结点 - 路径
对于树种的任意两个结点Ki,Kj;若树中村在一个结点序列(Ki,Ki1,Ki2.....Kin,Kj),使得序列中除Ki以外的任一结点都是其在序列中前一个结点的后继结点,则该序列为Ki到Kj的一条路径 - 路径长度
该路径所通过的结点数目-1 - 孩子结点
每个结点的后继节点 - 双亲结点
每个结点的前驱结点 - 兄弟结点
具有同一双亲结点的孩子互为兄弟结点 - 子孙结点
每个结点对应子树中的所有结点(除自身外)称为该结点的子孙结点 - 结点层次结点深度
从树根开始定义,根结点为第一层,,它的孩子为第二层,以此类推。 - 树的高度或深度
树中结点的最大层次 - 森林
n(n>0)个互补相交的树的集合的合称
树的性质
1、结点树等于所有结点的度数之和加一
2、度为m的数中第i层上最多有m^i-1个结点(i>=1)
3、当一棵m次数的第i层上有m^i-1(i>=1)个结点时,该层是满的
4、高度为h的m次数最多有(m^h-1)/(m-1)个结点
5、具有n个结点的m次树的最小高度为[log m (n(m-1)+1]
1.1二叉树的结构
1.11 2种存储结构
- 顺序
用一组地址连续的存储单元以此自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组中下标为i-1的分量中。对于一般二叉树,则应将其每个结点与完全二叉树上的结点相对照,存储在一维数组的相应分量中,空缺表示不存在此结点。因此,这种顺序存储结构仅适用于完全二叉树,因为,在最坏的情况下,一个深度为k且只有k个节点的单支树(树中不存在度为2的结点)却需要长度为2k-1的一维数组。
优点:对于第i个结点,容易找到它的父亲和孩子结点。父亲结点:i/2;左孩子:2i;右孩子:2i+1.对于完全二叉树,顺序存储结构十分方便。
缺点:对于普通二叉树,空间利用率低。查找,插入,删除不方便。
- 链式
二叉树的链式存储结构(简称二叉链表)是指用一个链表来存储一棵二叉树,二叉树中每一个结点用链表中的一个链结点来存储。在二叉树中,标准存储方式的结点结构如图所示:
其中,data表示值域,用于存储对应的数据元素,lchild和rchild分别表示左指针域和右指针域,分别用于存储左子女结点和右子女结点(即左、右子树的根结点)的存储位置(即指针)。
结构体
typedef struct node
{
int data;
struct node* lchild;
struct node* rchild;
}BTree;
n个结点,指针域:2n;非空指针域n-1;空指针域:n+1;分支数n-1。
1.12 二叉树的创建
- 先序
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];
}
- 先序,中序遍历构造二叉树
1、先序遍历提供根结点,
2、中序遍历提供了由根结点将整个序列分为左,右子树的信息(根结点左边为左子树,根结点右边右子树)
3调用递归,将左子树和右子树堪称一棵二叉树,重复上面的步骤
- 后序,中序遍历构造二叉树
1、后序遍历的最后一个结点为根结点
2、找出根结点在中序遍历中的位置,根左边为左子树,根右边为右子树
3、调用递归,将左子树和右子树堪称一棵二叉树,重复上面的步骤
- 层次遍历,中序遍历构造二叉树
1、根据层次遍历的第一个确定根节点
2、找出根结点在中序遍历的位置,根左边为左子树,根右边为右子树
3、调用递归,将左子树和右子树堪称一棵二叉树,重复上面的步骤
1.13 二叉树的遍历
- 先序遍历
void PreorderPrintLeaves(BinTree BT)
{
if (BT != NULL)
{
cout<<BT->Data<<" ";
PreorderPrintLeaves(BT->Left);
PreorderPrintLeaves(BT->Right);
}
}
- 中序
void PreorderPrintLeaves(BinTree BT)
{
if (BT != NULL)
{
PreorderPrintLeaves(BT->Left);
cout<<BT->Data<<" ";
PreorderPrintLeaves(BT->Right);
}
}
- 后序遍历
void PreorderPrintLeaves(BinTree BT)
{
if (BT != NULL)
{
PreorderPrintLeaves(BT->Left);
PreorderPrintLeaves(BT->Right);
cout<<BT->Data<<" ";
}
}
- 层次遍历
BTree CreateBTree(string str,int i)
{
int len;
BTree bt;
bt=new TNode;
len=str.size();
if(i>len-1||i<=0)
{
return NULL;
}
if(str[i]=='#')return NULL;
bt->data=str[i];
bt->lchild=CreateBTree(str,2*i);
bt->rchild=CreateBTree(str,2*i+1);
return bt;
}
1.14 线索二叉树
若结点有左子树,则lchild指向左孩子,否则指向前驱(即线索),
若结点有右子树,则rchild指向右孩子,否则指向后继(即线索)。
线索需用虚线表示,线索要依据遍历来设计
- 中序线索二叉树
为避免悬空,应增设头结点
头结点左孩子指向根结点,右孩子为线索指向最后一个孩子,遍历序列第一个结点前驱为头结点,最后一个结点后继为头结点
找任一结点前驱:有左孩子,则为左子树最右孩子结点;若无左孩子,则为前驱线索指针指向结点。
找任一结点后继:有右孩子,则为右子树最左孩子结点;若无右孩子,则为后继线索指针指向结点。
好处:遍历二叉树不需要递归,所有结点只需遍历一次,也没有用栈,空间利用率高。时间复杂度O(n)
typedef struct node
{
int data;
int ltag,rtag;,//增加的线索标记
struct node *lchild,*rchild;//右孩子或线索指针
}TBTNode;
1.15 二叉树的应用--表达式树
- 构造
思路
定义num,op栈分别存放数字和运算符,将#入op栈,
当读入的表达式不为空时:(利用while循环)
1、若读入的字符是数字,新建结点,并将新建结点的左右孩子置空,再将数字入num栈
2、若读入的字符str[i]是运算符,判断op栈与读入字符str[i]的优先级,(利用switch)若较小,str[i]入op栈,跳出switch;若相等,op栈出栈,跳出循环;若较大,新建结点T,将op栈顶元素赋值给T指向的数据,再取num栈栈顶赋值给T->rchild,num栈顶出栈,再取num栈栈顶赋值给T->lchild,num栈顶出栈,跳出循环。
单独判断op栈栈顶还有运算符时(利用while循环)
新建节点,
T->data = op.top();
op.pop();
T->rchild = num.top();
num.pop();
T->lchild = num.top();
num.pop();
num.push(T);
代码
void InitExpTree(BTree& T, string str) //建表达式的二叉树
{
stack <BTree> num;//存放数字
stack<char> op;//存放运算符
op.push('#');//必须将#进哦op栈
int i = 0;
while (str[i])
{
if (!In(str[i]))//数字
{
T = new BiTNode;
T->data = str[i++];
T->lchild = T->rchild = NULL;//将左右孩子置空
num.push(T);
}
else//运算符
{
switch (Precede(op.top(), str[i]))
{
case'<':op.push(str[i]); i++; break;//运算符比op栈顶低,入op栈
case'=':op.pop(); i++; break;//运算符与op栈顶相等,op栈顶出栈
case'>':T = new BiTNode;//运算符比op栈顶高,新建结点T
T->data = op.top();//取op栈顶运算符
T->rchild = num.top();//数字栈取栈顶,并出栈
num.pop();
T->lchild = num.top();
num.pop();
num.push(T);//将新建结点入栈
op.pop();//op栈出栈
break;
}
}
}
while (op.top() != '#')
{
T = new BiTNode;
T->data = op.top();
op.pop();
T->rchild = num.top();
num.pop();
T->lchild = num.top();
num.pop();
num.push(T);
}
}
- 计算
当T不为空时,
T的左右孩子都为空时,return T->data-'0';//最终结果
否则//给a b赋值
a = EvaluateExTree(T->lchild);
b = EvaluateExTree(T->rchild);
判断运算符(switchT->data)
+:return a + b; break;
-:return a - b; break;
*:return a * b; break;
/:
if (b == 0)
{
cout << "divide 0 error!" << endl;
exit(0);
}
return a / b; break;
代码
uble 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 error!" << endl;
exit(0);
}
return a / b; break;
}
}
}
1.2 多叉树
1.21 多叉树结构
多叉树是指一个父节点可以有多个子节点,但是一个子节点依旧遵循一个父节点定律,通常情况下,二叉树的实际应用高度太高,可以通过多叉树来简化对数据关系的描述。
例如:Linux文件系统,组织架构关系,角色菜单权限管理系统等,通常都基于多叉树来描述。
typedef struct node_t
{
char * name;//结点名
int numc;//子结点个数
int level;//某一结点再多叉树中的层数
struct node_t** children;//指向其自身的子节点,children一个数组,该数组中的元素是node_t指针
}NODE;对结构体重命名
//实现一个栈,用于后续操作
typedef struct stack_t
{
NODE**arry;//arry 是一个数组,其内元素是NODE*型指针
int index;//栈顶元素
int size;//栈的大小
}STACK;//重命名
//实现一个队列,用于后续操做
typedef struct queue_t
{
NODE**arry;//arry 是一个数组,其内元素是NODE*型指针
int head;//队头
int tail;//队尾
int num;//队中元素个数
int size;//栈的大小
}QUEUE;
//生成多叉树结点
NODE* create_node()
{
NODE* q;
q=new NODE*;
q->numc=0;
q->level=-1;
q->children=NULLL;
return q;
}
1.22多叉树遍历
先根遍历(递归,根左右)
后根遍历(递归,左右根)
层次遍历
1.3 哈夫曼树
1.3.1 哈夫曼树定义
什么是哈夫曼树?,哈夫曼树解决什么问题?
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
哈夫曼编码:根结点到叶子结点经过的路径组成的0 1序列,左分支 0,右分支 1
求传送报文的最短长度问题转化为求由字符集中的所有字符作为叶子结点,由字符出现频率作为其权值所产生的哈夫曼树的问题。利用哈夫曼树来设计二进制的前缀编码,既满足前缀编码的条件,又保证报文编码总长最短。
- 特点
没有单分支结点n1=0;因为每次两棵树合并
n=n0+n1+n2=n0+n2=2n0-1
1.3.2 哈夫曼树的结构体
typedef struct
{
char data;//节点值
float weight;//权重
int parent;//父亲结点
int lchild;//左孩子结点
int rchild;//右孩子结点
}HTNode;
1.3.3 哈夫曼树构建及哈夫曼编码
过程
1、根据给定的n个权值{w1,w2,……wn},构造n棵只有根结点的二叉树。F={T1,T2,…,Tn}。
2、在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其
左、右子树根结点权值之和。
3、在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中。
4、重复(2)、(3)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。
- 思路
1.初始化哈夫曼数组ht,包含n个叶子结点,2n-1个总节点
所有2n-1个节点的parent、lchild和rchild域置为初值-1。
输入n个叶子节点有data和weight域值
2.构造非叶子节点ht[i](存放在ht[n]~ht[2n-2]中)
从ht[0] ~ht[i-1]中找出根节点(即其parent域为-1)最小的两个节点ht[lnode]和ht[rnode]
ht[lnode]和ht[rnode]的双亲节点置为ht[i],并且ht[i].weight= ht[lnode].weight+ht[rnode].weight。
3.如此这样直到所有2n-1个非叶子节点处理完毕。
代码 时间复杂度O(n^2)
void CreateHT(HTNode ht[],int n)
{ int i,j,k,lnode,rnode; float min1,min2;
//此处补充叶子节点相关设置
for (i=0;i<2*n-1;i++) //所有节点的相关域置初值-1
ht[i].parent=ht[i].lchild=ht[i].rchild=-1;
for (i=n;i<2*n-1;i++) //构造哈夫曼树
{ min1=min2=32767; lnode=rnode=-1;
for (k=0;k<=i-1;k++)
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; }
} //if
ht[lnode].parent=i;ht[rnode].parent=i;
ht[i].weight=ht[lnode].weight+ht[rnode].weight;
ht[i].lchild=lnode;ht[i].rchild=rnode;
}
}
对一组权值{w1,w2,......,wn}哈夫曼树不唯一,但WPL唯一
1.4 并查集
什么是并查集?
并查集,在一些有N个元素的集合)应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
并查集的结构体、查找、合并操作如何实现?
**结构体**
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=i;//数据为该人的编号
t[i].rank=0;//秩初始化为0
t[i].parent=i;//双亲初始化指向自己
}
**查找**
void 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); //查找x所在分离集合树的编号
y=FIND_SET(t,y); //查找y所在分离集合树的编号
if (t[x].rank>t[y].rank) //y结点的秩小于x结点的秩
t[y].parent=x; //将y连到x结点上,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
}
}//对于n个人,合并算法的时间复杂度=查找的复杂度=O(log2(n))
1.5对树的认识及学习体会
树的专业术语较多,比较难记住,且树中许多遍历等都用到了递归,比去年学的递归更难以理解,特别是有高度,参数传递方面很重要。
PTA作业
2.1 二叉树
-
输出每层结点
思路
定义LastNode, NowNode,TNode//上一结点,现在结点,临时结点
LastNode = bt;
NowNode = bt;
q.push(bt);
利用while遍历队列,
让NowNode = q.front();
若NowNode的右孩子不为空,则让它的右孩子赋值给TNode
若NowNode的左孩子不为空,则让它的左孩子赋值给TNode
输出NowNode指向的数据
若NowNode的左孩子不为空,进队
若NowNode的右孩子不为空,进队
删除队首元素
NowNode == LastNode && !q.empty()
换行输出层数
更新LastNode=NowNode -
代码
#include <iostream>
#include <string>
#include <queue>
//#define MaxSize 103;
using namespace std;
typedef struct node
{
char data;
struct node* lchild, * rchild;
}BTNode;
typedef BTNode* BTree;
BTree CreateTree(string str, int &i);
void PrintTree(BTree bt);
int main()
{
string str;
BTree BT;
cin >> str;
int i = 0;
BT = CreateTree(str, i);
PrintTree(BT);
return 0;
}
BTree CreateTree(string str, int &i)
{
BTree T;
if (i > str.size()-1)
return NULL;
if (str[i] == '#')
return NULL;
T = new BTNode;
T->data = str[i];
T->lchild = CreateTree(str, ++i);
T->rchild = CreateTree(str, ++i );
return T;
}
void PrintTree(BTree bt)
{
queue<BTree>q;
if (!bt)
{
cout << "NULL";
return;
}
BTree LastNode, NowNode,TNode;
LastNode = bt;
NowNode = bt;
q.push(bt);
int flag = 0;
int h = 1;
while (!q.empty())
{
if (flag == 0)
{
cout << "1:";
flag = 1;
}
NowNode = q.front();
if (NowNode->rchild)
TNode = NowNode->rchild;
else if (NowNode->lchild)
TNode = NowNode->lchild;
cout<<NowNode->data<<",";
if (NowNode->lchild)
q.push(NowNode->lchild);
if (NowNode->rchild)
q.push(NowNode->rchild);
q.pop();
if (NowNode == LastNode && !q.empty())
{
cout << endl;
cout << ++h << ":";
LastNode = TNode;
}
}
}
悦读代码
代码
#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;
}
- 思路
利用flag控制是否完全遍历计算每一层,利用for进行每层权值相加,若遍历相加完,flag=1,跳出循环,最后判断哪一个和最大,并输出 - 伪代码
for layer = 0; // 枚举每一层,习惯上从 0 开始
long long sum = 0, a;
layer++
end for
for int i = 0 toi < (1 << layer)// 每一层的结点个数
cin >> a;
sum += a;
if++cnt >= n
then flag = 1;
break;
end if
i++;
end for
if sum > maxnum
then maxnum = sum, maxlayer = layer + 1;
end if
if flag
then break;
end if
}
输出 maxlayer;
- 解题优势
只要利用简单的for循环和if条件判断即可,不需要借助栈,队列来求解 - 注意点
1、数据可能有小于 0 的,导致每层总和可能小于 0。
2、最多的一层可能有 100000-2^16 大约是 35000 个结点,所以要用 long long 防止总合爆 int。