一.树
1. 定义:由n个有限个节点组成一个有层次关系的集合,看起来像一颗倒挂的树。
特点:
1.每个节点有0个或多个子节点。
2.没有父节点 的节点称为根节点。
3.每一个非根节点有且只有一个父节点
4.除了根节点外,每个子节点 可以分为多个不相交的子树
注:上图F有两个父节点,不能称为 树结构
二.树的术语
1.节点的度: 一个节点含有的子树个数
2.树的度: 树中所有节点的度的最大值
3.叶节点: 度为0的节点 (I,J,K)
4.子节点: 一个节点含有的子树的根节点,称为该节点的子节点(B是A的子节点;不能说D是A的子节点)
5.父节点: 诺一个节点有子节点,娜美这个节点就是其子节点的父节点
6.兄弟节点; 具有相同的父节点的节点互称为兄弟节点
7.堂兄弟节点: 在同一层的节点互称堂兄弟节点
8.祖先节点: 从根到该节点所经历路径上的所有节点(A,B,D都是I的祖先节点)
9.子孙节点: 以某节点为根的子根中所有的节点(所有的节点都是A的子孙节点)
10.节点层次: 根节点层次为1,其他节点层次是父节点的层次+1 (所有节点都是A的子孙节点)
11.树的深度: 树中所有节点的层次的最大值(深度为4)
12.森林: 多棵不相交的树的集合
三.二叉树
1.二叉树:每个节点最多含有两个子树的树称为二叉树
2.完全二叉树:除了最底层外,其他各层的节点均达到最大值,且最底层的节点应该从右往左紧密排列
3.满二叉树:所有叶节点都在最底层的完全二叉树
4.二叉搜索树:对于一个节点,它的左子树上的所有节点的值都比它小,右子树上的所有节点的值都比它大。
四.二叉树的存储结构
1.顺序存储:从上往下,从左往右的将树存储到顺序表中
2.优点:遍历方便,可以用索引来表示节点的关系
3.缺点:可能会队存储空间造成极大的浪费
4.顺序存储适用于存完全二叉树
遍历结果为:5,4,C,None,None,G,H,None,None,None,None,M,N,F,O
5.链式存储:每个检点具有左右指针域和数据域,此来连接。
优点:不会浪费空间
遍历结果为:5,4,C,G,H,M,N,F,O
五.二叉树的遍历
前序遍历
顺序:(根节点->左子树->右子树)
struct TreeNode {
char data; // 数据域,存储节点的数据(假设为字符类型)
TreeNode *lchild; // 左子树指针,指向左孩子节点
TreeNode *rchild; // 右子树指针,指向右孩子节点
};
void PreOrderTraverse(TreeNode *t, int level) {
if (t == nullptr) {
return; // 如果当前节点为空,直接返回,结束当前函数的执行
}
// 输出当前节点的数据和所在的层次(level)
cout << "data = " << t->data << " level = " << level << endl;
// 递归遍历左子树,level + 1 表示下一层次
PreOrderTraverse(t->lchild, level + 1);
// 递归遍历右子树,level + 1 表示下一层次
PreOrderTraverse(t->rchild, level + 1);
}
前序遍历结果:G D A F E M H Z
注:采用递归方法。递归函数原理:
递归调用栈:每次调用都会在栈上创建一个栈帧,用于存储函数的局部变量,参数以及返回地址。
当函数调用自身处理时,当前函数的执行状态被压入栈中,处理完并return后,栈顶被弹出,控制流程回到原来的调用点。
前序遍历递归调用顺序:先处理左树(最先处理根节点),然后处理右树。
整个递归函数能从左子树,右子树的处理回溯到根节点,实现了完整的前序遍历过程。
中序遍历
中序遍历顺序:(左子树->根节点->右子树)
struct TreeNode {
char data; // 数据域,存储节点的数据(假设为字符类型)
TreeNode *lchild; // 左子树指针,指向左孩子节点
TreeNode *rchild; // 右子树指针,指向右孩子节点
};
void PreOrderTraverse(TreeNode *t, int level) {
if (t == nullptr) {
return; // 如果当前节点为空,直接返回,结束当前函数的执行
}
// 递归遍历左子树,level + 1 表示下一层次
PreOrderTraverse(t->lchild, level + 1);
// 输出当前节点的数据和所在的层次(level)
cout << "data = " << t->data << " level = " << level << endl;
// 递归遍历右子树,level + 1 表示下一层次
PreOrderTraverse(t->rchild, level + 1);
}
中序遍历结果:A D E F G H M Z
后序遍历
struct TreeNode {
char data; // 数据域,存储节点的数据(假设为字符类型)
TreeNode *lchild; // 左子树指针,指向左孩子节点
TreeNode *rchild; // 右子树指针,指向右孩子节点
};
void PreOrderTraverse(TreeNode *t, int level) {
if (t == nullptr) {
return; // 如果当前节点为空,直接返回,结束当前函数的执行
}
// 递归遍历左子树,level + 1 表示下一层次
PreOrderTraverse(t->lchild, level + 1);
// 递归遍历右子树,level + 1 表示下一层次
PreOrderTraverse(t->rchild, level + 1);
// 输出当前节点的数据和所在的层次(level)
cout << "data = " << t->data << " level = " << level << endl;
}
后序遍历结果:A E F D H Z M G
六.哈夫曼树与哈夫曼编码
哈夫曼树基本概念
路径:从树中一个节点到另一个节点之间的分支构成这两个节点间的路径。
节点的路径长度:两节点路径上的分支数。
a图:从A到B,C,D,E,F,G,H,I的路径长度分别为:1,1,2,2,3,3,4,4
树的路径长度TL(a)=0+1+1+2+2+3+3+4+4=20
b图:从A到B,C,D,E,F,G,H,I的路径长度分别为:1,1,2,2,2,2,3,3
树的路径长度TL(b)=0+1+1+2+2+2+2+3+3=16
图c:带权路径长度
c. WPL=7*2+5*2+2*2+4*2=36
图d:带权路径长度
d. WPL=7*3+5*3+2*1+4*2=46
哈夫曼树:最优树,带权路径长度(WPL)最短的树
哈夫曼树构造
主要方法:选两棵根节点的权值最小的树作为左右树构造一棵新的二叉树,且设置新的二叉树的根节点的权值为其左右子树上根节点的权值之和,并删除两棵树,提示将得到的二叉树存入森林中(不断重复,直到成为一棵树)
哈夫曼算法中,初始有n棵二叉树,结果n-1此合并
n-1此合并产生n-1给新节点
哈夫曼树中共有n+n-1=2n-1个节点
哈夫曼编码的实现
输入的字符集:D={C,A,S,T,;}
字符出现频率 w={2,4,2,3,3}
T:00 ;:01 A:10 C:110 S:111
电文是{CAS;CAT;SAT;AT}
编码:11010111011101000011111000011111000011000
前缀编码:在一组编码中,任一编码都不是其他任何编码的前缀,前缀码抱枕了在解码时不会有多种可能
七.并查集
并查集的定义:
一种树形的数据结构,用于处理右斜不交集的合并及查询问题,不交际指的是一系列没有重复元素的集合,并查集住哟啊持两种操作:
合并:将两个集合合并成一个集合
查找:确定某个元素属于哪个集合,通常是返回集合内的一个
路径压缩
在集合很大或者树哼不平衡时,使用快速合并思路实现并查集的大妈效果横插,最坏情况下,树会退化成一条链,单次查询时间复杂度高达O(n).并查集最坏情况:
路径压缩分为
1.隔代压缩
2.完全压缩
1.隔代压缩
在查询时,两步一压缩,一直循环执行(把后面节点指向它的父亲节点的父亲节点)这样操作,从而减小树的深度。
2.完全压缩
完全压缩:在查询时,把查询的节点到根节点的路径上的所有父亲节点设为根节点,从而减少树的深度。
按秩合并
因为路径压缩只在查询时进行,并且只压缩一棵树上的路程,所以并查集最终的结构仍然可能是比较复杂的,为了避免这种情况,另一个优化方式是 按秩合并。
秩有两种定义
1.指树的深度
2.树的大小(集合节点个数)
按深度合并
每次合并操作时,都把深度较小的树根节点指向深度较大的树根节点
按大小合并
节点少的往节点多的合并