今天看看初阶的二叉树吧,全文10000字!
目录
树的概念及结构
树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的结点,称为根结点,根节点没有前驱结点;
除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继;
因此,树是递归定义的;
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
树的相对概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;
树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
树的实际应用
比如这些文件是不是就是运用的树的结构呢?
二叉树的概念及结构
概念
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
1. 二叉树不存在度大于2的结点
比特科技
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
现实中的二叉树
哈哈哈哈哈哈哈有点憨憨的感觉
特殊的二叉树
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 2^k-1,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
注意完全二叉树不可能有一个节点只有右子树而没有左子树
二叉树的性质
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:log2(n+1)是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否则无右孩子
二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面到高阶数据结构如红黑树等会用到三叉链
二叉树的顺序结构及实现
二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
堆
二叉树链式结构的实现
说明
由于现在的知识储备,我现在还不能手动创建一颗链式二叉树,因此用以下代码创建一棵简易二叉树
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* BinaryTreeCreate()
{
BTNode* node1 = BuyBTNode(1);
BTNode* node2 = BuyBTNode(2);
BTNode* node3 = BuyBTNode(3);
BTNode* node4 = BuyBTNode(4);
BTNode* node5 = BuyBTNode(5);
BTNode* node6 = BuyBTNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
二叉树是:
1. 空树
2. 非空:根节点,根节点的左子树、根节点的右子树组成的
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
敷衍一下
二叉树的遍历
前序、中序、后序遍历
所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。根左右,也叫先根遍历
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。左根右,也叫中根遍历
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。左右根,也叫后根遍历
我们以这个刚刚建的树为例,把NULL的位置也算上,来写一下三种遍历的遍历顺序
前:1 2 3 NULL NULL NULL(2的右树) 4 5 NULL NULL 6 NULL NULL
中:NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL
后:NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1
下面来写一下代码,用递归的话代码是很好写的,就是在理解上较难,这里如果对递归理解不够透彻的同学欢迎再看一下我写的递归总结博客递归总结!
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
因为是前序遍历所以先判断根的情况,如果是NULL直接返回,这也是递归终止的条件,然后再遍历左树右树
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
BinaryTreeInOrder(root->left);
printf("%d ", root->data);
BinaryTreeInOrder(root->right);
}
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%d ", root->data);
}
这里图就不画了,理解以后还是很简单的
层序遍历
实现层序遍历用到了队列先进先出的性质,这里用到了队列的代码,详情可以查看栈和队列
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
比如还是这棵二叉树,我们来分析一下具体过程。
第一步:把根节点先放进队列
第二步:拿出根节点(遍历第一层),同时要把根节点的左右子树依次放进队列
第三步:现在有了第二层的节点在队列中,那么就可以遍历第二层,先拿出2,同时把2的左子树3和右子树(空,不放)放进队列
第四步:拿出4,同时把的左右子树依次放进队列
第五步:接下来剩下的3,5,6都是叶子结点了,也就是说左右子树是空,不需要放进队列了,那么依次拿出
至此,队列为空了,也就是二叉树遍历完毕,而且我们观察顺序,也正好是层序遍历的顺序,因此验证正确
代码的实现:
void BinaryTreeLevelOrder(BTNode* root)
{
Queue a;
QueueInit(&a);
if (root)
{
QueuePush(&a, root);
}
while (!QueueEmpty(&a))
{
BTNode* front = QueueFront(&a);
QueuePop(&a);
printf("%d ", front->data);
if(front->left)
QueuePush(&a,front->left);
if(front->right)
QueuePush(&a,front->right);
}
printf("\n");
QueueDestroy(&a);
}
注意!!!
这里要把上一次队列中Queue.h作以下修改
首先声明以下我们的二叉树结构,然后把二叉树结构作为重定义,而不再是int,这就是重定义的好处,我们只需要修改一处就可以把队列的所有逻辑都更改。
在代码中我们注意当拿出队首元素时记得要删除队首元素,来看一下结果
拿捏
下面来看一个用层序遍历实现的功能(判断二叉树是否为完全二叉树)
bool BinaryTreeComplete(BTNode* root)
{
Queue a;
QueueInit(&a);
if (root)
{
QueuePush(&a, root);
}
while (!QueueEmpty(&a))
{
BTNode* front = QueueFront(&a);
QueuePop(&a);
if (front == NULL)
break;
QueuePush(&a, front->left);
QueuePush(&a, front->right);
}
while (!QueueEmpty)
{
BTNode* front = QueueFront(&a);
QueuePop(&a);
if (front)
return false;
}
return true;
}
只需要作一点点修改,就是在拿出队首元素后,插入元素时不需要进行判断,把空节点也插入队列,然后如果某一次循环拿出的是空,那么退出循环,然后再进行遍历队列,如果空后面还有非空元素,说明是非完全二叉树,如果空后面全都是空,说明是完全二叉树
比如这个非完全二叉树,在遍历第三层的时候3后面有一个空,而后面还有5,6没有遍历
如果我们这样在2的节点右边补一个7节点,那么现在就变成了完全二叉树,在遍历第三层结束后3,7,5,6的左右空节点开始遍历,空节点是连续的,期间没有非空元素,因此是完全二叉树。
接下来是比较好写但比较难理解的无赖恶心之无敌递归
节点个数以及高度
这里要用到分治的思想,也就是把复杂的问题分成小规模的子问题,子问题再分成更小规模的子问题,直到子问题不能再分割,可以直接求出结果。举个例子
先求一下总节点个数
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right)+1;
}
如果根节点为空,则返回0,否则返回左树的节点数+右树的节点数+一个根节点的数目1
下面来看一下求叶节点的个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
如果根是空,直接返回0,如果左右树都是空,那么说明这个节点是叶节点,返回1,如果不是全为空,则返回左树叶节点个数+右树叶节点个数
再来求一下第k层节点的个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k >= 1);
if (root = NULL)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
k=3时,等价于求叶节点
k=2时,
如果根节点为空,返回0,如果k为1,也就相当于只有根节点一个节点,返回1,否则返回左右子树的第k-1层节点数之和,注意是k-1层,因为去掉了根节点这一层
求树的高度
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL)
return 0;
int leftdepth = BinaryTreeDepth(root->left);
int rightdepth = BinaryTreeDepth(root->right);
return leftdepth > rightdepth?leftdepth + 1 : rightdepth + 1;
}
返回左右子树高度中较大的那个+1即可,很ez啦
二叉树oj练习
bool isUnivalTree(struct TreeNode* root){
if(root==NULL)
return true;
if(root->left&&root->val!=root->left->val)
return false;
if(root->right&&root->val!=root->right->val)
return false;
return isUnivalTree(root->left)&&isUnivalTree(root->right);
}
如果是空,符合单值二叉树的条件,返回真,如果根和左右子节点的值不同返回假,如果左右子树也都是单值二叉树返回真。
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
if(p==NULL&&q==NULL)//根都是空返回真
return true;
if(p==NULL||q==NULL)//只有一个根是空返回假
return false;
if(p->val!=q->val)//根不同返回假
return false;
return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);
//根左右两个子树都相同再返回真
}
bool _isSymmetric(struct TreeNode* p, struct TreeNode* q)
{
if(p==NULL&&q==NULL)//根都是空返回真
return true;
if(p==NULL||q==NULL)//只有一个根是空返回假
return false;
if(p->val!=q->val)//根不同返回假
return false;
return _isSymmetric(p->left,q->right)&&_isSymmetric(p->right,q->left);
//根左右两个子树都相同再返回真
}
bool isSymmetric(struct TreeNode* root){
if(root==NULL)
return false;
return _isSymmetric(root->left,root->right);
}
用一下上一题的代码,只要改一下对应相等的是左子树还是右子树就可以了
int TreeSize(struct TreeNode* root)
{
if(root==NULL)
return 0;
return TreeSize(root->left)+TreeSize(root->right)+1;
}
void PreOrder(struct TreeNode* root,int* a,int* i)
{
if(root==NULL)
return;
a[(*i)++]=root->val;
PreOrder(root->left,a,i);
PreOrder(root->right,a,i);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize){
int size=TreeSize(root);
int*a =(int*)malloc(sizeof(int)*size);
*returnSize=size;
int i=0;
PreOrder(root,a,&i);
return a;
}
先算出来长度,开辟好相应空间,再前序遍历放到数组中即可
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
if(p==NULL&&q==NULL)
return true;
if(p==NULL||q==NULL)
return false;
if(p->val!=q->val)
return false;
return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
if(root==NULL)
return false;
return isSameTree(root,subRoot)||
isSubtree(root->left,subRoot)||
isSubtree(root->right,subRoot);
}
还是用上面判断树是否相同的代码,先判断根是否为空
二叉树的创建和销毁
这里通过一个oj题来看一下创建和销毁
#include <stdio.h>
#include <stdlib.h>
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
char data;
}BTNode;
BTNode* BinaryCreatTree(char*s,int* pi)
{
if(s[*pi]=='#')
{
(*pi)++;
return NULL;
}
BTNode* root=(BTNode*)malloc(sizeof(BTNode));
root->data=s[(*pi)++];
root->left=BinaryCreatTree(s,pi);
root->right=BinaryCreatTree(s,pi);
return root;
}
void InOrder(BTNode* root)
{
if(root==NULL)
return;
InOrder(root->left);
printf("%c ",root->data);
InOrder(root->right);
}
void Destroy(BTNode* root)
{
if (root == NULL)
return;
Destroy(root->left);
Destroy(root->right);
free(root);
}
int main()
{
char a[100];
while(~scanf("%s",a))
{
int i=0;
BTNode* root=BinaryCreatTree(a,&i);
InOrder(root);
printf("\n");
Destroy(root);
}
return 0;
}
这个题给出的字符串是按前序输入的,那么我们创建二叉树的时候也要采用前序的方式,而且参数是i的地址,和前面前序遍历一样。
OK!