树
0.PTA得分截图
1.本周学习总结(5分)
1.1 二叉树结构
二叉树定义:1. 严格区分左、右子树。
2. 有限结点的集合。
二叉树性质:1. 非空二叉树上叶子结点数等于双分支结点数+1;
2. 非空二叉树第i层上至多有2^(i-1)个结点(i>=1);
3. 高度为h的二叉树至多有2^(h)-1个结点(h>=1);
4. 具有n个(n>0)结点的完全二叉树的高度为[log2(n)]+1;
1.1.1 二叉树的2种存储结构
树的顺序存储和链式存储结构,并分析优缺点。
- 顺序存储:
优点:对完全二叉树而言,简单、节省存储空间。
缺点:1. 对一般的二叉树,易造成存储空间的浪费。
2. 对二叉树做插入和删除结点操作时,要大量移动结点。
- 链式存储结构:
优点:1. 节省存储空间;
2. 插入删除结点时只需要修改指针。
缺点:寻找指定结点时很不方便。
1.1.2 二叉树的构造
总结二叉树的几种构造方法。分析你对这些构造方法的看法。务必介绍如何通过先序遍历序列和中序遍历序列、后序遍历序列和中序遍历序列构造二叉树。
1. 二叉树的顺序存储结构转成二叉链
代码
//如果i从0开始时 注意孩子和父亲的关系
BTree CreareBTree(string str, int i)//递归法建立二叉树
{
BTree t;
t = new TNode;
int len;
len = str.size();
if (i > len - 1 || i <= 0)
return NULL;
if (str[i] == '#')
{
return NULL;
}
else
{
t->data = str[i];//存入
}
t->lchild = CreareBTree(str, 2 * i);//左
t->rchild = CreareBTree(str, 2 * i + 1);//右
return t;
}
2. 先序遍历递归建树
ABDGCEHFI
代码实现:
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;
}
3. 先序遍历序列和中序遍历序列构造二叉树
任何n(n>=0)个不同结点的二叉树,都可由它的中序序列和先序序列唯一确定。
BTree Create(char *pre, char *in,int n)
{
BTNode*s;char *p; int k;
if(n<=0) return NULL;
s= new BTNode;
s->data= *pre ;
for (p=in; p<in+n;p++)
if(*p==*pre)
break;
k=p-in;
s->ichild =CreateBT1(pre+1,in,k); //构造左子树
s->rchild =CreateBT1(pre+k+1,p+1,n-k-1);//右子树
return s;
}
1.1.3 二叉树的遍历
总结二叉树的4种遍历方式,如何实现。
. 二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问依次且仅被访问一次。
. 四种遍历方式分别为:先序遍历、中序遍历、后序遍历、层序遍历。
- 前序遍历,所谓的前序遍历就是先访问根节点,再访问左节点,最后访问右结点。
如上图所示,前序遍历结果为:ABDFECGHI
代码实现:
/**
* 二叉树前序遍历 根-> 左-> 右
* @param node 二叉树节点
*/
public static void preOrderTraveral(TreeNode node){
if(node == null){
return;
}
System.out.print(node.data+" ");
preOrderTraveral(node.leftChild);
preOrderTraveral(node.rightChild);
}
- 中序遍历,所谓的中序遍历就是先访问左节点,再访问根节点,最后访问右节点.
如上图所示,中序遍历结果为:DBEFAGHCI
代码实现
/**
* 二叉树中序遍历 左-> 根-> 右
* @param node 二叉树节点
*/
public static void inOrderTraveral(TreeNode node){
if(node == null){
return;
}
inOrderTraveral(node.leftChild);
System.out.print(node.data+" ");
inOrderTraveral(node.rightChild);
}
- 后序遍历,所谓的后序遍历就是先访问左节点,再访问右节点,最后访问根节点。
如上图所示,后序遍历结果为:DEFBHGICA
代码实现:
/**
* 二叉树后序遍历 左-> 右-> 根
* @param node 二叉树节点
*/
public static void postOrderTraveral(TreeNode node){
if(node == null){
return;
}
postOrderTraveral(node.leftChild);
postOrderTraveral(node.rightChild);
System.out.print(node.data+" ");
}
- 层序遍历
具体步骤如下:
(1). 首先申请一个新的队列,记为queue;
(2). 将头结点head压入queue中;
(3). 每次从queue中出队,记为node,然后打印node值,如果node左孩子不为空,则将左孩子入队;如果node的右孩子不为空,则将右孩子入队;
(4). 重复步骤3,直到queue为空。
代码实现:
public static void levelOrder(TreeNode root){
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
root = queue.pop();
System.out.print(root.data+" ");
if(root.leftChild!=null) queue.add(root.leftChild);
if(root.rightChild!=null) queue.add(root.rightChild);
}
}
1.1.4 线索二叉树
线索二叉树如何设计?
中序线索二叉树特点?如何在中序线索二叉树查找前驱和后继?
1.线索二叉树
*. 二叉树存储结构时,每个节点有两个指针域,总共有2n个指针域。
*. 有效指针域:n-1(根节点没指针指向);
*. 空指针域:n+1;
*. 利用这些空链域,指向该线性序列中的“前驱”和“后继”的指针,称作线索。
2. 线索化二叉树
1). 若结点有左子树,则lchild指向左孩子;否则,lchild指向其直接前驱(即线索);
2). 若结点有右子树,则rchild指向其右孩子;否则,rchild指向其直接后继(即线索)。
typedef struct node
{
ElemType data;//结点数据域
int ltag, rtag;//增加的线索标记
struct node *lchild;//左孩子或线索指针
struct node *rchild;//右孩子货线索指针
}TBTNode;//线索树结点类型定义
- 中序线索二叉树
(1). 特点:
- 在线索二叉树中再增加一个头结点
- 头结点左孩子指向根节点
- 右孩子为线索,指向最后一个孩子
- 遍历序列第一个结点前驱为头结点,最后一个节点后继为头结点
(2). 在中序线索二叉树查找前驱和后继 - 结点后继
. 结点有右孩子,则为右子树最左孩子节点。
. 结点无右孩子,则为后继线索指针指向节点。 - 结点前驱
. 结点有左孩子,则为左子树最右那个孩子。
. 结点无左孩子,则为前驱线索指针指向节点。
代码:
TBTNode *pre;//全局变量
TBTNode *CreatThread(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;
}
void Thread(TBTNode *&p)//对二叉树b进行中序线索化
{
if (p != NULL)
{
Thread(p->lchild);//左子树线索化
if (p->lchild = ENULL)//前驱线索化
{
p->lchild = pre; p->ltag = 1;
} //建 立当前结点的前驱线索else p->Itag=0;
if (pre->rchild = NULL)//后继线索化
{
pre->rchild = p; pre->rtag = 1;
} // 建 立前驱结点的后继线索else pre->rtag = 0;
pre FP;
Thread(p->rchild); // 递归 调用石子树线索化
}
}
1.1.5 二叉树的应用--表达式树
介绍表达式树如何构造
如何计算表达式树
题目:
表达式树的叶子是操作数,内部节点是操作符。
- 上面所对应的表达式是
(a+b)*(c*(d+e);
*对该树进行后序遍历得到后缀表达式
ab+cde+**;
这里实现的是如何根据一个后缀表达式,构造出其相应的表达式树。
- 算法思想:主要就是栈的使用。算法时间复杂度是O(n),n是后缀表达式长度。
从前向后依次扫描后缀表达式,如果是操作数就建立一个单节点树,并把其指针压入栈。
如果是操作符,则建立一个以该操作符为根的树,然后从栈中依次弹出两个指针(这2个指针分别指向2个树),作为该树的
左右子树,然后把指向这棵树的指针压入栈。直到扫描完后缀表达式。
最后栈中就会只有一个指针。这个指针指向构造的表达式树的根节点。
代码:
#include <iostream>
#include <stack>
using namespace std;
struct Node
{
char data;
Node *left, *right;
Node(char c) :data(c), left(NULL), right(NULL) {}
};
Node *create_ET(char *src)
{
stack<Node*> st;
char *s = src;
Node *temp;
while (*s)
{
if (*s == '+' || *s == '-' || *s == '*' || *s == '/')
{
temp = new Node(*s);
temp->right = st.top();
st.pop();
temp->left = st.top();
st.pop();
st.push(temp);
}
else
{
temp = new Node(*s);
st.push(temp);
}
s++;
}
return st.top();
}
void postorder(Node *root)//这是后序遍历
{
if (root != NULL)
{
postorder(root->left);
postorder(root->right);
cout << root->data << ' ';
}
}
int main()
{
Node *r = create_ET("ab+cde+**");
postorder(r);
return 0;
}
1.2 多叉树结构
1.2.1 多叉树结构
主要介绍孩子兄弟链结构
- 双亲存储结构
结构体定义:
typedef struct
{
ElemType data; //结点的值
int parent; //指向双亲的位置
}PTree[MaxSize];
缺点:找父亲容易,找孩子不容易
2. 孩子链存储结构
结构体定义:
typedef struct node
{
ElemType data; //结点的值
struct tnode *sons[MaxSons]; //指向孩子结点
}TSonNode;
缺点:空指针太多,找父亲不容易
3. 孩子兄弟链存储
结构体定义:
typedef struct tnode
{
ElemType data; //结点的值
struct tnode *son; //指向兄弟
struct tnode *brother; //指向孩子结点
}TSBNode;
缺点:每个结点固定只有两个指针域,找父亲不容易。
注:1). 在一棵树中最常用的操作是查找某个结点的祖先结点,采用双亲存储结构最合适。
2). 如最常用的操作是查找某个结点的所有兄弟,采用孩子链存储结构或者孩子兄弟链存储结构。
1.2.2 多叉树遍历
介绍先序遍历做法
定义:它是由n(n>=0)个有限结点组成一个具有层次关系的集合
- 先根遍历(递归)(根左右)
若树不空,则先访问根结点,然后依次先根遍历各棵子树 - 后根遍历(递归)(左右根)
若树不空,则先依次后根遍历各棵子树,然后访问根结点 - 层次遍历
若树不空,则自上而下、自左至右访问树中每个结点。
1.3 哈夫曼树
1.3.1 哈夫曼树定义 - 什么是哈夫曼树?,哈夫曼树解决什么问题?
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树
也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。 - 构造原则:
权值越大的叶结点越靠近根结点;
权值越小的叶结点越远离根结点;
WPL:带权路径和
1.3.2 哈夫曼树的结构体
教材是顺序存储结构,也可以自己搜索资料研究哈夫曼的链式结构设计方式。
- 顺序结构
typedef struct
{ char data; //节点值
float weight; //权重
int parent; //双亲节点
int lchild; //左孩子节点
int rchild; //右孩子节点
} HTNode;
- 初始化哈夫曼树
typedef struct
{
int data;
int parent;
int lchild;
int rchild;
}HTNode,*HuffmanTree;
void CreateHTree(HuffmanTree &ht, int n)
{
int len;
len = 2 * n - 1;
ht = new HTNode[len];
}
1.3.2 哈夫曼树构建及哈夫曼编码
结合一组叶子节点的数据,介绍如何构造哈夫曼树及哈夫曼编码。
(可选)哈夫曼树代码设计,也可以参考链式设计方法。
哈夫曼树并不唯一,但带权路径长度一定是相同的。
- 由给定结点构造哈夫曼树
- 8个结点的权值大小如下:
- 从19,21,2,3,6,7,10,32中选择两个权小结点。选中2,3。同时算出这两个结点的和5。
- 从19,21,6,7,10,32,5中选出两个权小结点。选中5,6。同时计算出它们的和11。
- 从19,21,7,10,32,11中选出两个权小结点。选中7,10。同时计算出它们的和17。
(注:这时选出的两个数字都不是已经构造好的二叉树里面的结点,所以要另外开一棵二叉树;或者说,如果两个数的和正好是下一步的两个最小数的其中的一个,那么这个树直接往上生长就可以了,如果这两个数的和比较大,不是下一步的两个最小数的其中一个,那么就并列生长。)
- 从19,21,32,11,17中选出两个权小结点。选中11,17。同时计算出它们的和28。
- 从19,21,32,28中选出两个权小结点。选中19,21。同时计算出它们的和40。另起一颗二叉树。
- 从32,28, 40中选出两个权小结点。选中28,32。同时计算出它们的和60。
- 从 40, 60中选出两个权小结点。选中40,60。同时计算出它们的和100。 好了,此时哈夫曼树已经构建好了。
- 由上述所得哈夫曼树写出哈夫曼编码
这棵构造好的哈夫曼树:(经过左边路径为0,经过右边路径为1)。
则可直接写出编码,例如:
A:11 B:001 C:011 D E:0000 F:0001 G:0100 H:0101 I:1000 J:1001
为了简便起见,我们从树的左边开始考虑,即B,E,F节点。
. 对于节点B,其深度为3,权值为5,那么其带权路径长度为5*3 = 15;
. 那么我们再看一下节点B的父亲节点,其权值为9,是由权值为4和权值为5的节点B构造而成,那么即是9 = 4 + 5;
. 同样的再往上一层,节点B的爷爷节点,其权值为16,是由权值为9和权值为7的节点构造而成,而权值为9的节点的构造前面已经说明,则有16 = 4 + 5 + 7;
再往上一层就到根节点了。
. 那么到这里我们可以看到,节点B的父亲节点和爷爷节点的组成部分都有节点B的“功劳”,即节点B的权值是其另外两个的“组成部分”,那么节点B的带权路径长度即为其到根节点路径上(不包含根节点),与其(或者说是与其父节点,爷爷节点等)有父子关系的节点抽取出节点B的组成部分(包括节点B本身),再全部相加,这样的话就得到了节点B的带权路径长度为5 + 5 + 5 = 15;
. 同样的,节点E,F按照同样的方法进行推导。
所以我们从上面的分析得出:
. 每个带权叶节点到根节点的带权路径长度等于其到根节点路径上所有节点的包含. . 该带权叶节点权值组成部分之和。
. 因此,最后我们推导出,所有叶节点,即整棵哈夫曼树的带权路径长度 WPL即为:
. 除了根节点以外,所有节点的权值之和。
. 如上图哈夫曼树的带权路径长度 WPL即为:
WPL = 16 + 10 + 9 + 7 + 5 + 5 + 4 + 5 + 3 + 4 + 2 + 3 + 2 + 2 + 2 + 1 + 1 + 1 = 82
1.4 并查集
1. 什么是并查集?
并查集,在一些有N个元素的集合)应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
合并查找的集合,用于多个可以不相关的树的合并与查找。
对于大量数据根据某些特征进行合并,查找,不仅空间需要很大,时间效率也比较低
2. 并查集解决什么问题,优势在哪里?
(1). 初始化:每个点看做一棵树 ,并且为每个树的树根;树根就是每个组别的代表。
(2). 查询:对于点对(a,b),通过a和b去向上查找他们的祖先节点直到树根,如果有相同的祖先节点,则他们在已经在一棵树下,属于同一组别。
(3). 合并:若不在同一组别,令其中一个点(比如a)所在树的根节点成为另一个点(比如b)的根节点的孩子。这样即便再查询到a,最终会判断认为a属于b的组别。
大树小树合并技巧: 小树变成大树的子树,会比大树变成小树的子树更加不易增加树高,这样可以减少查询次数。
- 并查集的结构体
typedef struct node
{ int data; //结点对应人的编号
int rank; //结点秩:子树的高度,合并用
int parent; //结点对应双亲下标
} UFSTree; //并查集树的结点类型
- 初始化
int fa[MAXN];
inline void init(int n)
{
for (int i = 1; i <= n; ++i)
fa[i] = i;
}
- 查询
int find(int x)
{
if(fa[x] == x)
return x;
else
return find(fa[x]);
}
- 合并
inline void merge(int i, int j)
{
fa[find(i)] = find(j);
}
1.5.谈谈你对树的认识及学习体会。
. 对于树学到了树的多种存储结构,孩子树,双亲树,孩子兄弟树,二叉树,结构体中的内容差不多都是值和指针的组合,但是也各有个的优缺点,比如孩子树找孩子十分方便找双亲就困难,空指针域也较多比较浪费等。对于树的操作一般从建树开始,然后遍历,遍历又分为多种顺序的遍历,比如二叉树的遍历就有四种,其中先序中序后序遍历可用递归完成,其后就是一些求高度求宽度找路径,插入删除的操作。
. 关于树的学习,我觉得要记住的内容有点多,树的基本术语,树的性质,好多条没有好记性记了好久一直忘记或记错,还有就是画树的课堂派题目,用画图软件画真的很费时间,一题能画半个小时,最近这两周的时间比较不够选修课有大作业,树有大作业又要复习概率,又有物理实验,几乎没时间写pta ,题目真的写得少,然后不知不觉树学完了。
2.PTA实验作业(4分)
此处请放置下面2题代码所在码云地址(markdown插入代码所在的链接)。如何上传VS代码到码云
2.1 二叉树
输出二叉树每层节点
- 整体思路:运用两个指针记住当前结点位置和每层的最后一个结点位置,以便于当当前结点等于层最后结点时,输出换行并输出下一层结点;而每层的结点都要通过队列依次存入并且依次输出。
伪代码:
void LayerNode(BTree bt)//输出层节点
定义layer等于1表示输出的层数
定义flag用于判断是否为第一层结点
定义一个BTree型的队列qtree,用于存放结点
定义tNode,node和lastNode指针分别用于存放遍历中途结点的孩子结点,此时遍历到的结点和下一层要的最够一个结点
node=lastNode=bt
if bt不为空 do
bt入队列
else do
输出NULL并返回
end if
while 队列qtree不为空 do
if flag=0 do
输出“1:”,并将flag置为1 //即只在第一次循环时输出1
end if
node取队列qtree的front
if node->rchild do
将node->rchild赋值给tNode
else if node->lchild do
将node->lchild赋值给tNode /*先判断右结点是为了保证下一层最后一个结点在右孩子结点,若不在,则再判断左孩子*/
end if
输出node->data
并将node的左右孩子入队 //实现层层的遍历
将node结点出队
if node=lastNode and 队列不为空 do
换行再输出++layer //即下面输出的是下一层结点
lastNode = tNode //将下一层最右边的孩子结点赋值给lastNode
end if
end while
发现当每一层最后结点不在上一层最后结点的右结点,则答案会出现错误;解决方法是记录上一层每个结点最靠右边的结点,在要进入下一层结点遍历时赋值给lastNode即可;
2.2 目录树
建树之后就是先序遍历,每次递归一层就多输出两个空格,回溯的时候减少两个输出空格数量。然后需要对每一层的目录和文件按字典序排序,题目要求先输出目录再输出文件,所以先按目录还是文件排序然后再按字典序排序就行。
#include <bits/stdc++.h>
using namespace std;
struct File
{
char name[300];
};
struct Node
{
char name[300];
Node* next[300];
File* files[300];
int next_cnt = 0;//子目录个数
int files_cnt = 0;//文件个数
};
int Nodecmp(Node* a, Node* b)//目录字典序排序
{
return strcmp(a->name, b->name) < 0;
}
int Filecmp(File* a, File* b)//文件字典序排序
{
return strcmp(a->name, b->name) < 0;
}
void PlantTree(Node* p, char *s)
{
int t = 0, pos, len = strlen(s);
for (pos = 0; s[pos] != '\\'&&pos < len; pos++);//找到第一个‘\’
if (s[pos] == '\\') {
char str[300];
int f = 0;
strncpy(str, s, pos);//将'\'前的字符串赋值给str
str[pos] = '\0';
Node *a;
for (int i = 0; i < p->next_cnt; i++)
{
if (strcmp(str, p->next[i]->name) == 0)
{//如果已经存在此目录
f = 1;
a = p->next[i];//赋值给a,用于下一次调用
break;
}
}
if (f == 0)
{//如果不存在此目录
a = (Node*)malloc(sizeof(Node));
strcpy(a->name, str);
a->files_cnt = 0;
a->next_cnt = 0;
p->next[p->next_cnt] = a;//将此目录存入节点中
p->next_cnt++;//子目录数+1
}
if (pos < strlen(s) - 1)PlantTree(a, s + pos + 1);//如果之后还有字符串,继续调用自身函数
else return;
}
else
{//如果是一个文件
File* b = (File*)malloc(sizeof(File));
strcpy(b->name, s);
p->files[p->files_cnt] = b;//将此文件存入节点中
p->files_cnt++;//文件数+1
return;
}
}
void PrintTree(Node* p, int n)
{
for (int i = 0; i < n; i++)cout << " ";//每递归调用一次就是目录更深一次,空两个空格
cout << p->name << endl;//输出目录
n++;
sort(p->next, p->next + p->next_cnt, Nodecmp);//排序
for (int i = 0; i < p->next_cnt; i++)PrintTree(p->next[i], n);//递归调用,依次输出文件和目录
sort(p->files, p->files + p->files_cnt, Filecmp);//排序
for (int i = 0; i < p->files_cnt; i++) {
for (int j = 0; j < n; j++)cout << " ";
cout << p->files[i]->name << endl;//输出文件
}
return;
}
int main()
{
int n;
cin >> n;
Node* root = (Node*)malloc(sizeof(Node));
strcpy(root->name, "root");
for (int i = 0; i < n; i++)
{
char s[300];
cin >> s;
PlantTree(root, s);
}
PrintTree(root, 0);
return 0;
}
3.阅读代码(0--1分)
找1份优秀代码,理解代码功能,并讲出你所选代码优点及可以学习地方。主要找以下类型代码:
代码相关地址:http://www.cnblogs.com/NeilHappy/archive/2013/04/04/2999050.html
层次遍历是用队列,一级一级地入队列然后输出。