一、树的基本概念:
为什么会有树这种结构?因为现实生活中,我们需要一对多的数据处理。
树的结点:使用树结构存储的每一个数据元素都被称为“结点”。例如,上图1中,数据元素 1 就是一个结点;
父结点(双亲结点)、子结点和兄弟结点:对于上图1中的结点 1,2,3,4 来说,1 是 2,3,4 结点的父结点(也称为“双亲结点”),而 2,3,4 都是 1 结点的子结点(也称“孩子结点”)。对于 2,3,4 来说,它们都有相同的父结点,所以它们互为兄弟结点。
根结点:每一个非空树都有且只有一个被称为根的结点。上图1中,结点1就是整棵树的根结点。
叶子结点:如果结点没有任何子结点,那么此结点称为叶子结点(叶结点)。例如上图1中,结点 11,12,6,7,13,9,10都是这棵树的叶子结点。
结点的度(Degree):对于一个结点,拥有的子树数/直接子结点(结点有多少分支)。
树的度:一棵树的度是树内各结点的度的最大值。
结点的层次:从一棵树的树根开始,树根所在层为第一层,根的孩子结点所在的层为第二层,依次类推。对于上图1来说,1 结点在第一层,2,3,4 为第二层,5,6,7,8,9,10在第三层,11,12,13在第四层。
一棵树的深度(高度): 是树中结点所在的最大的层次。上图1树的深度为 4。
有序树:如果树中结点(兄是兄,弟是弟)的子树从左到右看,谁在左边,谁在右边,是有规定的,这棵树称为有序树;反之称为无序树。
森林:由 m(m >= 0)个互不相交的树组成的集合被称为森林。上图1中,分别以2,3,4为根结点的三棵子树就可以称为森林。
1、二叉树的链式存储结构:
二叉树并不适合用数组存储,因为并不是每个二叉树都是完全二叉树,普通二叉树使用顺序表存储或多或多会存在空间浪费的现象。
接下来我们介绍二叉树的链式存储结构。
如上图所示,此为一棵普通的二叉树,若将其采用链式存储,则只需从树的根节点开始,将各个节点及其左右孩子使用链表存储即可。我们称这样的结构为二叉链表。
采用二叉链表存储二叉树时,其节点结构由 3 部分构成(如下图所示):
- 指向左孩子节点的指针(Lchild);
- 节点存储的数据(data);
- 指向右孩子节点的指针(Rchild);
typedef struct BitNode
{
int data;
struct BitNode *lchild,*rchild;
}BitNode,*BitTree;
因此,上上图对应的二叉树的链式存储结构如下图所示:
二、各种典型树:
二叉树:
二叉树几个性质:
- 二叉树不存在度大于2的结点
- 二叉树有左右子树的概念,即使结点只有一棵树
- 二叉树第i层上的结点数目最多为2^(i-1)
- 深度为k的二叉树至多有2^k-1个结点
- 二叉树还可以继续分类,衍生出满二叉树和完全二叉树。
- 包含n个结点的二叉树的高度至少为(log2n)+1
- 在任意一棵二叉树中,若叶子结点的个数为n0,度为2的结点数为n2,则n0=n2+1 【可根据分支线和结点的数目关系列方程组证明】
【证明:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1
证明:因为二叉树中所有结点的度数均不大于2,不妨设n0表示度为0的结点个数,n1表示度为1的结点个数,n2表示度为2的结点个数。三类结点加起来为总结点个数,于是便可得到:n=n0+n1+n2 (1)
由度之间的关系可得第二个等式:n=n0*0+n1*1+n2*2+1即n=n1+2n2+1 (2)
将(1)(2)组合在一起可得到n0=n2+1】
满二叉树:
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
满二叉树除了满足普通二叉树的性质,还具有以下性质:
- 满二叉树中第 i 层的节点数为 2n-1 个。
- 深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2k-1。
- 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。
- 具有 n 个节点的满二叉树的深度为 log2(n+1)。
完全二叉树:
定义:只有最后一层结点不满并且其叶结点集中在靠左的位置上。(在完全⼆叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最⼤
特点:
- 叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。
- 显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。
- 同样结点数的二叉树,完全二叉树深度最小。
如上图 所示是一棵完全二叉树,如上图3b中由于最后一层的节点没有按照从左向右分布,因此只能算作是普通的二叉树。
完全二叉树的性质:
- n 个结点的完全二叉树的深度为 ⌊log2n⌋+1。 【 [log2n]表示取小于 log2n 的最大整数。例如,[log24] = 2,而 [log25⌋]结果也是 2。】
- 或者说,深度为k,有2^k-1个节点的⼆叉树。
- 对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号(图3),对于任意一个结点 i ,完全二叉树还有以下几个结论成立:
(1)当 i>1 时,父亲结点为结点 [i/2] 。(i=1 时,表示的是根结点,无父亲结点)
(2)如果 2 * i>n(总结点的个数) ,则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2*i 。
(3)如果 2 * i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2*i+1 。
⼆叉 搜索/排序/查找 树:
- 若它的左⼦树不空,则树中任意左⼦树上所有结点的值均⼩于它的根结点的值;
- 若它的右⼦树不空,则树中任意右⼦树上所有结点的值均⼤于它的根结点的值;(仍然是递归的定义方式)
- 对二叉搜索树进行中序遍历可以得到一个从小到大的有序序列。
- 构造这样一颗二叉排序树的目的,当然并不是为了排序,而是为了提高后面反复查找、插删的速度。(结合了数组和链表的优点)
-
二叉查找树有一种极端的存在,二叉树的大部分子节点都比父节点值小,然后导致所有的数据偏向左侧,进而退化成链表,如下图所示:
我们使用二叉树的目的是因为其效率高于链表查询,但这种退化为链表的现象很显然就突兀,怎么办呢。所以为了解决二叉树退化成一棵链表就引入了平衡二叉树。
平衡二叉树:
扩展二叉树:
由于先序、中序和后序序列中的任一个都不能唯一确定一棵二叉树,所以对二叉树做如下处理,将二叉树的空结点用·补齐。这样可以实现仅靠一个序列就确定一棵二叉树。
扩展二叉树的先序和后序序列之一都能唯一确定其二叉树。
三、二叉树相关算法题:
树的基本操作代码及解释:(注意遍历算法中的非递归(使用栈)的代码也要会写。)
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
typedef struct BiTNode {
char data;
struct BiTNode *lChild, *rChild;
} BiTNode, *BiTree;
//先序创建二叉树
void CreateBiTree(BiTree *T) {
char ch;
scanf("%c", &ch);
if (ch == '#') {
*T = NULL;
} else {
*T = (BiTree)malloc(sizeof(BiTNode));
if (!(*T)) {
exit(-1);
}
(*T)->data = ch;
CreateBiTree(&(*T)->lChild);
CreateBiTree(&(*T)->rChild);
}
}
//先序遍历二叉树
void TraverseBiTree(BiTree T) {
if (T == NULL) {
return;
}
printf("%c", T->data);
TraverseBiTree(T->lChild);
TraverseBiTree(T->rChild);
}
//中序遍历二叉树
void InOrderBiTree(BiTree T) {
if (T == NULL) {
return;
}
InOrderBiTree(T->lChild);
printf("%c", T->data);
InOrderBiTree(T->rChild);
}
//后序遍历二叉树
void PostOrderBiTree(BiTree T) {
if (T == NULL) {
return;
}
PostOrderBiTree(T->lChild);
PostOrderBiTree(T->rChild);
printf("%c", T->data);
}
//二叉树的深度
int TreeDeep(BiTree T) {
if (T == NULL)
return 0;
else{
int ldeep = TreeDeep(T->lChild);
int rdeep = TreeDeep(T->rChild);
return ldeep >= rdeep ? ldeep + 1 : rdeep + 1;
}
}
//求二叉树叶子结点个数
int Leafcount(BiTree T, int &num) {
if (T) {
if (T->lChild == NULL && T->rChild == NULL) {
num++;
}
Leafcount(T->lChild, num);
Leafcount(T->rChild, num);
}
return num;
}
//判断二叉树是否为空
bool TreeEmpty(BiTree T) {
if (T == NULL) //bool类型,如果二叉树为空,返回true,否则返回false
return true;
return false;
}
//复制一颗和T完全相同的二叉树
void CopyTree(BiTree T, BiTree &NewT) {
if (T == NULL) {
//递归终止条件
NewT = NULL;
return ;
} else {
NewT = (BiTNode *)malloc(sizeof(BiTNode)); //申请一个根结点
NewT->data = T->data;//复制根结点
CopyTree(T->lChild, NewT->lChild); //递归复制左子树
CopyTree(T->rChild, NewT->rChild); //递归复制右子树
}
}
//销毁一棵二叉树
void destroy(BiTree
T) { //注意一定要最后释放该结点的内存,因为左子树和右子树都是需要该结点中指向左孩子的指针和指向右孩子的指针索引的,释放了该结点的内存就找不到左子树和右子树了,就造成了内存泄漏。
if (T) {
destroy(T->lChild);
destroy(T->rChild);
free(T);
}
}
//主函数
int main(void) {
BiTree T;
BiTree *p = (BiTree *)malloc(sizeof(BiTree));
int deepth, num = 0;
printf("先序构造一棵二叉树(左/右孩子为空用'#'表示):\n");
CreateBiTree(&T);
printf("先序遍历二叉树:\n");
TraverseBiTree(T);
printf("\n");
printf("中序遍历二叉树:\n");
InOrderBiTree(T);
printf("\n");
printf("后序遍历二叉树:\n");
PostOrderBiTree(T);
printf("\n");
deepth = TreeDeep(T);
printf("数的深度为:%d", deepth);
printf("\n");
Leafcount(T, num);
printf("数的叶子结点个数为:%d", num);
printf("\n");
destroy(T);
return 0;
}
//测试用例:AB#CD##E##F#GH###
1 二叉树的四序(前中后层)遍历:
- 二叉树遍历的概念定义本身天然是递归定义。
⼆叉树总体有两类遍历⽅式:1. 深度优先遍历:先往深⾛,遇到叶⼦节点再往回⾛。包括前、中、后序遍历。2. ⼴度优先遍历:⼀层⼀层的去遍历。
***三序(前中后)递归版本-实现:
递归遍历代码是很多二叉树题目的基础!!!
实质上,三种遍历实际的递归过程(走的路径)是一样的,只是 访问/取数据 时机不一样,从递归版本的代码也可以看出。
因此,递归版的代码书写和记忆都非常简单。
94. 二叉树的中序遍历
左 根 右
144. 二叉树的前序遍历
根 左 右
145. 二叉树的后序遍历
左 右 根
三序(前中后)迭代版本-实现:
中序:
前序:
后序(难点):
层序遍历实现:
若树为空,则空操作返回,否则从树的第一层,从上到下,从左到右逐个访问结点。
2 LCR 175. 计算二叉树的深度
解题思路:
求树的深度需要遍历树的所有节点
官方的解法思路更清晰:对递归的掌握更精髓!
111. 二叉树的最小深度
计算二叉树结点数---再次理解递归遍历的精髓!
四 关于二叉树性质的一些计算题
面试题1:
如果一个完全二叉树的结点总数为768个,求叶子结点的个数。
由二叉树的性质知:n0=n2+1,将之带入768=n0+n1+n2中得:768=n1+2n2+1,因为完全二叉树度为1的结点个数要么为0,要么为1,那么就把n1=0或者1都代入公式中,很容易发现n1=1才符合条件。所以算出来n2=383,所以叶子结点个数n0=n2+1=384。
总结规律:如果一棵完全二叉树的结点总数为n,那么叶子结点等于n/2(当n为偶数时)或者(n+1)/2(当n为奇数时)
面试题2:
已知二叉树的前序中序(或者后序中序)遍历序列,求二叉树(或者后序(前序)遍历序列)
- 注意:已知前后序列,不能确定二叉树
- 这种题学会已知的两个序列轮流用