1.1. 概念
二叉树Binary Tree
是n
个结点的有限集合(算上根结点)。
它可以是 空集n=0
,或可以是 由一个根结点以及两颗互不相交、分别称为左子树和右子树的二叉树组成。
1.2. 特点
二叉树与普通有序树不同,二叉树 严格区分 左子和右子,即使只有一个 子结点 也要区分左右。
二叉树的 树度数 最大为2
。
1.3. 性质
- 二叉树的第
k
层上的 结点数 最多个2k-1
个
最多的情况,解释
最多的情况
层1:21-1 = 20 = 1
层2:22-1 = 21 = 2
层3:23-1 = 22 = 4
层4:24-1 = 23 = 8
层k:2k-1
- 深度为
k
的二叉树最多有2k-1
个结点
公式解释
层1:20 = 1
层2:21 = 2
层3:22 = 4
等比数列求和:
Sn= 1*(1-2k) / 1-2 = 2k-1
- 在任意一颗二叉树中,树叶(终端结点) 的数目 比 度数为
2
的结点数目多1
。
解释
N
:结点的总数(算上 根结点的 总结点数)
N0
:没有后继的结点个数(叶子结点个数)
N1
:只有一个后继的结点个数(只有一个孩子的树)
N2
:有两个后继的结点个数(只有俩孩子的树)
总结点 = 各结点 数目 之和 N = N0 + N1 + N2
总结点 = 所有类别的子节点数目 + 根 N = 0 × N0 + 1 × N1 + 2 × N2 + 1
联立以上两式可得: N0 = N2 + 1
(网易) 一棵二叉树有8
个度为2
的节点,5
个度为1
的节点,那么度为0
的节点个数为 ( )
A. 不确定 B. 7 C. 8 D. 9 E. 6
1.4. 常见特例
1.4.1. 满二叉树
满二叉树:深度为k(k>=1)
时结点个数为2k-1
(叶子结点是满的)
1.4.2. 完全二叉树
完全二叉树:只有 最下面两层 有度数小于2
的节点,且最下面一层的结点集中在最左边的若干位置上。
- 倒数第二层,必须全是满的
- 倒数第一层,集中在最左侧
1. 实现 完全 二叉树
二叉树的 存储结构 有两种,分为 顺序存储 和 链式存储
顺序 存储 普通二叉树,需要将其 提前 转换成 完全二叉树。
链式 存储 二叉树,无特殊要求 ?
1.1. 顺序存储(无代码、概念、做题)(知道即可)
二叉树的顺序存储,只能用顺序表存储。
想要顺序存储普通二叉树,就需要将其提前转换成完全二叉树。
普通二叉树转完全二叉树的方法很简单,只需给二叉树额外添加一些结点,将其"拼凑"成一个完全二叉树即可。
解决二叉树的转化问题,接下来就是如何顺序存储完全(满)二叉树。
左侧是普通二叉树,右侧是转化后的完全(满)二叉树。
存储 完全二叉树
完全(满)二叉树的顺序存储,仅需要从根结点开始,按照层次依次将树中结点存储到数组即可。
存储图 2 所示的完全二叉树:
存储由 普通二叉树 转化来的 完全二叉树
图 1 中的普通二叉树在数组中的存储状态如图所示:
-----分割线-----
如何从顺序表中去还原完全二叉树?:
当根结点号为 非0,比如1时:
将完全二叉树中结点 按照 层次 并从左到右依次编号(1
2
3
...
),若结点i
有左子,则其左子的结点编号为2*i
,右子编号为2*i+1
。()
- 设 完全二叉树的 结点数为
n
,某结点的编号为i
。 - 当
i>1
时(不是根结点时),有父节点,其编号为i/2
。 - 当
2*i <= n
时,有左子,其编号为2*i
;否则没有左子,没左子一定没右子,其本身为叶节点。 - 当
2*i+1 <= n
时,有右子,其编号为2*i+1
,否则就没有右子。
1.2. 遍历:
1.2.1. 先序遍历
先序:根----->左----->右
A B D H I E J C F K G
1.2.2. 中序遍历
中序:左----->根----->右
H D I B E J A F K C G
1.2.3. 后序遍历
后序:左----->右----->根
H I D J E B K F G C A
1.3. 根据 遍历结果,画出对应的二叉树(不强调 什么种类的 树)(一定要会)
例子:已知前序中序,画出对应二叉树(有序树,非完全)
前序:A B C E H F I J D G K
根----->左----->右
中序:A H E C I F J B D K G
左----->根----->右
思路
先看前序,前序中A肯定是根
去中序看A
,A
前无结点,故A
无左子。
回到前序,A后为B,根据前序特点根左右,B一定为A的左子或右子,故B为右子。
去中序看B
,AB
之间为B
的左子树(H E C I F J)
回到前序, B后为C,根据前序特点根左右,C一定为B的左子或右子,故C为左子。(因为其在左子树)
去中序看C
,AC
之间为C
的左子树(H E)
回到前序, C后为E,根据前序特点根左右,E一定为C的左子或右子,故E为左子。(因为其在左子树)
去中序看E
,AE
之间为E
的左子树(H )
回到前序, E后为H,根据前序特点根左右,H一定为E的左子或右子,故H为左子。(因为其在左子树)
技巧
得到根结点的左右子后,去模块化(连续成组,最低两个)
前序:A B C E H F I J D G K
根----->左----->右
中序:A H E C I F J B D K G
左----->根----->右
ABCFD
ACFBD
1.4. 链式存储(递归 + 代码)
链式存储此二叉树,从根结点开始,将各个结点以及其左右子 使用链表 进行存储即可。
1.4.1. 定义结点 结构体
结点结构体由三部分构成:
- 指向左子结点的指针(
Lchild
) - 结点存储的数据
- 指向右子结点的指针(
Rchild
)
typedef struct BinaryTreeNode
{
int data; // 数据域
struct BinaryTreeNode *Lchild; // 左子指针
struct BinaryTreeNode *Rchild; // 右子指针
} BTN, *BT;
1.4.2. 创建二叉树
返回:创建成功后已存在根结点,返回其地址。
1.4.3. 先序遍历
1.4.4. 中序遍历
1.4.5. 后序遍历
1.4.6. 层次遍历
将每个根结点出列,用root
来接收,打印完数据后,再判断有无左子,如果有左子,则让左子入列。
左子B
入列。有右子C
,让C
入列。
所以是一层层的。
示意图:
1.4.7. 无 层次遍历代码:单文件
#include <stdio.h>
#include <stdlib.h>
typedef struct BinaryTreeNode
{
int data; // 数据域
struct BinaryTreeNode *Lchild; // 左孩子指针
struct BinaryTreeNode *Rchild; // 右孩子指针
} BTN, *BT;
// 2. 创建二叉树
BT BTInit(int n, int i) // n 结点总数; i 目前结点的编号,根节点就是1
{
// 1. 开辟空间存放根结点
BT root = (BT)malloc(sizeof(BTN));
if (NULL == root)
{
printf("BTInit failed, root malloc err.\n");
return NULL;
}
// 2. 初始化根结点成员
// 2.1 对根结点进行赋值(编号)
root->data = i; // 传过来的根结点编号
// 下面是给左右子赋值,赋值的前提是左右子存在。那么就需要判断一下
// 怎么判断?
// 2.2 判断有无左右孩子
// 2.2.1 有左子,让左子作为根重复本函数的操作
if (2 * i <= n)
root->Lchild = BTInit(n, 2 * i); // 因为根的编号是i,根的左子编号就是2*i
// 2.2.2 没有左子,那么将root->Lchild置为NULL
else
root->Lchild = NULL;
// 2.2.3 有右子,让右子作为根重复本函数的操作
if (2 * i + 1 <= n)
root->Rchild = BTInit(n, 2 * i + 1);
// 2.2.4 没有右子,那么将root->Rchild置为NULL
else
root->Rchild = NULL;
return root;
}
// 3. 先序遍历二叉树
// 根——左——右
void PreOrder(BT root)
{
// 空树
if (root == NULL)
return;
// 3.1 打印根结点数据(编号)
printf("%d ", root->data); // 根
// 3.2 如果有左子则将左子作为根进行打印
if (root->Lchild != NULL)
PreOrder(root->Lchild); // 左
// 3.3 如果有右子则将右子作为根进行打印
if (root->Rchild != NULL)
PreOrder(root->Rchild); // 右
}
// 4. 中序遍历二叉树
// 左——根——右
void InOrder(BT root)
{
// 空树
if (root == NULL)
return;
// 4.1 如果有左子则将左子作为根进行打印
if (root->Lchild != NULL)
InOrder(root->Lchild); // 左
// 4.2 打印根结点数据(编号)
printf("%d ", root->data); // 根
// 4.3 如果有右子则将右子作为根进行打印
if (root->Rchild != NULL)
InOrder(root->Rchild); // 右
}
// 5. 后序遍历二叉树
// 左——右——根
void PostOrder(BT root)
{
// 空树
if (root == NULL)
return;
// 5.1 如果有左子则将左子作为根进行打印
if (root->Lchild != NULL)
PostOrder(root->Lchild); // 左
// 5.2 如果有右子则将右子作为根进行打印
if (root->Rchild != NULL)
PostOrder(root->Rchild); // 右
// 5.3 打印根结点数据(编号)
printf("%d ", root->data); // 根
}
int main(int argc, char const *argv[])
{
// 适用于,根节点编号 非0情况
BT root = BTInit(13, 1);
printf("先序遍历:");
PreOrder(root);
printf("\n");
printf("中序遍历:");
InOrder(root);
printf("\n");
printf("后序遍历:");
PostOrder(root);
printf("\n");
}
1.4.8. 有 层次遍历代码:多文件
目录结构:
#include "BiTree.h"
// 2. 创建二叉树
BT BTInit(int n, int i) // n 结点总数; i 目前结点的编号,根节点就是1
{
// 1. 开辟空间存放根结点
BT root = (BT)malloc(sizeof(BTN));
if (NULL == root)
{
printf("BTInit failed, root malloc err.\n");
return NULL;
}
// 2. 初始化根结点成员
// 2.1 对根结点进行赋值(编号)
root->data = i; // 传过来的根结点编号
// 下面是给左右子赋值,赋值的前提是左右子存在。那么就需要判断一下
// 怎么判断?
// 2.2 判断有无左右孩子
// 2.2.1 有左子,让左子作为根重复本函数的操作
if (2 * i <= n)
root->Lchild = BTInit(n, 2 * i); // 因为根的编号是i,根的左子编号就是2*i
// 2.2.2 没有左子,那么将root->Lchild置为NULL
else
root->Lchild = NULL;
// 2.2.3 有右子,让右子作为根重复本函数的操作
if (2 * i + 1 <= n)
root->Rchild = BTInit(n, 2 * i + 1);
// 2.2.4 没有右子,那么将root->Rchild置为NULL
else
root->Rchild = NULL;
return root;
}
// 3. 先序遍历二叉树
// 根——左——右
void PreOrder(BT root)
{
// 空树
if (root == NULL)
return;
// 3.1 打印根结点数据(编号)
printf("%d ", root->data); // 根
// 3.2 如果有左子则将左子作为根进行打印
if (root->Lchild != NULL)
PreOrder(root->Lchild); // 左
// 3.3 如果有右子则将右子作为根进行打印
if (root->Rchild != NULL)
PreOrder(root->Rchild); // 右
}
// 4. 中序遍历二叉树
// 左——根——右
void InOrder(BT root)
{
// 空树
if (root == NULL)
return;
// 4.1 如果有左子则将左子作为根进行打印
if (root->Lchild != NULL)
InOrder(root->Lchild); // 左
// 4.2 打印根结点数据(编号)
printf("%d ", root->data); // 根
// 4.3 如果有右子则将右子作为根进行打印
if (root->Rchild != NULL)
InOrder(root->Rchild); // 右
}
// 5. 后序遍历二叉树
// 左——右——根
void PostOrder(BT root)
{
// 空树
if (root == NULL)
return;
// 5.1 如果有左子则将左子作为根进行打印
if (root->Lchild != NULL)
PostOrder(root->Lchild); // 左
// 5.2 如果有右子则将右子作为根进行打印
if (root->Rchild != NULL)
PostOrder(root->Rchild); // 右
// 5.3 打印根结点数据(编号)
printf("%d ", root->data); // 根
}
// 6. 层次遍历
void UnOrder(BT root)
{
// 6.1 创建一个队列,队列的数据域变成指向树结点的指针
LQ *PQ = LQInit();
if (root != NULL)
LQPush(PQ, root); // 入队时候传递的是指针
while (PQ->front != PQ->rear) // 出列出列出着出着就空了
{
root = LQPop(PQ);
printf("%d ", root->data);
// 只要左子不为空,就入列,之后出列的时候打印
if (root->Lchild != NULL)
LQPush(PQ, root->Lchild);
// 只要右子不为空,就入列,之后出列的时候打印
if (root->Rchild != NULL)
LQPush(PQ, root->Rchild);
}
// 思想:先让根入队、再左子入队、再右子入队,这样依次入队,入队完之后每次循环出队一个。
// 出着出着就空了
}
#ifndef _BINARY_TREE_H_
#define _BINARY_TREE_H_
#include <stdio.h>
#include <stdlib.h>
#include "LinkQueue.h"
typedef struct BinaryTreeNode
{
int data; // 数据域
struct BinaryTreeNode *Lchild; // 左孩子指针
struct BinaryTreeNode *Rchild; // 右孩子指针
} BTN, *BT;
// 2. 创建二叉树
BT BTInit(int n, int i);
// 3. 先序遍历二叉树
// 根——左——右
void PreOrder(BT root);
// 4. 中序遍历二叉树
// 左——根——右
void InOrder(BT root);
// 5. 后序遍历二叉树
// 左——右——根
void PostOrder(BT root);
// 6. 层次遍历
void UnOrder(BT root);
#endif
#include "LinkQueue.h"
// 2. 创建一个空的链式队列
LQ *LQInit()
{
// 2.1 开辟空间存放操作链式队列的结构
// 想单独使用结构体指针必须要开辟空间
LQ *PQ = (LQ *)malloc(sizeof(LQ));
if (NULL == PQ)
{
printf("LQInit failed, PQ malloc err.\n");
return NULL;
}
// 2.2 对结构体成员进行初始化(malloc开辟空间存放链表结点)
PQ->front = PQ->rear = (LL)malloc(sizeof(LLN));
if (NULL == PQ->front)
{
printf("LQInit failed, front&rear malloc err.\n");
return NULL;
}
// 2.3 对链表结点进行初始化,即next置NULL
PQ->front->next = NULL;
// PQ->rear->next = NULL;
return PQ;
}
// 3. 入列,尾插(知道终端结点的单向链表尾插)
int LQPush(LQ *PQ, LLNDataType data)
{
// 3.1 创建一个新结点保存即将插入的数据
LL PNew = (LL)malloc(sizeof(LLN));
if (NULL == PNew)
{
printf("PQPush faild, PNew malloc err.\n");
return -1;
}
// 3.2 初始化新结点
PNew->data = data;
PNew->next = NULL;
// 3.3 新结点链接到链表的尾巴
PQ->rear->next = PNew;
// 3.4 移动队尾指针rear使其指向新的终端结点
PQ->rear = PNew;
return 0;
}
// 4. 打印链式队列(使用front队头指针进行遍历,打印结点的数据域)
// void LQPrint(LQ *PQ)
// {
// LL H = PQ->front;
// while (H->next) // while (H->next != NULL)
// {
// H = H->next;
// printf("%d\t", H->data);
// }
// printf("\n");
// }
// 5. 数据出列(返回NULL)
LLNDataType LQPop(LQ *PQ)
{
// 5.1 容错判断(空了没法出)
if (PQ->front == PQ->rear)
{
printf("LQPop failed, LQ is empty.\n");
return NULL;
}
// 5.2 PDel指向头结点
LL PDel = PQ->front;
// 5.3 队头指针向后移动
PQ->front = PQ->front->next;
// 5.5 释放老头结点
free(PDel);
PDel = NULL;
// 5.6 返回出列数据
return PQ->front->data;
}
// 6. 求队列长度
int LQLength(LL H)
{
int len = 0;
while (H->next != NULL)
{
H = H->next;
len++;
}
return len;
}
// 7. 清空队列
void LQClear(LQ *PQ)
{
while (PQ->front != PQ->rear)
LQPop(PQ);
}
#ifndef _LINK_QUEUE_H_
#define _LINK_QUEUE_H_
#include <stdio.h>
#include <stdlib.h>
#include "BiTree.h" // 需要知道 二叉树结点 的结构体类型
// 1. 定义结构体
// 1.1 定义链式队列结点结构体
typedef struct BinaryTreeNode *LLNDataType; // 把队列的数据域变成指向树结点的指针,不能连续typedef
typedef struct LinkListNode
{
LLNDataType data;
struct LinkListNode *next;
} LLN, *LL;
// 1.2 定义操作链式队列的结构体
typedef struct LinkQueue
{
LL front; // 队头结点指针(LLN * front)(struct LinkListNode *)
LL rear; // 队尾结点指针
} LQ;
// 2. 创建一个空的链式队列
LQ *LQInit();
// 3. 入列,尾插(知道终端结点的单向链表尾插)
int LQPush(LQ *PQ, LLNDataType data);
// 4. 打印链式队列(使用front队头指针进行遍历,打印结点的数据域)
//void LQPrint(LQ *PQ);
// 5. 数据出列(返回NULL)
LLNDataType LQPop(LQ *PQ);
// 6. 求队列长度
int LQLength(LL H);
// 7. 清空队列
void LQClear(LQ *PQ);
#endif
#include "LinkQueue.h"
#include "BiTree.h"
int main(int argc, char const *argv[])
{
// 适用于,根节点编号 非0情况
BT root = BTInit(13, 1);
printf("先序遍历:");
PreOrder(root);
printf("\n");
printf("中序遍历:");
InOrder(root);
printf("\n");
printf("后序遍历:");
PostOrder(root);
printf("\n");
printf("层序遍历:");
UnOrder(root);
printf("\n");
return 0;
}
CC=gcc
CFLAG=-c -g -Wall -o
OBJS=MainTest.o LinkQueue.o BiTree.o
target:$(OBJS)
$(CC) $^ -o $@
%.o:%.c
$(CC) $(CFLAG) $@ $<
.PHONY:clean
clean:
rm *.o target -f
1.5. 练习题
- 深度为8的二叉树,其最多有( 28-1 ) 个节点,第
8
层最多有( 27 )个节点 (网易) - 数据结构中,沿着某条路线,依次对树中每个结点做一次且仅做一次访问,对二叉树的节点从
1
开始进行连续编号,要求每个结点的编号大于其左、右孩子的编号,同一结点的左右孩子中,其左子的编号小于其右子的编号,可采用( )次序的遍历实现编号 (网易)
根要大于左右,即根要在后面被访问。
A. 先序 B. 中序 C. 后序 D. 从根开始层次遍历
- 一颗二叉树的 前序: A B D E C F, 中序:B D A E F C 问树的深度是 ( ) (网易)
画出树
A. 3 B. 4 C. 5 D. 6