前言
什么是二叉树?二叉树就是只两个叉的树,这么理解是对的,但是还差点意思,二叉树指的是任何节点的度不大于2的树。怪抽象的,简单的来说,就是任何节点都只能有至多两个子节点的树叫二叉树。下面我们来看看几种特殊的二叉树。
1. 斜树
斜树就是所有节点都斜向一边的树,比如左斜树就是所有节点都只有左子树的树,右斜树就是所有节点都只有右子树的树,如下:
是不是觉得这和线性表顺序表没啥区别,确实如此。
2. 满二叉树
满二叉树就是所有节点要么拥有两个子树,要么没有子树(叶子),且作为的那些叶子的节点必须在同一深度,且在最下一层,就像下面这棵树:
3. 完全二叉树
完全二叉树就是可以缺失部分叶子的满二叉树,但是这个缺失有个条件,那就是在叶子所在的那一深度上,要从右往左缺失,只要不符合这个条件,统统不属于完全二叉树
一、树、森林与二叉树的转换
1. 树转换为二叉树
这就是我们之前讲的树的兄弟孩子表示法,左子树为第一个孩子,右子树为兄弟,转换如下:
2.森林转换为二叉树
首先,先将每一棵树用兄弟孩子表示法转换成二叉树,每个根节点的右子树都是空着的,这时候再将其他树,作为根节点的右子树,如下图:
二、二叉树的性质
性质1. 二叉树的第i层至多有2i-1个节点
要证明这个性质很简单,因为二叉树限定了任一节点的子树至多有两个,那么第i层的节点数最多是上一层结点数的两倍,想让该层结点数最多就要让上一层节点数也最多,这不就是满二叉树嘛,而满二叉树的第i层的结点数恰好就是2i-1
性质2. 深度为k的满二叉树至多有2k-1个结点
想要结点数最多那就是满二叉树嘛,直接计算深度为k的满二叉树的结点数,恰好就是2k-1。
性质3. 任何一颗二叉树如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1
任意一棵树他的总结点数n = n0 + n1 + n2,从下往上看,每个结点对应一个分支处理根结点,所有总分支为n - 1,从上往下看,度为2的结点有两个分支,度为1的结点有一个,度为0的没有,所有总分支数为n1 + n2,所有n - 1 = n1 + n2
联立结点数和分支数的式子,就得到了n0 = n2 + 1。
性质4. 具有n个结点的完全二叉树的深度为 ⌊ l o g 2 ( n + 1 ) ⌋ \lfloor log_2(n+1) \rfloor ⌊log2(n+1)⌋ + 1
假设这个二叉树的深度为k,同深度的满二叉树的结点数为2k + 1,k - 1深度的二叉树的结点数为2k-1 + 1,由完全二叉树的定义我们可以知道2k-1 + 1 < n ≤ 2k + 1,化简得 ⌊ l o g 2 ( n + 1 ) ⌋ \lfloor log_2(n+1) \rfloor ⌊log2(n+1)⌋ + 1
性质5. 如果一个有n个结点的完全二叉树,结点编号i如图所示,则有如下性质:
1)如果i = 1,则结点是二叉树的根无双亲;如果i > 1,则其双亲是
⌊
i
/
2
⌋
\lfloor i / 2 \rfloor
⌊i/2⌋
2)如果2i > n,则结点无左孩子,否则其左孩子是结点2i
3)如果2i + 1 > n,则结点无右孩子,否则其右孩子是结点2i + 1
三、二叉树存储及其操作
(一)普通二叉树
顺序存储结构
顺序存储结构,在存储非完全二叉树时会十分浪费空间,这里我们只讲他的存储思路,实现代码十分简单定义一个数组就好。我们知道树是可以编号的,他对应的号码和满二叉树的的一样,基于此,我们只需要把数据存到相应的位置就好
链式存储结构
1. 二叉链表
由于限定了结点的度数,这个时候孩子表示法就变得轻松多了
typedef struct BiTNode{
TElemType data;
struct BiTNode *lchild, *rchild;
}*BiTree;
2. 三叉链表
我们也可以设置一个指向双亲的指针(双亲孩子表示法),这样我们无论是上溯还是下寻都方便很多
typedef struct TriTNode{
TElemType data;
struct TriTNode *lchild, *parent, *rchild;
}*TriTree;
操作
1. 构造二叉树
构造一个二叉树用递归的方法最好理解了,首先设置一个停止递归的条件,然后给当前结点赋值后递归构造该结点的左右子节点
Status CreatBiTree(BiTree &T){
cin >> ch;
if(ch == '#') return OK;
else{
if(!(T = new BiTNode)) exit(OVERFLOW);
T -> data = ch;
CreatBiTree(T -> lchild);
CreatBiTree(T -> rchild);
}
return ok;
}
2. 复制二叉树
赋值二叉树的思路和构造二叉树一致,用递归来遍历整棵树,同时将值给取出来
int copy(BiTree T, BiTree &NewT){
if(T == NULL){
NewT = NULL;
return 0;
}else{
NewT = new BiTNode;
NewT -> data = T -> data;
copy(T -> lchild, NewT -> lchild)
copy(T -> rchild, NewT -> rchild)
}
}
3. 遍历二叉树
遍历树有三种方法,分别为线序遍历、中序遍历、后序遍历,分别代表先遍历根后左右子树、先遍历左子树接着遍历根最后遍历右子树、先遍历左右子树最后根,不同的方法遍历出来的顺序非常不一样,但是对应代码不过是改变了三条语句的顺序
Status PreOrderTraverse(BiTree T){
if(T == NULL) return OK;
else{
visit(T); // 访问根节点
PreOrderTraverse((T -> lchild); // 遍历左子树
PreOrderTraverse((T -> rchild); // 遍历右子树
}
}
4. 中序遍历二叉树(用栈代替递归)
首先初始化一个栈来存放还未遍历到的根节点,当T指指向空或者栈非空的情况下,判断T是否为空,如果不是就需要让他进栈去判断他的左子树,如果为空就说明遍历左子树已经到头了,轮到根,然后将他的值赋值为右子树,接着遍历
Status InOrdertraverse(BiTree T){
InitStack(S);
while(T || !StackEmpty(s)){
if(T){
push(S, T);
T -> lchild;
}else{
pop(S, T);
cout << T -> data;
T = T -> rchild;
}
}
return OK;
}
5. 层次遍历二叉树(用队列代替递归)
根据层次来遍历整棵树,只要队列不为空,就说明还有需要遍历的结点,将他取出来读出数据后,再将他的左右子树入队等待遍历
void LevelOrder(BiTree T){
SqQueue *qu;
InitQueue(qu);
enQueue(qu, T);
while(!EmptyQueue(qu)){
deQueeu(qu, T);
cout << T -> data;
if(T -> lchild != NULL) enQueue(T -> lchild);
if(T -> rchild != NULL) enQueue(T -> rchild);
}
}
6. 获取树的深度
int Depth(BiTree T){
if(T == NULL) return 0;
else{
int m = 0, n = 0;
m = Depth(T -> lchild);
n = Depth(T -> rchild);
return m > n ? (m+1) : (n+1);
}
}
7. 获取树的结点数
int NodeCount(BiTree T){
if(T == NULL) return 0;
return NodeCount(T -> lchild) + NodeCount(T -> rchild) + 1;
}
8. 获取树的叶子数
int LesfCount(BiTree T){
if(T == NULL) return 0;
if(T -> lchild == NULL && T -> rchild == NULL) return 1;
else return LeafCount(T -> lchild) + LeafCount(T -> rchild);
}
(二)线索二叉树
一个有n个结点的二叉树有2n个指针,但只有n-1条分支,因而有n+1个指针被浪费了,我们要把它作为线索利用起来指向他的前驱和后继,为了区分他指向的是孩子还是线索,听到这里你是不是觉得这是什么垃圾结构,为了利用空闲位置还让我倒贴两倍的位置进去。如果线索二叉树止步于此,那它确实是一个垃圾结构,但是线索二叉树的优点并不在于能节省空间,而在于能节省时间,我们之前对树做一个遍历,常常用到递归,这十分的消耗内存和空间,而线索二叉树可以帮我们摆脱递归,让时间复杂度变成O(n)。
存储结构
typedef enum{Link, Thread} PointerTag;
typedef struct BiThrNode{
TElemType data;
BiThrNode *lchild; *rchild;
PointerTag LTag, RTag;
}*BiThrTree;
操作
1. 二叉树线索化
通过中序遍历的方式线索化一个二叉树,首先线索化他的左子树,线索化完之后对当前结点进行处理,如果他没有左子树,就将他的前驱赋值给左孩子,如果他的前驱没有右子树就将他赋值给他的前驱的右孩子
BiThrTree pre; // 指向当前访问结点的上一结点
void InThreading(BiThrTree T){
if(T){
InThreading(T -> lchild);
if(!p -> lchild){
p -> LTag = Thread;
p -> lchild = pre;
}
if(!pre -> rchild){
pre -> RTag = Thread;
pre -> rchild = p;
}
pre = p;
InThreading(T -> rchild)
}
}
2. 中序遍历线索二叉树
Status InOrderTraverse_Thr(BiThrTree T){
BiThrTree p;
p = T -> lchild;
while(p != T){
while(p -> LTag == Link) p = p -> lchild;
cout << p -> data;
while(p -> RTag == Thread && p -> rchild != T){
p = p-> rchild;
cout << p -> data;
}
p = p -> rchild;
}
return OK;
}
四、哈夫曼树
如果一个二叉树的结点是带权的,那么我们要如何让整棵树的权值最小呢?答案很简单,只需要将权值高的放在前面就好了,哈夫曼树就是这个原理,可以用它来解决远距离通信时数据数据压缩的问题
1. 哈夫曼树的存储(顺序存储)
typedef struct HTNode{
int weight;
int parent, lchild, rchild;
}*HuffmanTree;
void CreatHuffmanTree(HuffmanTree HT, int n){
// 创建带权结点
if(n <= 1) return;
m = 2*n - 1;
HT = new HTNode[m+1];
// 初始化结点
for(int i = 1; i < m; ++i){
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
//输入权重
for(int i = 1; i < n; ++i){
cin >> HT[i].weight;
}
//合并结点并把他们存在后n-1个位置
for(int i = n+1; i <= m; ++i){
// 选取权重最小的两个结点
select(HT, i-1, s1, s2);
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}