408个人复习记录,数据结构强化④--树与二叉树

8.17日开始(预计两天之内)

第一天先捋清楚本章主要内容,记忆知识点

第二天解决题

第五章  树与二叉树

5.1 树的定义

树是一种递归的数据结构,树也是一种逻辑结构(分层结构)

特征:树中所有结点有0或多个后继,树除了根节点外只有一个前驱

        n个结点的树中只有n-1条边

基本术语:
       树中一个结点的孩子个数称为该结点的度, 树中结点的最大度数称为树的度。
        一棵有n个结点的树的所有结点的度数之和为n-1
        树中结点总数为n,n=分支数+1;
                分支数=树中所有结点的度之和
                假设树中度为i的结点个数分别为ni;则分支数为i*ni
n = 0*n0+1*n1+2*n2+3*n3+4*n4   +1
其中n0表示叶结点个数

5.2 二叉树

5.2.1 定义与性质

一、

二叉树中结点度不超过2

二叉树的子树有左右之分,不能交换次序。

二叉树与度为 2 的有序树的区别
①度为 2 的树至少有 3 个结点 而二叉树可以为空
②度为 2 的有序树的孩子的左右次序是相对于另一孩子而言的 若某个结点只有一个孩子,
        则这个孩子就无须区分其左右次序, 而二叉树无论其孩子数是否为 2, 均需确定其左右次序,
        即二叉树的结点次序不是相对于另一结点而言的, 而是确定的
由二叉树的先序序列和中序序列可以唯一地确定一棵二叉树。
二、特殊二叉树
1.满二叉树
        高度为h且节点数为2^h  -1的二叉树;
对于编号为i的结点若有双亲,则其双亲为2i(向下取整)
        若有左孩子, 则左孩子为 2i      若有右孩子 则右孩子为 2i+1
 2.完全二叉树
   当且仅当其每个结点都与高度为介的满二叉树中编号为1〜n 的结点一一对应时 称为完全二叉树
 完全二叉树的性质:
(1)if n<=n/2(向下取整) 则结点i为分支节点,否则为叶结点
(2)叶结点只可能在层次最大的两层上出现,在最下层中叶结点依次排列在最左边
(3)若有度为1的结点,则只能有1个,且该节点只有左孩子没有右孩子
(4)若n为奇数则每个分知结点都有左右孩子;
        若n为偶数,则编号最大的分支结点(n/2)只有左孩子没有右孩子
3.二叉排序树
        左子树上所有结点的关键字均小于根结点的关键字;
        右子树上的所有结点的关键字均大于根结点的关键字;
        左子树和右子树又各是一棵二叉排序树
4.平衡二叉树
树上任意一个结点的左子树和右子树的深度之差不超过 1

 三、二叉树的性质

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.顺序存储结构

二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素        将完全二叉树上编号为的结点元素存储在一维数组下标为的分量中。
完全二叉树和满二叉树采用顺序存储比较合适。
缺点:顺序存储的空间利用率较低
注:
这种存储结构建议从数组下标 1 开始存储树中的结点 若从数组下标 0 开始存储 则 不满足性质4 的描述 比如结点/存储在 0 下标位置上时 无法根据性质 4 来计算出其孩子结点 在数组中的位置)

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)。

在递归遍历 中, 递归工作栈的栈深恰好为树的深度 所以在最坏情况下 二叉树是有n个结点且深度为n的 单支树, 遍历算法的空间复杂度为O( n)

 

沿虚线游走
可以分别得到先序序列 ABDEC 中序序列 DBEAC 后序序列 DEBC.A

4.非递归算法

       使用非递归算法的话要使用栈的思路,用中序遍历举例:

①沿着根的左孩子依次入栈直到左孩子为空,这说明找到了能输出的点,此时栈内元素为ABD

②栈顶元素出栈并访问,若其右孩子为空,则继续执行;

                                        若其右孩子不为空,将右子树执行①

栈顶D 出栈并访问 它是中序序列的第一个结点
D 右孩子为空 栈顶 B 出栈并访问;B 右孩子不空 将其右孩子 E 入栈 E左 孩子为空, 栈顶E 出栈并访问 E 右孩子为空 , 栈顶A 出栈并访问;A 右孩子不空 将其右孩子 C 入栈 C 左孩子为空 栈顶 C 出栈并访问 由 此得到中序序列DBEAC

先序遍历:

①沿着根的左孩子依次访问在入栈 直到左孩子为空,这说明找到了最左下元素,此时栈内元素为ABD

②栈顶元素出栈,若其右孩子为空,则继续执行;

                                        若其右孩子不为空,将右子树执行①

后序遍历:

①从根结点开始将其入栈然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点。

如果其有右子树还需按相同的规则对其右子树进行处理;

直至上述操作进行不下去,若栈顶元素想要出栈被访问要么右子树 为空,要么右子树刚被访问完(此时左子树早已访问完)这样就保证了正确的访问顺序。        

5.层次遍历

 要进行层次遍历需要借助一个队列首先将二叉树根结点入队,然后出队’访问出队结点

'若它有左子树'则将左子树根结点入队;

若它有右子树,则将右子树根结点入队完成入队后出队访问出队结点…

        如此反复直至队列为空

6.由遍历序列构造二叉树

(1)前序+中序确定一个二叉树

(2)后序+中序确定一个二叉树

(3)层序+中序确定一个二叉树

5.3.2 线索二叉树

一、 线索二叉树基本概念

遍历二叉树是以一定的规则将二叉树中的结点排列成一个线性序列 从而得到几种遍历序列, 使得该序列中的每个结点 第一个和最后一个结点除外 都有一个直接前驱和直接后继.
二叉链表存储仅能体现一种父子关系 ,不能直接得到结点在遍历中的前驱或后继。且在含 n 个结点的二叉树中 有n  + 1 个空指针.
        线索二叉树是为了加快查找结点前驱和后继的速度。对 空指针加以利用。
线索二叉树的存储结构
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.双亲表示法

    在每个结点中增设一个伪指针,指示 其双亲结点在数组中的位置

根结点下标为 0, 其伪指针域为 -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的结点构造过程:

        (将权值最小的两结点放在最底层,两个结点之和作为它们的父结点,父节点与权值仅大于它们的结点求和,向上逐层递增)

        

哈夫曼树的性质:
        1.哈夫曼树不唯一
        2.权值越小的结点路径长度越大
        3.哈夫曼树的结点总数为2n - 1
        4.哈夫曼树构造前,结点总数为n
        5.构造哈夫曼树需要执行n - 1次合并,每次合并新增一个结点,即新增n - 1个结点
                n + n - 1 = 2n - 1
        6.哈夫曼树不存在度为1的点(要么为0(叶子结点),要么为2(分支节点))

 
 三、哈夫曼编码

左为0,右为1

该哈夫曼树的 WPL 可视为最终编码得到二进制编码的长度,WPL是几就代表二进制编码的长度是多少位
可变长度编码
其特点是对频率高的字符赋以短编码, 而对频率较低的字符则赋以较 长一些的编码, 从而可以使字符的平均编码长度减短 起到压缩数据的效果
        哈夫曼编码是一种 被广泛应用而且非常有效的数据压缩编码

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;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>