树的基本概念

树的定义:树是一种非线性的数据结构,是由一个根和若干个不相交的子树构成的,每一棵子树也是一棵树,即树的定义是递归的,树的结点数可以为0

树的基本术语:

结点:包含数据元素和指向子树的分支

结点的度:结点拥有的子树或者说叫分支的个数

树的度:书中各结点度的最大值

叶子结点:又叫做终端结点,指的是度为零的结点(没有分支)

非终端结点:又叫做分支结点,度不为零的结点

孩子:结点的子树的根

双亲:与孩子的定义相对应,结点的孩子的双亲就是该结点

兄弟:同一个双亲的孩子互为兄弟

祖先:从根到某结点的路径上的所有结点都是这个结点的祖先

子孙:以某结点为根的子树的全部结点,都是该结点的子孙

层次:从根开始,根为第一层,根的孩子为第二层,以此类推

树的高度:树中结点的的最大层次

结点的深度和高度:

1)结点的深度是从根结点到该结点路径上的结点个数

2)从某结点往下走可能到达多个叶子结点,对应了多条通往这些叶子结点的路径,其中最长的那条路径上结点的个数即为该结点在树中的高度

3)结节点的高度为树的高度

堂兄弟:双亲在同一层的结点互为堂兄弟

有序树:树中节点的子树从左到右是有次序的,不能交换,这样子叫做有序树

无需树:树中结点的子树没有顺序,可以任意交换

丰满树:丰满树即为理想平衡树,要求除了最底层外,其余层都是满的

森林:若干棵不相交的树的集合

树的存储结构

1、顺序存储结构

2、链式存储结构

二叉树

二叉树的定义:

  1. 每个结点最多只有两棵子树,即二叉树中结点的度只能为0,1,2,

  2. 子树有左右顺序之分,不能颠倒

满二叉树:如果所有的分支结点都有左孩子和右孩子,并且叶子结点都集中在二叉树的最下层

完全二叉树:树上所有结点满足二进制结构的二叉树,可以观察得到,所有的儿子结点的双亲都为儿子结点除以2得到的树,一棵完全二叉树可以由满二叉树按照“从右至左,从下至上”的顺序删除得到

二叉树的主要性质:

1、非空二叉树的叶子节点数等于双分支节点数加1

证明:设二叉树叶子节点数为\(n_0\),单分支节点数为\(n_1\),双分支结点数为\(n_2\),总结点数为\(n_0+n_1+n_2\)。在一棵二叉树中,所有结点的分支数等于单分支节点数加上双分支结点数的两倍,即总的分支数为\(n_0+n_1+n_2\)。由于二叉树中除根节点之外,每个结点都有惟一的一个分支指向它,因此二叉树中有总分支数=总结点数-1
由此可得:\(n_0+n_1+n_2-1=n_1+2n_2\)
化简得:\(n_0=n_2+1\)
2、二叉树的第\(i\)层上最多有\(2^{i-1}(i\geq1)\)个节点

3、高度(或深度)为k的二叉树最多有\(2^{k}-1(k\geq1)\)个结点。换句话说,满二叉树的前k层结点数量为\(2^k-1\)

4、卡特兰函数Catalan():给定n个结点,能构成\(h(n)\)种不同的二叉树, \(h(n)=\frac{C_{2n}^{n}}{n+1}\)

5、具有 \(n(n\geq1)\) 个结点的完全二叉树的高度(深度)为 \(\lfloor log_{2}n \rfloor+1\)

线索二叉树:n个结点的二叉树有n+1个空链域,将原来的空指针利用起来,指向结点的前驱或者后驱,对于不同的遍历顺序,把原来的树形结构变成线性结构,更方便多次遍历

ltag和rtag:

  • 如果ltag=0,则表示lchild为指针,指向结点的左孩子;如果=1,则表示lchild为线索,指向结点的直接前驱
  • 如果rtag=0,则表示rchild为指针,指向结点的右孩子;如果=1,则表示rchild为线索,指向结点的直接后驱
#include<bits/stdc++.h>
using namespace std;

const int maxSize=1e5+50;
//实际上线索二叉树就是将原来的二叉树变成了链表,方便多次遍历或者查询 
typedef struct TBTnode{
	char data;
	//对于线索二叉树来说,如果ltag为0表示lchild是左儿子,否则是自己的前驱,rtag为0表示rchild是右儿子,否则是后驱 
	int ltag,rtag;
	struct TBTnode *lchild;
	struct TBTnode *rchild;
}TBTnode;

