8.17日开始(预计两天之内)
第一天先捋清楚本章主要内容,记忆知识点
第二天解决题
第五章 树与二叉树
5.1 树的定义
树是一种递归的数据结构,树也是一种逻辑结构(分层结构)
特征:树中所有结点有0或多个后继,树除了根节点外只有一个前驱
n个结点的树中只有n-1条边
5.2 二叉树
5.2.1 定义与性质
一、
二叉树中结点度不超过2
二叉树的子树有左右之分,不能交换次序。
三、二叉树的性质
1.非空二叉树上的叶结点数等于度为2的结点数加1;即即n0 = n2 + 1。
拓展到任意一棵树,若 结点数量为n,则边的数量为n-1
2.非空二叉树中第k层至多有2^(k-1)个结点
3.高度为h的二叉树至多有2^h -1个结点
4.对完全二叉树按从上到下从左到右的顺序依次编号,会有以下关系
(1)当i>1时结点i的双亲编号为2i(向下取整)
(2)当2i<=n时结点i的左孩子编号为2i,否则没有左孩子
(3)当2i+1<=n时结点i的右孩子编号为2i+1,否则没有右孩子
(4)结点i所在层次(深度)为log2(i)(向下取整)+1
5.具有n个结点的完全二叉树高度为log2(n+1)(向上取整)
5.2.2 二叉树的存储结构
1.顺序存储结构
2.链式存储结构
结点结构包括数据域,左指针域lchild,右指针域rchild
二叉树的顺序存储结构
typedef struct{
elemtype data[maxsize];
int length;
}Tree;
二叉树的链式存储结构
typedef struct BiTNode{
elemtype data; //数据域
struct BiTNode *lchild, *rchild;//左右孩子指针
}BiTNode,*BiTree;
在含有n个结点的二叉链表中,含有n + 1个空链域(空链域可以用来组成线索链表)
课后题:(难以解决)
6.设二叉树由2n个结点,且m<n,则不可能存在的是()的结点
A.n个度为0;B. 2m个度为0;C.2m个度为1;D.2m个度为2
解析:
14.
第八层120个叶结点,第七层4个叶结点,
这4个叶结点中的最左边可以有1个 左孩子不影响叶节点总数
一共八层2^8 -1 -8=256-9=247+1=248
二.4.(2)(3)(4)
5.3 二叉树的遍历和线索二叉树
5.3.1 二叉树的遍历
二叉树的遍历是指: 按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。常见的遍历次序有先序(NLR)、中序(LNR) 和后序(LRN)三种遍历算法
1.先序遍历
typedef
2.中序遍历
3.后序遍历
三种算法每个结点仅访问一次,所以时间复杂度为O(n)。
4.非递归算法
使用非递归算法的话要使用栈的思路,用中序遍历举例:
①沿着根的左孩子依次入栈直到左孩子为空,这说明找到了能输出的点,此时栈内元素为ABD
②栈顶元素出栈并访问,若其右孩子为空,则继续执行;
若其右孩子不为空,将右子树执行①
先序遍历:
①沿着根的左孩子依次访问在入栈 直到左孩子为空,这说明找到了最左下元素,此时栈内元素为ABD
②栈顶元素出栈,若其右孩子为空,则继续执行;
若其右孩子不为空,将右子树执行①
后序遍历:
①从根结点开始,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点。
②如果其有右子树,还需按相同的规则对其右子树进行处理;
直至上述操作进行不下去,若栈顶元素想要出栈被访问,要么右子树 为空,要么右子树刚被访问完(此时左子树早已访问完),这样就保证了正确的访问顺序。
5.层次遍历
要进行层次遍历,需要借助一个队列。首先将二叉树根结点入队,然后出队’访问出队结点
'若它有左子树'则将左子树根结点入队;
若它有右子树,则将右子树根结点入队。完成入队后出队,访问出队结点……
如此反复,直至队列为空。
6.由遍历序列构造二叉树
(1)前序+中序确定一个二叉树
(2)后序+中序确定一个二叉树
(3)层序+中序确定一个二叉树
5.3.2 线索二叉树
一、 线索二叉树基本概念
线索二叉树的存储结构
typedef struct ThreadNode{
Elemtype data; //数据域
struct ThreadNode *lchild,*rchild;//左右孩子指针
int ltag,rtag; //左右线索标志
}
2.先序中序和后序线索二叉树的构造
(1)中序线索二叉树
3.中序线索二叉树的遍历
中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。
在对其进行遍历时,只要先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空。
在中序线索二叉树中找结点后继的规律是:
若其右标志为"1”,则右链为线索,指示其后继,否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继。
王道课后题
19.
#include <stdio.h>
#include <stdlib.h>
// 定义二叉树结点的结构体
typedef struct TreeNode {
int weight;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
// 创建新的二叉树结点
TreeNode* createNode(int weight) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
newNode->weight = weight;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
// 递归计算二叉树的WPL
int calculateWPL(TreeNode* node) {
// 如果当前节点是叶节点,则直接返回节点的权值
if (node->left == NULL && node->right == NULL) {
return node->weight;
}
// 递归计算左子树和右子树的WPL
int leftWPL = node->left != NULL ? calculateWPL(node->left) : 0;
int rightWPL = node->right != NULL ? calculateWPL(node->right) : 0;
// 返回当前节点的WPL,即当前节点的权值加上左右子树的WPL之和
return node->weight + leftWPL + rightWPL;
}
int main() {
// 创建二叉树
TreeNode* root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
// 调用算法计算二叉树的WPL
int wpl = calculateWPL(root);
printf("该二叉树的WPL: %d\n", wpl);
return 0;
}
20.
//20.请设计一个算法,将给定的表达式树(二叉树)转换为等价的中缀表达式
//(通过括号反映操作符的计算次序)并输出。
//例如,当下列两棵表达式树作为算法的输入时
#include<stdio.h>
# include<stdlib.h>
typedef struct node{
char data;
struct node *left,*right;
}BTree;
node* createnode(char data){
node * newnode = (node *)malloc(sizeof(node));
newnode->data = data;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
//中序遍历//同时要给出括号
//如果当前节点没有子节点,说明当前节点为叶子结点,
//否则为加( 继续递归,在递归调用回到这层时加一个)
void mid(node *root){
if(root == NULL){
return;
}
//不是叶子节点的话
if(root->left != NULL || root->right!=NULL){
printf("(");
//加左括号,继续对子树做中序遍历
mid(root->left);
printf("%c",root->data);
mid(root->right);
printf(")");
//递归回到这层时补上对应的右括号
}
else{//是叶子节点
printf("%c", root->data);
}
}
int main(){
char data[20] = "a+b*c*-d";
//可以替换成data[i];但是懒得改了!
node *root = createnode('*');
root->left = createnode('+');
root->right = createnode('*');
root->left->left = createnode('a');
root->left->right = createnode('b');
root->right->left = createnode('c');
root->right->right = createnode('-');
root->right->right->right = createnode('d');
mid(root);
}
5.4 树、森林
5.4.1 树的存储结构
树的存储要求能唯一的反映树中个结点之间的逻辑关系
1.双亲表示法
在每个结点中增设一个伪指针,指示 其双亲结点在数组中的位置
parent指向的是其父节点的位置。
双亲表示法的存储结构
typedef struct{ //树的结点定义
Elemtype data; //数据元素
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTRNode nodes[maxsize];//双亲表示
int n;//结点个数
}PTree;
该存储结构可以很快的找到每个结点的双亲结点,但要求孩子结点时要遍历整个结构
2.孩子表示法
将每个结点的孩子结点用单链表连接起来,形成一个线性结构,此时n个结点就有n个孩子链表,叶结点的孩子链表为空表。
找孩子结点容易,找双亲结点要遍历整个链表。
3.孩子兄弟表示法(二叉树表示法)
以二叉链表作为树的存储结构,每个结点包括三个部分:
结点值,指向结点第一个孩子结点的之后的指针,指向结点下一个兄弟结点的指针
typedef struct CSNode{
elemtype data; //数据域
struct CSNode *firstchild, *rbro;//第一个孩子和右兄弟指针
}CSNode, *CSTree;
优点:灵活,可以方便的实现树转换为二叉树的操作
缺点:从当前结点查找其双亲结点比较麻烦,可以为每个结点增设一个parent指针指向其父节点。
5.4.2树、森林与二叉树的转换
孩子兄弟表示法的本质就是森林和树的相互转换:
森林和树的转化中,左指针指向的是孩子,右指针指向的是树
5.4.3森林的遍历
1.森林的先序遍历
依次对每个树进行先序遍历
(B (E KL) F) (CG) (D (HM) I J)
2.森林的中序遍历
依次对每个树进行后根遍历
(KLEBF)(GC)(MHIJD)
5.5树和森林的应用
5.5.1哈夫曼树和哈夫曼编码
一、哈夫曼树的定义
从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积称为该结点的带权路径长度
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树
(a)WPL = (7+5)*2+(2+4)*2 = 36
(b)WPL = (7+5)*3+4*2+2=46
(c)WPL = (2+4)*3+5*2+7*1=35
c的WPL最小,所以c为哈夫曼树
二、哈夫曼树的构造
给定n个权值分别为W1,W2,W3……Wn的结点构造过程:
(将权值最小的两结点放在最底层,两个结点之和作为它们的父结点,父节点与权值仅大于它们的结点求和,向上逐层递增)
2.权值越小的结点路径长度越大
3.哈夫曼树的结点总数为2n - 1
4.哈夫曼树构造前,结点总数为n
5.构造哈夫曼树需要执行n - 1次合并,每次合并新增一个结点,即新增n - 1个结点
6.哈夫曼树不存在度为1的点(要么为0(叶子结点),要么为2(分支节点))
三、哈夫曼编码
左为0,右为1
5.5.2并查集
一、定义:
并查集:将元素划分为若干个互不相干的子集
并:将两个集合归并为一个集合(让一个树成为另一个树的子树)
查:确定一个指定元素的集合(查看其根节点是哪一个)
采用双亲表示法:并和查仅需更改或查看指向其双亲结点的伪指针
1.初始化:将每个结点的值设置为-1,表示每个结点都是一颗单独的树,即n个子集
2.查(最坏O(n)):找该结点的根节点
并(O(1)):①两个结点是根节点:直接修改root2的值为root1的下标
②两个结点是非根节点:通过查操作,分别找到root1和root2的根节点,然后将root2的根节点的值为root1的根节点的下标
#define MAXSIZE 100
int UFsets[MAXSIZE]; //集合数组
//初始化并查集
void Initial(int S[]){
//-1表示集合中(森林)每个元素都是独立的个体(树)
for (int i = 0; i < MAXSIZE; i++) S[i] = -1;
}
//查找,传入数组和数组下标
int Find(int S[], int x){
//循环遍历查找其根节点,根节点的值为-1
while(S[x] >= 0) x = S[x];
return x;
}
//并,两个集合合并为一个
void Union(int S[], int root1, int root2){
//两个元素是同一个集合
if (root1 == root2) return;
//将root2的根节点改为root1
S[root2] = root1;
}
二、并查集的合并(Union)操作优化(小树合并到大树)
1.如果每次都是大树合并成小树,则树的高度每次都会+1,导致并查集的使用效率降低,查的时间复杂度为O(n)
2.每次都让小树合并到大树,可以延缓树的增高,从而使查的最坏时间复杂度为O(logn)
优点:根节点的数据的绝对值等于其结点总数,结点总数更大的树为大树,将更小的树并入大树
//并优化
void Union(int S[], int root1, int root2){
//root1的树结点更多,相较下root2为大树
if (root1 < root2){
S[root1] += S[root2]; //root1的结点数更新
S[root2] = root1; //root2并入root1
}
else{ //root2为大树
S[root2] += S[root1]; //root2的结点数更新
S[root1] = root2; //root1并入root2
}
}
根节点代表的是该树的总结点个数,因此合并时,可以通过两个树根节点的值相加的方式得到合并后的树的总结点个数
三、查(Find)操作的优化(压缩路径)
第一轮循环:和之前的find一样,找到该节点的根节点,
第二轮:将该结点的find操作经历的每个结点挂到根节点上(修改路径上每个结点的值为根节点的下标)
find操作可以进一步优化为O(a(n))<<O(logn),a(n)通常为常数级
int Find(int S[], int x){
int root = x;
//向上循环遍历树,找到其根节点
while(S[root] >= 0) root = S[root];
while (x != root){
int temp = S[x];
S[x] = root; //将x的双亲结点改为root
x = temp;
}
return root;
}