DS博客作业03--树
| 这个作业属于哪个班级 | C语言--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业03--树 |
| 这个作业的目标 | 学习树结构设计及运算操作
| 名字 | 黎钊涵
0.PTA得分截图
1.本章学习总结
1.1二叉树
树是一种重要的非线性数据结构,直观地看,它是数据元素(在树中称为结点)按分支关系组织起来的结构
1.1.1二叉树结构
- 特点:
1.二叉树,顾名思义每个结点最多有两颗子树
2.左子树和右子树是有顺序的,次序不能任意颠倒
3.即使树中某结点只有一颗子树,也要区分它是左子树还是右子树 - 二叉树的五种基本形态
1.空二叉树
2.只有一个根结点
3.根结点只有左子树
4.根结点只有右子树
5.根结点:我都有
1.1.2二叉树的2种存储结构
1.1.2.1树的顺序存储结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树
结构体如下
typedef ElemType SqBinTree[MaxSize];
优点:可以根据编号,快速找到祖先及孩子,也可以根据数组大小判断高度。
缺点:顺序存储,会将所有结点数据都储存,由于二叉树一般是不完美的,所以会对存储空间造成严重浪费,一般情况顺序存储结构只适用于完全二叉树。
1.1.2.2链式存储二叉树结构
既然顺序存储适用性不强,我们就要考虑链式存储结构。二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。
结构定义如下
//二叉树的链表结构
typedef char ElementType;/* 所有ElementType等同于char */
typedef TNode* Position;
typedef Position BinTree;/* 二叉树类型 */
struct TNode {
ElementType Data; /* 结点数据 */
BinTree Left; /* 指向左子树 */
BinTree Right; /* 指向右子树 */
};
1.1.3二叉树的遍历
总结二叉树的4种遍历方式,如何实现。
- 先序遍历
(1)访问根节点
(2)访问左子树
(3)访问右子树
void PreOrder(BTree bt)
{
if(bt == NULL) return; //递归出口
cout << bt->data; //根节点
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
- 中序遍历:
1)访问左子树
2)访问根节点
3)访问右子树
void InOrder(BTree bt)
{
if(bt == NULL) return;
InOrder(bt->lchild);
cout << bt->data; //根节点
InOrder(bt->rchild);
}
- 后序遍历:
1)访问左子树
2)访问右子树
3)访问根节点
void PostOrder(BTree bt)
{
if(bt == NULL) return;
PostOrder(bt->lchild);
PostOrder(bt->rchild);
cout << bt->data; //根节点
}
- 层次遍历
(1)队列
(2)先将T,入队列
(3)while循环,队列不空为约束条件
(4)若队头元素有 左(右)孩子,左(右)孩子入队列
(5)队头元素出队
初始化队列 que
que.push(T),先将 T入队
while( 队列不空 ) do
q = que.front()
出队,que.pop()
if q 有左孩子,左孩子入队
if q 有右孩子,右孩子入队
end while
1.1.4 线索二叉树
- 线索二叉树如何设计?
二叉链存储结构时,每个结点有两个指针域,总共有2n个指针域
有效指针域:n-1(根节点没指针指向)
空指针:n+1
(1)线索二叉树性质:
增加两个线索标记,ltag和rtag
若ltag=0,lchild域指向左孩子,若ltag=1,lchild域指向其前驱(线索);
若rtag=0,rchild域指向右孩子,若rtag=1,rchild域指向其后继(线索);
结构体定义:
typedef struct node
{
Elem Type data;
int ltag,rtag;
struct node *lchild;//左孩子或线索指针
struct node *rchild;//右孩子或线索指针
}TBTNode;
(2)中序线索二叉树特点?如何在中序线索二叉树查找前驱和后继?
特点:
中序线索二叉树可以找到对应树每个节点的前驱和后继节点
1.头结点左孩子指向根节点
2.右孩子为线索,指向最后一个孩子
3.遍历序列第一个结点前驱为头结点,最后一个结点后继为头结点
找中序遍历的第一个结点
左子树上处于“最左下”(没有左子树)的结点
找中序线索化链表中结点的后继
若无右子树,则为后继线索所指结点
否则为其右子树最左那个结点
中序遍历线索化代码
BiThrTree pre; /* 全局变量,始终指向刚刚访问过的结点 */
/* 中序遍历进行中序线索化 */
void InThreading(BiThrTree p)
{
if (p)
{
InThreading(p->lchild);/*递归左子树线索化*/
if (!p->lchild) /* 没有左孩子 */
{
p->ltag = 1; /* 前驱线索 */
p->lchild = pre; /* 左孩子指针指向前驱 */
}
if (!pre->rchild) /* 前驱没有右孩子 */
{
pre->rtag = 1; /* 后继线索 */
pre->rchild = p; /* 前驱右孩子指针指向后继 */
}
pre = p; /* 保持pre指向p的前驱 */
InThreading(p->rchild);/*递归右子树线索化*/
}
}
遍历线索二叉树
/* T指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的 */
/* 最后一个结点。中序遍历二叉线索链表表示的二叉树T */
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p = T->lchild; /* p指向根结点 */
while (p != T) /* 空树或遍历结束时,p==T */
{
while (p->LTag == 0)/* 当LTag==0时循环到中序序列第一个结点 */
p = p->lchild;
printf("%c", p->data);
while (p->RTag == 1 && p->rchild != T)
{
p = p->rchild;
printf("%c", p->data); /* 访问后继结点 */
}
p = p->rchild; /* p进至其右子树根 */
}
return OK;
}
1.1.5 二叉树的应用--表达式树
void InitExpTree(BTree &T,string str)
{
遍历字符串str{
如果当前字符串为数字{
T->data=当前数字
入栈Q1
}
如果当前字符串为运算符{
与Q2栈顶比较优先级
优先级低入栈Q2
优先级相同Q2出栈
优先级高 T->data=Q2栈顶元素
}
}
}
double EvaluateExTree(BTree T)
{
double x,y;
x=递归调用左子树的值
y=递归调用右子树的值
根据data的运算符分别运算并返回结果
}
1.2多叉树
1.2.1多叉树结构
- 双亲存储结构
结构体定义:
typedef struct
{
ElemType data; //结点的值
int parent; //指向双亲的位置
}PTree[MaxSize];
-
缺点:找父亲容易,找孩子不容易
-
孩子链存储结构
结构体定义:
typedef struct node
{
ElemType data; //结点的值
struct tnode *sons[MaxSons]; //指向孩子结点
}TSonNode;
-
缺点:空指针太多,找父亲不容易
-
孩子兄弟链存储
结构体定义:
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 个带权叶子结点构成的所有二叉树中,带权路径 WPL(叶子权值 × 路径长度) 最小的二叉树被称为哈夫曼树,或是最优二叉树
-
哈夫曼树的作用: 如哈夫曼编码,使用频率越高的字符采用越短的编码
1.3.2 哈夫曼树的结构体
教材是顺序存储结构,也可以自己搜索资料研究哈夫曼的链式结构设计方式。
- 顺序存储哈夫曼树:
typedef struct
{
int data; //结点值
double weight; //权值
int parent; //双亲结点
int lchild; //左孩子结点
int rchild; //右孩子结点
}HTNode;
- 链式存储哈夫曼树:
typedef struct HTNode {
char data;
double weight;
int deep;
struct HTNode* lnode;
struct HTNode* rnode;
struct HTNode* parent;
}HTNode, * HTree;
1.3.3 哈夫曼树构建及哈夫曼编码
- 哈夫曼构造
(1)
例如:
频率表 A:60, B:45, C:13 D:69 E:14 F:5 G:3
第一步:找出字符中最小的两个,小的在左边,大的在右边,组成二叉树。在频率表中删除此次找到的两个数,并加入此次最小两个数的频率和。
由频率表可知G,F最小,先删除FG,并返回两者的和8给频率表
重复以上动作....
每个 字符 的 二进制编码 为(从根节点 数到对应的叶子节点,路径上的值拼接起来就是叶子节点字母的应该的编码)
1.4并查集
1.4.1并查集定义
并查集,在一些有N个元素的集合)应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中
合并查找的集合,用于多个可以不相关的树的合并与查找。
对于大量数据根据某些特征进行合并,查找,不仅空间需要很大,时间效率也比较低
1.4.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;
}
假如有编号为1, 2, 3, ..., n的n个元素,我们用一个数组fa[]来存储每个元素的父节点(因为每个元素有且只有一个父节点,所以这是可行的)。一开始,我们先将它们的父节点设为自己。
- 查询
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.对树的认识及学习体会
-
1.遍历二叉树 是指以一定的次序访问二叉树中的每个结点。所谓 访问结点 是指对结点进行各种操作的简称。例如,查询结点数据域的内容,或输出它的值,或找出结点位置,或是执行对结点的其他操作。
-
2.对于一个二叉树的存储,首先想到的是能不能用一个数组顺序表存储,用数组下标映射每个节点的存储位置。事实是是可行的,但是存在着诸多问题。
对于完全二叉树是完全可行的,但是对于一般二叉树,就会出现空间浪费的问题。
树是一种重要的非线性数据结构,其定义是递归的,所以递归的调用是树中是很重要的 -
困难:操作多,对于伪代码到代码的转换不熟练,递归函数的运用不熟练只会生搬硬套。树的非递归算法掌握不熟练
2.PTA实验作业
2.1输出二叉树每层节点
2.1.1 解题思路及伪代码
- 解题思路
先先序遍历二叉树
层次遍历,输出每层结点
是否为第一层结点,引用node和lastnode分别用于存放遍历中途结点的孩子结点并判断是否找到这一层的最后一个结点
运用两个指针记住当前结点位置和每层的最后一个结点位置,以便于当当前结点等于层最后结点时,输出换行并输出下一层结点
而每层的结点都要通过队列依次存入并且依次输出。
- 伪代码
PrintfBTree(BTree bt)
{ 创建两个结点curnode,lastnode;//第一个结点,末尾结点
置行数level = 1;
if (树为空)
输出空,并返回;
q.push(bt)//入根
lastnode = bt
控制格式
当树不为空
{
curnode = q.front();//队头赋
if存在左孩子,入队
if存在右孩子,入队
if (curnode == lastnode)//一层遍历结束,修改
//赋队尾
控制队的长度至少大于1
出队
}
- 代码实现
#include<iostream>
#include<string>
#include<queue>
using namespace std;
struct node
{
char data;
struct node* lchild, * rchild;
};
typedef node* BTree;
int len;
BTree CreareBTree(string str, int& i);
void PrintfBTree(BTree bt);
int main()
{
int i = 0;
string str;
cin >> str;
BTree bt;
bt = CreareBTree(str, i);
PrintfBTree(bt);
return 0;
}
BTree CreareBTree(string str, int& i)//递归法建立二叉树
{
BTree bt;
len = str.size();
if (i > len - 1 || i < 0 || str[i] == '#')
return NULL;
bt = new node;
bt->data = str[i];//存入
bt->lchild = CreareBTree(str, ++i);//左
bt->rchild = CreareBTree(str, ++i);//右
return bt;
}
void PrintfBTree(BTree bt)
{
queue<BTree>q;
BTree curnode;
BTree lastnode;//第一个结点,末尾结点
int level = 1;
if (bt == NULL)
{
cout << "NULL";
return;
}
q.push(bt);//入根
lastnode = bt;
cout << level << ":";
while (!q.empty())
{
curnode = q.front();//队头赋
cout << curnode->data << ",";
if (curnode->lchild)
q.push(curnode->lchild);
if (curnode->rchild)
q.push(curnode->rchild);
if (curnode == lastnode)//一层遍历结束啦,修改
{
level++;
lastnode = q.back();//赋队尾
if (q.size() > 1)
cout << endl << level<<":";
q.pop();
}
else
{
q.pop();
}
}
}
2.1.2 总结解题所用的知识点
-
运用queue库函数,运用队列存放元素
-
先序遍历和层次遍历
-
运用两指针存放当前结点位置和每层的最后一个结点位置,以便于当当前结点等于层最后结点时,输出换行并输出下一层结点;而每层的结点都要通过队列依次存入并且依次输出
2.2 目录树
2.2.1 解题思路及伪代码
- 解题思路
* 分析
此题目文件树需要用左孩子右兄弟的二叉链表存储
* 建树
注意输出的顺序,即同层目录排在文件前,同类按字典顺序输出
* 插入作为孩子
没有第一个孩子,直接生成第一个孩子结点
有孩子,则需要判断是否需要更改孩子
结点存在,即数据相等且目录文件相等,则不需要新建孩子结点,当前指针更改为孩子位置
若孩子为文件内,新结点为目录则更改孩子为新结点
若孩子文件属性为同新结点,但是值比新结点大,则需更改当前指针
* 插入作为兄弟
没有兄弟,则插入新结点为兄弟
有兄弟,找新节点插入位置
若新结点属性和兄弟属性相等且值大于兄弟,则继续遍历下个兄弟
若新节点为文件,兄弟为目录,则继续遍历下个兄弟
遍历中发现兄弟是文件,新结点是目录,退出循环
遍历结束,兄弟结点值和新结点值先弄个等,则不需要插入新结点,更改当前指针为兄弟
插入新结点,更改新结点兄弟关系,并把插入位置的前一个结点的兄弟改成新结点
- 伪代码
void CreatTree(Tree& bt, string str, int i)//建树,
{
设置字符
if 该段字符串为目录,isfile改为false;
if (temp为文件)
InitFile(temp, bt);//插入文件
else //temp为目录
InitList(temp, bt);//插入目录
CreatTree(temp, str, i);
}
void InitList(Tree& temp, Tree& bt)//插入目录
{
定义结构体指针btr来遍历二叉树bt;
btr = bt->child;//btr先指向bt的孩子;
/*先对第一个兄弟结点进行判断*/
if (没有第一个孩子或者 btr为文件 或者 第一个孩子字典序大于该结点)//可插入
进行插入temp->brother = btr;bt->child = temp;//修改孩子指针
else if (二者相等)
直接使temp指向btr;
else //查找兄弟节点
while 为空
if (兄弟节点为文件 || 兄弟节点字典序大于该节点)
找到可插入位置,break;
else if (二者相等)
直接使temp指向btr->brother;break;
else
遍历下一兄弟结点;
end if
end while
if (btr->brother为空 || btr->brother->name != temp->name)
进行插入temp
end if
}
void InitFile(Tree& temp, Tree& bt)//对文件temp找一个可插入位置
{
结点btr先指向bt的孩子;
if (第一个孩子为空 || btr为文件 && 结点字典序大于等于该节点)
进行插入,修改bt的孩子指针;
else //判断兄弟结点
if (btr->brother为文件 并且 兄弟节点字典序大于该节点)
找到可插入位置,break;
else
遍历下一个兄弟结点
end if
end while
temp进行插入
end if
}
2.2.2 总结解题所用的知识点
3.阅读代码
3.1 题目及解题代码
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<List<Integer>>() ;
if(root == null) {
return res;
}
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
queue.push(root);
//true:从右往左, false:从左往右
boolean flag = false;
while(!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> list = new ArrayList<Integer>(levelSize);
while(levelSize-- > 0) {
TreeNode curNode = queue.poll();
list.add(curNode.val);
if(curNode.left != null) {
queue.offer(curNode.left);
}
if(curNode.right != null) {
queue.offer(curNode.right);
}
}
if(flag && list.size()>1) {
//需要翻转数组
Collections.reverse(list);
}
flag = !flag;
res.add(list);
}
return res;
}
3.2 该题的设计思路及伪代码
- 使用二叉树的层次遍历
3.3 分析该题目解题优势及难点
- 优势:层次遍历类似相关
- 难点:一层转换为另一层易出错