1.堆和二叉树的区别
(1)堆一定是完全二叉树
(2).堆的父节点一定比子节点要大(小),而普通二叉树没有这种要求
2.不同遍历方法
我们二叉树主要遍历方式分为
前序遍历,后序遍历,中序遍历和层序遍历
前序遍历:根->左子树->右子树
后序遍历:左子树->根->右子树
中序遍历:右子树->根左子数
层序遍历 :一层一层去遍历
为了方便理解,我这个地方默认每一个数都有左右节点,没有数据的节点为空
3,前序遍历打印的实现
这个时候二叉树就不能用堆的方法,还是以上图为例子,像上图这种方式怎么实现物理结构和逻辑结构的统一啊?
你会发现这个图你画不出来,堆有严格的要求,而二叉树却不一定是完全二叉树去啊?
那这个地方最好的处理方式就是递归?为啥是递归?
你可以将每个节点都理解成一棵树,每个树有两个子节点(没有值节点就为NULL)
而这两个节点 每个节点 都有 两个子节点,而这两个子节点 每个也有 两个子节点,
这就像俄罗斯套娃一样,像这种结构的动作,我们想到了什么?当然是递归啊!
那我们就来实现一下上面的那个二叉树
这个地方我们一些简单的节点就直接跳过
tree.h
cpp
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int BTDatatype;
typedef struct BinaryTreeNode
{
BTDatatype data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
//申请节点
BTNode* BuyNode(BTDatatype x);
//前序遍历打印
void PreOrder(BTNode* root);
//创造二叉树
BTNode* CreatTree();
手动申请节点
cpp
//申请节点
BTNode* BuyNode(BTDatatype x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
例子的二叉树实现
cpp
BTNode* CreatTree()
{
BTNode* nodel1 = BuyNode(1);
BTNode* nodel2 = BuyNode(3);
BTNode* nodel3 = BuyNode(9);
BTNode* nodel4 = BuyNode(10);
BTNode* nodel5 = BuyNode(2);
BTNode* nodel6 = BuyNode(12);
BTNode* nodel7 = BuyNode(16);
BTNode* nodel8 = BuyNode(17);
nodel1->left = nodel2;
nodel1->right = nodel6;
nodel2->left = nodel3;
nodel2->right = nodel4;
nodel4->left = nodel5;
nodel6->right = nodel7;
nodel7->right = nodel8;
return nodel1;
}
这个地方重点怎么遍历打印?
先思考,什么时候打印不进行下去?
递归非常重要的一点就是我们需要一个停下来的条件
停下来的条件就是再往后没有数据了,也就是NULL,这就是这个地方BuyNode那个函数要
置为NULL的原因之一啊!
然后递归就很好写了
我们先是前序打印,那就是先根,再左节点最后右节点
那我们这个地方我们就可以实现递归了
cpp
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL\n");
return;
}
printf("%d\n", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
光看代码优点难受,那这个地方我们画一个图
下面有两个图,哪个图相对更符合上面二叉树的代码的
图1
图2
显然答案是1,为啥图2不行呢?
第一我们这个地方使用的都是指针,而不是结构体的递归,并且结构体不能递归
为什么?结构体一旦递归,它内存怎么算,同一个类型的结构体的内存大小不一样会很麻烦
传指针不仅我传的变量更小,而且效率更高,指针的大小和类型无关!这样可以保证同一类型的结构体内存大小相同
我们用前序递归来打印一下,看上面写的对不对
很显然没有问题,那么如果我要 中序遍历打印 后序遍历打印 或者层序遍历打印 怎么打印呢?
中序遍历打印 和 后序遍历打印 就直接把 前序打印的这三个 交换一下顺序
printf("%d\n", root->data); //根的打印
PreOrder(root->left); //打印左子树
PreOrder(root->right); //打印右子树
至于遍历打印,后面会单独实现
4.二叉树元素个数
这个地方我们要思考一下怎么去计算二叉树元素个数
方案1:我们首先想到的是函数传参多传一个参数size,每遍历一个size就++
但是这个地方显然有问题就是我们传的是实参,实参的改变不改变形参
而且我们这个地方递归,每一次函数的调用的size不是同一个size
方案2:用static,答案也不行,static修饰的静态变量只能初始化一次,我每一次函数调用都初始化一次,这显然矛盾,而且static修饰的静态变量出了函数无法使用,后续无法更改!
方案3:既然这些都不行,那我们就简单点嘛!我们传指针,但是这个地方也不是最优解,因为我每次调用一次这个函数就得重新创建一个变量和它的地址
我们接下来再思考一下
这个地方其实我们适合用斐波那契数列去类比理解
二叉树和斐波那契数列没有关系,只是类比为了方便理解
这本质上还是递归的思想
那这就很简单了
我们直接操作返回值就可以了
如果是NULL返回值就是0
然后返回值直接返回 左树的返回值+右树的返回值+1(这个1是节点,节点也要算进去)
那就很容易写代码啦
cpp
//二叉树元素个数
int TreeSize(BTNode* root)
{
return root == NULL ? 0 :
TreeSize(root->left)
+ TreeSize(root->right)
+1;
}
5.求树的深度
这个地方也可以用斐波那契数列的思想嘛!
那我们先来看一段代码
cpp
//二叉树的深度
int TreeHeight(BTNode*root)
{
if (root == NULL)
return 0;
return TreeHeight(root->left) > TreeHeight(root->right)
? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
}
这样写代码可不可以?
答案是不可以啊!你这个地方return是调用函数获得返回值,
TreeHeight(root->left) > TreeHeight(root->right)
这个地方就调用了两遍函数,
? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
还要再调用一边函数,函数的重复调用是很恐怖的这里,这里是递归啊,我多每一次调用都要调用函数好多次,直到递归停止啊!
那我们怎么办?
返回值得到就保存起来呗!
没必要每次要用返回值只能通过调用函数得到啊!
cpp
//二叉树的深度
int TreeHeight(BTNode*root)
{
if (root == NULL)
return 0;
int LeftHeight = TreeHeight(root->left);
int RightHeight = TreeHeight(root->left);
return LeftHeight > RightHeight
? LeftHeight : RightHeight;
}
5.树的第k层元素个数
这个地方其实很简单啊!
还是递归方法
树的第k层元素个数
但是这个要注意,我们停下来的条件是啥?
第一是到第k层了
第二就是遇到NULL 返回0;
返回0就算不是第k层遇到,加上0也不会影响第k层元素计算结果
cpp
//求第k层节点个数
int TreeLevel(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
return 0;
if (k == 1)
return 1;
return TreeLevel(root->left, k - 1)
+ TreeLevel(root->right, k - 1);
}