void Visit(TBTnode* p){
	
}
//二叉树的中序遍历构建线索二叉树
//代码中的pre意思上一个遍历过的结点 
void InThread(TBTnode *p,TBTnode *&pre){
	if(p!=NULL){
		InThread(p->lchild,p);
		if(p->lchild!=NULL){
			p->lchild=pre;
			p->ltag=1;
		}
		if(pre!=NULL&&pre->rchild==NULL){
			pre->rchild=p;
			pre->rtag=1;
		}
		pre=p;
		p=p->rchild;
		InThread(p,pre);
	}
}

void createInThread(TBTnode *root){
	TBTnode *pre=NULL;
	if(root!=NULL){
		InThread(root,pre);
		//给最开始的头指针补充信息 
		pre->rchild=NULL;
		pre->rtag=1;
	} 
}

//遍历中序线索二叉树
TBTnode *First(TBTnode *p){
	while(p->ltag==0){
		p=p->lchild;
	}
	return p;
} 

TBTnode *Next(TBTnode *p){
	if(p->rtag==0) return First(p->rchild);
	else return p->rchild;
}

//由此得到中序线索二叉树上执行中序遍历的方法
void Inorder(TBTnode *root){
	for(TBTnode *p=First(root);p!=NULL;p=next(p)) Visit(p);
} 
//先序线索二叉树遍历 
void PreThread(TBTnode *p,TBTnode *&pre){
	if(p!=NULL){
		if(p->lchild==NULL){
			p->lchild=pre;
			p->ltag=1;
		}
		if(pre!=NULL&&pre->rchild==NULL){
			pre->rchild=p;
			pre->rtag=1;
		}
		pre=p;
		if(p->ltag==0) PreThread(p->lchild,pre);
		if(p->rtag==0) PreThread(p->rchild,pre);
	}
}
void preorder(TBTnode *root){
	if(root!=NULL){
		TBTnode *p=root;
		while(p!=NULL){
			while(p->ltag==0){
				Visit(p);
				p=p->lchild;
			}
			visit(p);
			p=p->rchild;
		}
	}
} 
int main(){
	return 0;
}

树和森林与二叉树的互相转换

树转换成二叉树:

  • 将用一个结点的各孩子结点用线串起来
  • 每个结点的第一个孩子作为这个结点的左结点,多余的结点作为左结点的右节点,也就是说每一个结点的右结点都是这个结点在树上的兄弟结点
    二叉树转换成树,反过来操作就行了

森林转换成二叉树:

  • 每一颗单独的树构建同上
  • 每一个根结点的右结点是后面一棵森林的根结点

树和森林的遍历:

  • 分为先序遍历和后序遍历,区别在于根结点是在前还是在后
  • 注意森林的先序遍历是先遍历第一棵树,然后再遍历下一棵,后续同样

树与二叉树的应用

赫夫曼树和赫夫曼编码

基本概念:

  • 路径:路径是从树中一个结点到另一个结点的分支所构成的路线
  • 路径长度: 指的是路径上的分支数目
  • 树的路径长度:根到每个结点的路径长度之和
  • 带权路径长度:结点具有权值,权值乘以路径长度就是带权路径长度
  • 树的带权路径长度:带权路径长度之和

赫夫曼树的构造方法:
给定n个权值,用这个n个权值来构造赫夫曼树的算法:

  • 从n个结点选出最小的两个结点作为左右子树,中间结点为两个结点权值之和,将新的结点加入点集
  • 重复进行操作,直到只剩下一棵树

赫夫曼树的特点

  • 权值越大的结点距离根节点越近
  • 树中没有度为1的结点。这类树被叫做正则二叉树
  • 树的带权路径长度最短

赫夫曼编码:
以字符为叶子结点,其出现次数为权值,构造一棵赫夫曼树。对赫夫曼树的每个结点的左右分支进行编号,左0右1,则从根到每个结点的路径上的数字序列即为每个字符的编码。
赫夫曼编码当中任意字符的编码串都不是另一字符的编码串前缀,所以解码时候就不会出现歧义
解码过程:每次从当前起点出发,沿着数字在得出的赫夫曼树走,每次走到叶子结点的时候就得到了一个字符,之后再从下一个数字出发得到下一个字符

赫夫曼n叉树
对于结点大于等于2的待处理序列都可以得到赫夫曼二叉树,但是如果不能直接构造n叉树,那么就要补足0