我们在前两篇博客中主要介绍了堆及其应用,针对的对象堆是完全二叉树,存储方式采用顺序结构存储的方式。
那么好的,这篇博客我们浅谈二叉树的链式存储,针对的对象是二叉树,并不局限于完全二叉树了!
我们先来回顾以下二叉树的定义:
说简单点呢,二叉树是一颗特殊的树,这颗树的度最大为2,就像是对这颗树的节点进行了计划生育,最多只能生两个节点宝宝。
从概念中可以看出,二叉树定义是递归式的:二叉树被拆成根节点、左子树和右子树,子树又被拆成根节点、左子树和右子树……直到拆成空树停止。因此后序基本操作中基本都是按照该概念实现的。
讲二叉树的链式存储,鼠鼠我需要构建一颗链式二叉树,鼠鼠先用硬编码的方式构建一颗二叉树,真正的链式二叉树构建是用递归构建的,鼠鼠后面讲:
typedef char BTDataType;
typedef struct BTNode
{
BTDataType data;
struct BTNode* leftchild;
struct BTNode* rightchild;
}BTNode;
//动态申请二叉树节点
BTNode* CreateBinaryTreeNode(BTDataType data)
{
BTNode* tmp = (BTNode*)malloc(sizeof(BTNode));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
tmp->data = data;
tmp->leftchild = NULL;
tmp->rightchild = NULL;
return tmp;
}
int main()
{
BTNode* node1 = CreateBinaryTreeNode('A');
BTNode* node2 = CreateBinaryTreeNode('B');
BTNode* node3 = CreateBinaryTreeNode('C');
BTNode* node4 = CreateBinaryTreeNode('D');
BTNode* node5 = CreateBinaryTreeNode('E');
BTNode* node6 = CreateBinaryTreeNode('F');
BTNode* node7 = CreateBinaryTreeNode('G');
BTNode* node8 = CreateBinaryTreeNode('H');
BTNode* node9 = CreateBinaryTreeNode('I');
node1->leftchild = node2;
node1->rightchild = node6;
node2->leftchild = node3;
node3->leftchild = node4;
node4->rightchild = node5;
node6->leftchild = node7;
node7->leftchild = node8;
node8->rightchild = node9;
return 0;
}
对于链式二叉树来说是由一个个节点构成的,我们定义节点如BTNode所示,很简单就不解释了。用也是很简单的逻辑我们写出了动态申请二叉树节点这个函数,在main函数上我们就可以调用该函数构建任意我们想要的链式二叉树,如代码所示我们构建的链式二叉树想象图为:
1.二叉树的遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
1.1.前序遍历、中序遍历和后序遍历
二叉树的递归结构遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal 亦称先序遍历):访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal):访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal):访问根结点的操作发生在遍历其左右子树之后。
鼠鼠在这里顺便提一嘴,前序遍历是一种深度优先遍历(dfs)。
1.2.层序遍历
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
其实不难,这里我们不用递归实现,借用之前实现的队列即可。主要思想是 先将节点A的地址入队列,这样节点A地址就在对头了,再读取对头数据(即节点A地址)并将节点A地址出队列,通过读取的对头数据(即节点A地址)将A节点的左右子节点地址依次入队列(若为空指针就不入队列)。这样节点B的地址就在对头了,再读取对头数据(即节点B地址)并将节点B地址出队列,通过读取的对头数据(即节点B地址)将B节点的左右子节点地址依次入队列(若为空指针就不入队列)。这样节点F地址就在对头了,再……循环直到队列为空就停止这样就实现了层序遍历。
说通俗点就是A带B和F,B带C,F带G,C带D,G带H,D带E,H带I,当I出队列时队列就空了,那么就遍历完了!
我们看上面运行结果来说是将所有节点数据全部打印到一行上了,那我们如何将所有节点数据分层打印出来呢?就是说第一行打印A;第二行打印B、F ;第三行打印C、G;第四行打印D、H;第五行打印E、I。
鼠鼠在这里顺便提一嘴,层序遍历是一种广度优先遍历(bfs)。
2.二叉树节点个数
那我们该如何求呢?
当然我们可以用层序遍历,每出一个数据就计数一次,但是鼠鼠这里用的是递归的写法,思想是:当传入空树(就是传入节点指针为空指针)就返回0;不为空树就返回左子树节点个数+右子树节点个数+1即可。
3.二叉树叶子节点个数
递归的方法很简单,如果传入空树(就是传入的节点指针是空指针)就返回0;如果传入的树没有左右子节点那么返回1;返回左子树叶子节点个数+右子树叶子节点个数即可。基于这个思想,代码如下:
//二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->leftchild == NULL && root->rightchild == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->leftchild) + BinaryTreeLeafSize(root->rightchild);
}
4.二叉树第k层节点个数
5.二叉树高度
6.二叉树查找值为x的节点
找值为x的节点很简单,递归写法来说就是:如果传入空树,返回空指针:如果根节点就是值为x的节点,我们返回这个节点的地址;如果根节点的值不是x,我们在左子树找,找到返回,找不到就去右子树找,找到返回,找不到就返回空指针。
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* find1 = BinaryTreeFind(root->leftchild, x);
if (find1)
{
return find1;
}
BTNode* find2 = BinaryTreeFind(root->rightchild, x);
if (find2)
{
return find2;
}
return NULL;
}
7.通过前序遍历的数组构建二叉树, 叶节点子节点用'#'表示,如"ABD##E#H##CF##G##"
好的呀,我们前面讲过链式二叉树一般不用硬编码的方式构建,一般用递归构建,那么鼠鼠现在就来浅浅介绍一下!
这里的意思就是写一个用递归实现的函数,该函数能实现输入一组二叉树前序遍历得到的数组就能将这棵二叉树构建出来并返回二叉树指针。
8. 判断二叉树是否是完全二叉树
9.二叉树销毁
10.二叉树的性质
最后鼠鼠介绍一个二叉树的性质,挺重要的:对任何一棵二叉树, 如果度为0其叶结点个数为n, 度为2的分支结点个数为k,则有n=k+1。
比如这道题就用到了这个性质:
因为是二叉树,那么二叉树节点的度只可能为0、1和2。那么设度为0的节点(即叶子节点)个数为X,设度为1的节点个数为Y,设度为2的节点个数为Z,那么X+Y+Z=X+Y+(X-1)=2X+Y-1=2n。
又因为是完全二叉树,那么度为1的节点个数要么是0个,要么是1个。当Y=0时,不可能满足2Z+Y+1=2n,因为2n时偶数;所有Y肯定为1,那么代入2X+1-1=2n即可得出X=n。
感谢阅读,欢迎斧正!