树与二叉树
一、基本概念
树:n(n>=0)个结点的有限集合,n = 0时为空树,树的特点:
有且只有一个根节点
当n>1时,其余结点可分为m(m>0)个互不相交的有限集合,每个集合又是一个树,称为子树
二、基本术语
(一)结点之间的关系描述:
(二)结点、树的属性描述:
(三)有序树、无序树:
(四)森林:
三、树的常考性质
考点一:结点数 = 总度数+1,结点的度——结点有几个孩子
考点二:度为m的数与m叉树的区别
考点三:度为 m 的树第 i 层至多有 mi+1 个结点(i>=1), m 叉树第 i 层至多有 mi+1 个结点(i>=1)
考点四:高度为h的m叉树至多有(mh-1)/(m-1)个结点
考点五:高度为 h 的 m 叉树至少有 h 个结点;
高度为 h ,度为 m 的树至少有 h+m-1 个结点
考点六:具有 n 个结点的 m 叉树的最小高度为 logm(n(m-1)+1)
四、二叉树
(一)基本概念:
n(n>=0)个结点的有限集合
可以为空二叉树,即n=0
有一个根节点和两个互不相交的左右子树组成,左右子树又分别是一颗二叉树
(二)几种特殊的二叉树:
1.满二叉树:
只有最后一层有叶子节点且不存在度为1的结点
若按层序编号,结点 i 的左孩子为 2i ,右孩子为 2i+1 ,父节点为 i/2
2.完全二叉树:
必须要求所有的层序编号和满二叉树一一对应
只有最后两层有叶子节点;最多只有一个度为1的结点
若按层序编号,结点 i 的左孩子为 2i ,右孩子为 2i+1 ,父节点为 i/2
i<=(n/2)为分支结点,i>(n/2)为叶子结点
不是完全二叉树的情况:
3.二叉排列树:
4.平衡二叉树:
五、二叉树的常考性质
常考点一:
设非空二叉树度为0、1、2的结点个数为n0、n1、n2,则n0 = n2 + 1(叶子节点比二分支节点多一个)(树的结点数 = 总度数 + 1):
假设树中结点总数为n,则:
A、n = n0 + n1 + n2
用B - A得 n0 = n2 + 1
B、n = n1 + 2n2 + 1
常考点二:
二叉树的第i层至多有2i-1个结点(i>=1)
m叉树的第i层至多有mi-1个结点(i>=1)
常考点三:
高度为h的二叉树至多有2h-1个结点(满二叉树)
高度为h的m叉树至多有(mh-1)/(m-1)个结点
六、完全二叉树的常考性质
常考考点一:
具有n个结点的完全二叉树的高度为【log2(n+1)】或【(log2n)+1】
对【log2(n+1)】的推导:
高为h的满二叉树共有2h-1个结点
则2h-1-1 < n <= 2h-1,然后给此不等式分别加1后取对数,得:h-1< log2(n+1) <= h
高为h-1的满二叉树共有2h-1-1个结点
对【(log2n)+1】的推导:
高为h-1的满二叉树共有2h-1-1个结点
则2h-1 <= n < 2h,同时取对数得:h-1 <= (log2n) < h
高为h的完全二叉树至少有2h-1个结点、至多有2h-1个结点
常考考点二:
对于完全二叉树,可以由结点总数n推出度为0、1、2得结点个数是n0、n1、n2
完全二叉树最多只有一个度为1得结点,即:
n1 = 0或1
n0 = n2 + 1 可得:n0+n1一定是奇数,则有:
若完全二叉树有2k(偶数)个结点,则必有n1 = 1,n0 = k,n2 = k+1
若完全二叉树有2k-1(奇数)个结点,则必有n1 = 0,n0 = k,n2 = k-1
七、二叉树和完全二叉树得比较
八、二叉树的存储结构
(一)顺序存储(只适合完全二叉树)【很少用】:
使用静态数组,一层一层依次存储:
若一般二叉树判断是否有左右孩子,可以判断isEmpty是否为true来确定
结论:
(二)链式存储【常用】:
若没有左(右)孩子,则把对应指针设为NULL即可
#include <stdio.h>
#include <stdlib.h>
//二叉树的顺序存储:使用静态数组
#define MaxSize 100
struct TreeNode{
int value;//结点的数据元素
bool isEmpty;//结点是否为空
};
//二叉树的链式存储:使用链表
typedef struct BiTNode{
int data;//数据域
struct BiTNode* lchild;//左孩子
struct BiTNode* rchild;//右孩子
}BiTNode,*BiTree;
int main(){
//定义一个空树
BiTree root = NULL;
//插入根结点
root = (BiTree)malloc(sizeof(BiTree));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//插入新节点
BiTNode* p = (BiTNode*)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;//p成为根节点root的左孩子
return 0;
}
以上代码找指定结点的左(右)孩子很简单,只需判断左(右)指针是否为NULL,但是找指定结点的父节点却需要从根结点开始依次遍历,解决方法如下:
typedef struct FBiTNode{
int data;
struct FBiTNode* lchild;
struct FBiTNode* rchild;
struct FBiTNode* patent;//父节点指针
}FBiTNode,*FBiTree;
注意:n个结点的二叉链表共有n-1个空链域
九、二叉树的遍历
遍历:按照某种次序把所有结点 都访问一遍
二叉树的递归特性:要么是空二叉树,要么是由根节点+左子树+右子树组成的二叉树
(一)先序遍历【根 左 右】(NLR) :
(二)中序遍历【左 根 右】(LNR):
(三)后序遍历【左 右 根】(LRN):
(四)应用举例:
1.手算练习:
1.1.写出满二叉树的所有遍历:
1.2.写出一般二叉树的所有遍历:
1.3.算数表达式中的应用:
2.机算练习:
//定义一个二叉树
typedef struct BiTNode{
int data;
struct BiTNode* lchild;
struct BiTNode* rchild;
}BiTNode,*BiTree;
2.1.先序遍历:
若为空二叉树,什么都不做
若为非空二叉树,则先访问根节点,后访问左结点,最后访问右结点
void PreOrder(BiTree T){
if(T != NULL){
visit(T);//访问根结点
PreOrder(T->lchild);//访问左子树
PreOrder(T->rchild);//访问右子树
}
}
2.2.中序遍历:
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild);//访问左子树
visit(T);//访问根结点
InOrder(T->rchild);//访问右子树
}
}
2.3.后序遍历:
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild);//访问左子树
PostOrder(T->rchild);//访问右子树
visit(T);//访问根结点
}
}
十、二叉树的层序遍历
层序遍历:一层一层依次遍历
A先入队,没有兄弟结点,则A出队;然后A的左右孩子BC入队;然后B出队;然后B的左右孩子DE入队,然后然后C出队,然后C的左右孩子FG入队;然后D出队,然后D的左右孩子HI入队…
#include <stdio.h>
#include <stdlib.h>
//定义一个二叉树
typedef struct BiTNode{
int data;
struct BiTNode* lchild;
struct BiTNode* rchild;
}BiTNode,*BiTree;
//定义一个队列
typedef struct LinkNode{
BiTBode* data;
struct LinkNode* next;
}LinkNode;
typedef struct{
LinkNode* front;
LinkNode* rear;
}LinkQueue;
//初始化队列
void InitQueue(LinkQueue &Q){
Q.front = Q.rear= (LinkNode*)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
//入队
void EnQueue(LinkQueue &Q,int x){
LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s;
Q.rear = s;
}
//出队
bool DeQueue(LinkQueue &Q,int &x){
if(Q.front == Q.rear){
return false;
}
LinkNode* p = Q.front->next;
x = p->data;
Q.front->next = p->next;
if(Q.rear == p){
Q.rear = Q.front;
}
free(p);
return true;
}
//判空
bool isEmpty(LinkQueue Q){
if(Q.front == NULL){
return true;
}else{
return false;
}
}
//层序遍历
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue(Q);
BiTree p;
EnQueue(Q,T);
while(!isEmpty(Q)){
DeQueue(Q,p);
vist(p);
if(p->lchild != NULL){
EnQueue(Q,p->lchild);
}
if(p->rchild != NULL){
EnQueue(Q,p->rchild);
}
}
}
int main(){
return 0;
}
算法思想:
1)初始化一个辅助队列
2)根节点入队
3)若非空,则队头结点访问该节点,并将其左右孩子入队
4)重复3)至队列为空
十一、由遍历序列构造二叉树
(一)不同二叉树的遍历序列:
(二)结论:
给定二叉树,它的遍历序列是唯一的,但给定遍历序列,出现的二叉树并不唯一
即:若只给出一颗二叉树的前/中/后/层序遍历中的一种,并不能唯一确定一颗二叉树
那么,如何通过遍历确定一颗二叉树呢:可以通过前序+中序/后序+中序/层序+中序
(三)通过层序确定二叉树:
1.前序+中序:
已知:
前序遍历:A D B C E
中序遍历:B D C A E
根据中序,只能知道所有结点都有可能是根节点,但根据前序,确定出A为根结点,则根据中序的左-根-右可知:B D C 为左子树,E为右子树,即:
用以上方法判断B D C:可知D为根结点(现已判断出A,故可除去A),则根据中序遍历,B为左子树,即:
进一步练习:
2.后序+中序:
后序序列最后一个出现的是根节点:
3.层序+中序:
层序序列第一个被访问的是根节点,接下来是左子树,最后是右子树:
(四)总结:
十二、线索二叉树
(一)作用(以中序为例):
方便找出某一结点的前驱和后继:
(二)存储结构:
(三)三种线索二叉树:
1.先序线索二叉树:
2.后序线索二叉树:
3.对比:
(四)二叉树的线索化:
1.中序线索化:
#include <stdio.h>
//定义中序线索二叉树
typedef struct ThreadNode{
int data;
struct ThreadNode* lchild;
struct ThreadNode* rchild;
int ltag,rtag;//左右线索标志
}ThreadNode,*ThreadTree;
//建立全局变量 指向当前访问结点的前驱
ThreadNode* pre = NULL;
//建立线索
void visit(ThreadNode* q){
if(q->lchild == NULL){//左孩子为空
q->lchild = pre;//建立前驱结点的后继结点
pre->ltag = 1;
}
if(pre != NULL && pre->rchild == NULL){
q->rchild = q;//建立前驱结点的后继结点
pre->rtag = 1;
}
pre = q;//pre指向当前结点
}
//遍历二叉树
void InThread(ThreadTree T){
if(T != NULL){
InThread(T->lchild);//遍历左
visit(T);//遍历根
InThread(T->rchild);//遍历右
}
}
//中序线索化二叉树
void CreatInThread(ThreadTree T){
pre = NULL;//pre初始化为NULL
if(T != NULL){
InThread(T);
if(pre->rchild == NULL){
pre->rtag = 1;//处理遍历的最后一个结点
}
}
}
int main(){
return 0;
}
2.先序线索化:
3.后序线索化:
(五)总结:
十三、线索二叉树找前驱/后继
(一)中序:
(二)先序:
(三)后序:
十四、树
(一)树的逻辑结构:
树是n个结点的有限集合,n=0时,为空树,非空树需要满足下列条件:
1)有且仅有一个根结点;
2)当n>1时,其余结点可分为m个互不相交的有限集合,每个集合又是一棵树,称为根节点的子树;
3)树是一种递归定义的数据结构。
(二)双亲表示法(顺序存储):
每个结点中保存指向双亲的指针
#define MAX_TREE_SIZE 100
typedef struct{//树的结点定义
int data;//数据域
int parent; //双亲位置域
}PTNode;
typedef struct{//树的类型定义
PTNode nodes[MAX_TREE_SIZE];//双亲表示法
int n;//结点
}PTree;
优点:查指定结点的双亲很方便
缺点:查指定结点的孩子结点只能从头遍历
(三)孩子表示法(顺序+链式存储):
顺序存储各结点,每个结点中保存孩子链表的头指针
#define MAX_TREE_SIZE 100
//孩子表示法
struct CTNode{
int child;//孩子结点在数组中的位置
struct CTNode* next;//下一个孩子
};
typedef struct{
int data;
struct CTNode* firstChild;//第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n;//结点数
int r;//根的位置
}CTree;
int main(){
return 0;
}
(四)孩子兄弟表示法(链式存储):
typedef struct CSNode{
int data;
struct CSNode* firstchild;//左指针
struct CSNode* nextchild;//右指针
}CSNode,*CSTree;
(五)树、森林、二叉树的转换:
1.树和二叉树的转换:
2.森林和二叉树的转换:
森林:m个互不相交的树的集合。
十五、树和森林的遍历
(一)树的遍历:
1.先根遍历:
先访问根节点,然后依次对每颗子树进行先根遍历
void PreOrder(TreeNode* R){
if(R != NULL){
visit(R);
while(R还有下一个子树T){
PreOrder(T);
}
}
}
2.后根遍历:
先依次对每颗子树进行后根遍历,最后访问根节点
void PostOrder(TreeNode* R){
if(R != NULL){
while(R还有下一个子树T){
PostOrder(T);
visit(R);
}
}
}
3.层序遍历:
用队列实现,若树非空,则根节点入队;若队列非空,则队头元素出并访问,同时将该元素的孩子入队
(二)森林的遍历:
1.先序遍历:
访问森林中第一颗树的根节点,然后先序遍历第一颗树中根节点的子树森林,最后先序遍历除第一颗树之后的剩余的树构成的森林;等同于依次对各个树进行先根遍历
2.中序遍历:
中序遍历森林中第一颗树的根节点的子树森林,然后访问第一棵树的根节点,最后中序遍历除第一棵树之后的剩余树构成的森林;等同于依次对各个树进行后根遍历
十六、二叉排序树(BST)
(一)定义:
二叉排序树,又称二叉查找树(Binary Search Tree):
1)左子树上所有结点的关键字均小于根节点的关键字;
2)右子树上所有结点的关键字均大于根节点的关键字;
3)左子树和右子树又个是一颗二叉排序树。
即:左子树结点值 < 根节点值 < 右子树结点值
typedef struct BSTNode{
int key;
struct BSTNode* lchild;
struct BSTNode* rchild;
}BSTNode,*BSTree;
(二)查找操作:
若树非空,则目标值和根结点的值比较:
若相等,则查找成功;
若小于根结点,则在左子树上查找,否则在右子树上查找;
查找成功,则返回结点指针;查找失败,则返回NULL。
//查找值为n的结点
BSTNode* BST_Search0(BSTree T,int n){
while(T != NULL && n != T->key){//查找前提:树非空且查找值不等根节点
if(n < T->key){
T = T->lchild;
} else{
T = T->rchild;
}
}
return T;
}
//查找值为n的结点(递归实现)
BSTNode* BST_Search1(BSTree T,int n){
if(T == NULL){
return NULL;//查找失败
}
if(n == T->key){
return T;//查找成功
}else if(n < T->key){
return BST_Search1(T->lchild,n);
}else{
return BST_Search1(T->rchild,n);
}
}
(三)插入操作:
若原二叉排序树为空,则直接插入结点;
否则,若关键字小于根节点的值,则插入到左子树;若大于根节点的值,则插入到右子树。
//二叉排序树的插入(递归实现)
int BST_Insert0(BSTree &T,int k){
if(T == NULL){
T = (BSTree)malloc(sizeof(BSTree));
T->key = k;
T->lchild = T->rchild = NULL;
return 1;
}else if(k == T->key){
return 0;//插入的值相同,则插入失败
}else if(k < T->key){
return BST_Insert0(T->lchild,k);
} else{
return BST_Insert0(T->rchild,k);
}
}
//按照str = {50,66,60,26,21,30,70,68}建立BST
void Creat_BST(BSTree &T,int str[],int n){
T = NULL;
int i = 0;
while(i < n){
BST_Insert0(T,str[i]);
i ++;
}
}
(四)删除操作:
1)若被删除结点是叶子结点,则直接删除;
2)若被删除结点只有一颗左子树或右子树,则让其子树成为其父结点的子树,代替其位置;
3)若被删除结点右左右子树,则令其的直接后继或直接前驱代替其,然后从二叉排序树中删除这个直接后继或直接前驱,转换为第一种或者第二种情况
如下所示:
(五)查找效率分析:
查找长度:在查找运算中,需要对比关键字的次数,其反应查找操作的时间复杂度
十七、平衡二叉树(AVL)
(一)定义:
平衡二叉树(Balanced Binary Tree):简称平衡树(AVL树),其树上任意一结点的左右子树高度之差不超过1;
结点的平衡因子:左子树高 - 右子树高
平衡二叉树结点的平衡因子的值只可能是-1、0、1
typedef struct AVLNode{
int data;//数据域
int balance;//平衡因子
struct AVLNode* lchild;
struct AVLNode* rchild;
}AVLNode,*AVLTree;
(二)插入操作:
从插入点往回找到第一个不平衡结点,调整以该结点为根的子树(最小不平衡子树)
(三)插入新结点后如何调整"不平衡"问题:
1.在A的左孩子的左子树中插入(LL):
代码思路:
2.在A的右孩子的右子树中插入(RR):
代码思路:
3.在A的左孩子的右子树中插入(LR):
4.在A的右孩子的左子树中插入(RL):
十八、哈夫曼树
(一)带权路径长度:
结点的权:有某种现实含义的数值
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和(WPL):
(二)哈夫曼树的定义:
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树