目录
1. 什么是“树”
树是一种非线性的数据结构,它是由n(n>=0)个有限节点组成一个具有层次关系的集合。把它叫做树是因
为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的节点,称为根节点,根节点没有前驱节点
除根节点外,其余节点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继
因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
1.1 树的相关概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;(日常很少碰到森林,并查集本质就是一个森林)
1.2 树的表示(了解一下)
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存节点和节点之间的关系,如果我们使用C语言来写一个树,由于每个节点的孩子数量是不固定的,那我们如何来初始化每个节点的结构体呢? 前人的智慧给了我们几种方法:
1.C++的写法:顺序表存孩子的指针
// 使用动态数组容器来存储孩子,这样不管孩子有多少都放得下了
struct TreeNode
{
int data;
vector<struct TreeNode*>childs;
}
2.C语言写法:左孩子右兄弟
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子节点
struct Node* _pNextBrother; // 指向其下一个兄弟节点
DataType _data; // 节点中的数据域
}
2. 二叉树概念及结构
(注意,实际中普通二叉树增删查改没有意义,实际中有意义的是“搜索二叉树”)
2.1 概念
一棵二叉树是节点的一个有限集合,该集或者为空,或者由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
二叉树的特点:
1. 每个节点最多有两棵子树,即二叉树不存在度大于2的节点。
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
2.2 特殊的二叉树
满二叉树:一个二叉树,如果每一个层的节点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且节点总数是 ,则它就是满二叉树。
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个节点的二叉树,当且仅当其每一个节点都与深度为K的满二叉树中编号从1至n的节点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
2.3 二叉树的性质
(完全二叉树中若父亲在数组中下标为n,则孩子下标为2n+1和2n+2)
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个节点。
2. 若规定根节点的层数为1,则深度为h的二叉树的最大节点数是2^h - 1。
3. 对任何一棵二叉树, 如果度为0其叶节点个数为n0 , 度为2的分支节点个数为n2 ,则有n0 = n2 +1 。
4. 若规定根节点的层数为1,具有n个节点的满二叉树的深度,h= log2(n+1) (ps:是log以2为底,n+1为对数)
5. 对于具有n个节点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的节点有:
1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
2.4 存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
顺序存储的完全二叉树就是一个“堆”,关于“堆”可以看我另一篇文章。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出该节点左孩子和右孩子所在的链节点的存储地址。
链式结构又分为二叉链和三叉链,一般都是使用二叉链,高阶数据结构如红黑树等会用到三叉链。
2.5 二叉树的遍历与计算
要处理二叉树问题,就要先遍历二叉树。
需要记住,任何一棵二叉树分为三个部分:
1.根节点
2.左子树
3.右子树
在这里说这个是为了下文的算法做铺垫:
分治算法:
分而治之,大问题分成类似子问题,子问题再分成子问题...直到子问题不可再分割。
这个算法是不是有些眼熟?没错,就是递归。
要搞明白这里的递归,我们需要先学习一下“前序、中序、后序”。
2.5.1 前序、中序、后序、层序
所谓的“前序、中序、后序”是一种递归结构遍历:(本质算深度优先遍历)
前序遍历(Preorder Traversal)——访问根节点、访问左子树、访问右子树。
中序遍历(Inorder Traversal)——访问左子树、访问根节点、访问右子树。
后序遍历(Postorder Traversal)——访问左子树、访问右子树、访问根节点
以遍历上图二叉树为例: 前序遍历:A B D NULL NULL E NULL NULL C NULL NULL
中序遍历:NULL D NULL B NULL E NULL A NULL C NULL
后序遍历:NULL NULL D NULL NULL E B NULL NULL C A
(通过前序和中序遍历得到的结果,我们可以还原出一棵树)
(通过后序和中序遍历得到的结果,我们可以还原出一棵树)
(但是前序和后序遍历得到的结果无法还原一棵树,需要通过中序把左右子树分割开)
了解完了理论部分,我们就可以开始代码实现了。
前、中、后序遍历代码实现
#include <stdio.h>
#include <stdlib.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode_t;
/* 前序访问 */
void PrevOrder(BTNode_t* root)
{
if (NULL == root) // 空树不需要访问
{
printf("NULL ");
return;
}
printf("%c ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
/* 中序访问 */
void InOrder(BTNode_t* root)
{
if (NULL == root) // 空树不需要访问
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
/* 后序访问 */
void PostOrder(BTNode_t* root)
{
if (NULL == root) // 空树不需要访问
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
int main()
{
// 初始化树的节点
BTNode_t* A = (BTNode_t*)malloc(sizeof(BTNode_t));
A->data = 'A';
A->left = NULL;
A->right = NULL;
BTNode_t* B = (BTNode_t*)malloc(sizeof(BTNode_t));
B->data = 'B';
B->left = NULL;
B->right = NULL;
BTNode_t* C = (BTNode_t*)malloc(sizeof(BTNode_t));
C->data = 'C';
C->left = NULL;
C->right = NULL;
BTNode_t* D = (BTNode_t*)malloc(sizeof(BTNode_t));
D->data = 'D';
D->left = NULL;
D->right = NULL;
BTNode_t* E = (BTNode_t*)malloc(sizeof(BTNode_t));
E->data = 'E';
E->left = NULL;
E->right = NULL;
// 连接树的节点
A->left = B;
A->right = C;
B->left = D;
B->right = E;
// 前序
PrevOrder(A);
printf("\n");
// 中序
InOrder(A);
printf("\n");
// 后序
PostOrder(A);
printf("\n");
return 0;
}
代码中构造的树:
代码效果:(依次为前序、中序、后序)
2.5.2 计算二叉树节点数量
第一种方法:遍历计数
/* 求二叉树节点个数 */
void TreeSize(BTNode_t* root, int* psize)
{
// 思路:遍历计数
if (NULL == root)
{
return;
}
else
{
(*psize)++;
}
TreeSize(root->left,psize);
TreeSize(root->right,psize);
}
/* 调用计算 */
int Asize = 0;
TreeSize(A, &Asize);
printf("TreeSize:%d\n", Asize);
第二种方法:采用分治的思想计算节点数量
/* 求二叉树节点个数 */
int TreeSize(BTNode_t* root)
{
return NULL == root ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
/* 调用计算 */
int Asize = 0;
Asize = TreeSize(A);
printf("TreeSize:%d\n", Asize);
2.5.3 变式:计算叶子节点的数量
/* 求叶子节点个数 */
int TreeLeafSize(BTNode_t* root)
{
// 如果我是空
if (NULL == root)
return 0;
// 如果我是叶子节点
if (NULL == root->left && NULL == root->right)
return 1;
// 如果我不是空也不是叶子节点
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
2.5.4 二叉树第k层节点个数
思路和计算叶子节点个数的思路相似,从根开始,每下一层k-1,当k=1时说明该节点就是第k层的节点。
/* 二叉树第k层节点个数 */
int BinaryTreeLevelKSize(BTNode_t* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->left,k-1) + BinaryTreeLevelKSize(root->right,k-1);
}
2.5.5 二叉树查找值为x的节点
/* 二叉树查找值为x的节点 */
BTNode_t* BinaryTreeFind(BTNode_t* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
// 前序查找
if (root->data == x)
{
return root;
}
BTNode_t* left = BinaryTreeFind(root->left, x);
BTNode_t* right = BinaryTreeFind(root->right, x);
if (left != NULL)
return left;
if (right != NULL)
return right;
return NULL;
}
2.5.6 销毁二叉树
/* 销毁二叉树 */
void DestroyTree(BTNode_t* root)
{
if (root == NULL)
return;
// 后序销毁
DestroyTree(root->left));
DestroyTree(root->right);
free(root);
// 如果需要防止野指针,就传二级指针,将*root置空
}
2.5.7 层序遍历(广度优先遍历)
这种遍历方法借助队列的先进先出特性,核心思路是“上一层带下一层”。
创建一个队列,将根节点放入队列,若不为空,则出队列后将不为空的子节点放入队列,然后子节点出队列,将不为空的子子节点放入队列。每次循环出队列一个节点,直至队列为空。
/* 二叉树的层序遍历 */
void LevelOrder(BTNode_t* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q)) // 队列不为空时一直循环
{
BTNode_t* front = QueueFront(&q); // 取出数据
QueuePop(&q); // pop掉该节点
printf("%c ", front->data);
if (front->left) // 若左子树不为空,则入队列
{
QueuePush(&q, front->left);
}
if (front->right) // 若右子树不为空,则入队列
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestroy(&q); // 销毁队列,释放空间
}
(注:本文不赘述队列的增删查改,且以上代码都是在.c.h文件中编写)
打印效果:
2.5.8 判断二叉树是否为完全二叉树
思路:层序遍历二叉树,当队列头部第一次遇到NULL时停止pop,然后开始检查后续队列元素是否全为NULL,如果全为NULL则说明该二叉树是完全二叉树,否则不是。
/* 判断二叉树是否是完全二叉树 */
/* 是返回1,不是返回0 */
int BianryTreeComplete(BTDataType* root)
{
Queue q;
QueueInit(&q);
if (root == NULL)
return 1;
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode_t* front = QueueFront(&q);
QueuePop(&q);
// 遇到NULL直接break
if (front == NULL)
break;
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
// 检查后续队列是否全为NULL,如果不是全为NULL,则不是完全二叉树
while (!QueueEmpty(&q))
{
BTNode_t* front = QueueFront(&q);
QueuePop(&q);
if (front != NULL)
{
QueueDestroy(&q);
return 0;
}
}
QueueDestroy(&q);
return 1;
}
【提一嘴】搜索二叉树
对于搜索二叉树中的任何一棵树,左子树都比根要小,右子树都比根要大。
在搜索二叉树中查找一个树,最多查找“高度”次。
时间复杂度:O(N)
【其他】二叉树相关常见OJ题及解析
关于二叉树常见经典OJ我总结了一篇博客,学完二叉树可以练一下手:数据结构:二叉树经典OJ题与OJ题解析-CSDN博客