二叉树的认识以及基础操作
在前面我们学习了堆的时候就接触过了二叉树的基础知识
现在我们进一步认识二叉树
二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。
1.二叉树的性质
性质1:二叉树的第i层上至多有2i-1(i≥1)个节点。
性质2:深度为h的二叉树中至多含有2h-1个节点。
性质3:若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1。
性质4:具有n个节点的满二叉树深为log2n+1。
性质5:若对一棵有n个节点的完全二叉树进行顺序编号(1≤i≤n),那么,对于编号为i(i≥1)的节点:
当i=1时,该节点为根,它无双亲节点。
当i>1时,该节点的双亲节点的编号为i/2。
若2i≤n,则有编号为2i的左节点,否则没有左节点。
若2i+1≤n,则有编号为2i+1的右节点,否则没有右节点。
二叉树介绍链接: link
有了上面的性质我们就可以处理一些书本上的二叉树相关知识的题目。
2.二叉树的基本操作
下面来介绍二叉树的基本操作:
这里二叉树的操作基本都是使用递归的方法
这里我们先构建一下二叉树,代码如下:
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* BuyNode(int x)
{
BTNode* TreeNode = (BTNode*)malloc(sizeof(BTNode));
if (TreeNode == NULL)
{
perror("malloc fail");
return NULL;
}
TreeNode->data = x;
TreeNode->left = NULL;
TreeNode->right = NULL;
return TreeNode;
}
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode * node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
BTNode* node7 = BuyNode(7);
BTNode* node8 = BuyNode(8);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
node5->left = node7;
node7->left = node8;
return node1;
}
这里会生成如图这样的树,这样构建树的原因是,本人的水平不够…
a.二叉树的遍历
二叉树的遍历分为四种:前序,中序,后序,以及层序。这四种遍历的方式是:
前序:根节点,左子树,右子树
中序:左子树,根节点,右子树
后序:左子树,右子树,根节点
层序:顾名思义,一层一层的遍历树
这四种遍历方法分别适用不同的场景,到时需要自行判断,在一般的编程题中大多是前序。
首先是前序:在递归的过程中,我们首先要将所有的二叉树简化为,根节点,左子树和右子树。那在上面图中的前序遍历就是先遍历到我们的1,然后是以2为根节点的左子树,然后是以4为根节点的右子树,代码如下:
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
中序和后序也和前序思想差不多,代码如下:
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
二叉树的节点个数
根据上面的三种遍历,我们可以对上面的代码进一步修改得到获取二叉树节点数的代码,这个使用前中后序都可以实现,代码如下:
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return BinaryTreeSize(root->left)
+ BinaryTreeSize(root->right)
+ 1;
}
这里的return 后面的就是二叉树的前中后序遍历,其中有左右子树递归,还有个1是根节点,而这个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);
}
当遍历到叶子节点也就是第二个return时返回1,空节点返回0
否则则是分支节点继续递归,这里是一个前序遍历。
下面我们再来引申二叉树第k层节点个数这里就需要额外的参数来确定我们递归到了二叉树的第几层即可,代码如下:
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
也是一个前序遍历。
下面有一个函数也是跟节点查找有关,查找二叉树中值为x的节点,这个函数那我们看到首先它需要返回一个地址,那我们在递归时就需要将他的地址给带回,假如我们递归遍历找到了那个值,我们返回那个值的话,那就需要上一层函数有个变量来接收,否则会丢失了这个地址,说明我们在写这个函数时候需要创建一个局部变量ret,先判断当前节点是不是要找的节点,不是的话递归左子树然后递归右子树,将ret初始化为空指针,如果三个方向有一个带回来的是一个地址的话就代表有这样一个值,ret则在往后的过程中不需要在改变值了,需要一个条件判断,代码如下:
BTNode* BTreeFind(BTNode* root, BTDataType x)
{
BTNode* ret = NULL;
if (root == NULL)
return NULL;
if (root->data == x)
{
return root;
}
if (ret = BTreeFind(root->left, x))
return ret;
if (ret = BTreeFind(root->right, x))
return ret;
return ret;
}
也是一个前序遍历。
二叉树的层序遍历
层序遍历他是借助于队列这个数据结构来实现的,思路就是,当弹出一个节点时带出他的两个孩子节点(非空),这里顺序表的实现可以看这条链接链接: link,代码如下:
void BinaryTreeLevelOrder(BTNode* root)
{
Queue qu;
QueueInit(&qu);
QueuePush(&qu, root);
while (!QueueEmpty(&qu))
{
BTNode* front = QueueFront(&qu);
printf("%d ", front->data);
if (front->left)
{
QueuePush(&qu, front->left);
}
if (front->right)
{
QueuePush(&qu, front->right);
}
QueuePop(&qu);
}
QueueDestroy(&qu);
}
判断一棵树是否是完全二叉树
思路也是用层序遍历,当队列中弹出空指针后(这里的层序遍历会将空指针插入队列中),跳出循环,开始新循环直到队列中没有数据(包括空指针),当过程中有不是空指针的,那说明不是完全二叉树。代码如下:
bool BinaryTreeComplete(BTNode* root)
{
Queue qu;
QueueInit(&qu);
QueuePush(&qu, root);
while (!QueueEmpty(&qu))
{
BTNode* front = QueueFront(&qu);
QueuePop(&qu);
if (front == NULL)
{
break;
}
QueuePush(&qu, front->left);
QueuePush(&qu, front->right);
}
while (!QueueEmpty(&qu))
{
BTNode* front = QueueFront(&qu);
if (front)
{
QueueDestroy(&qu);
return false;
}
}
QueueDestroy(&qu);
return true;
}
二叉树的最大深度
代码如下:
int maxDepth(BTNode* root)
{
if (root == NULL)
return 0;
int left = maxDepth(root->left);
int right = maxDepth(root->right);
return left > right ? left + 1 : right + 1;
}
这里需要注意的是,需要使用变量来记录左右子树递归返回的数据,如果代码是这样是错误的,会大大加大代码的运算量。
链接: link,这是牛客网的一道题,让编写一个程序构建,题目中所给的前序遍历得到的序列构建二叉树,还是用一个前序遍历,加一个指向下标的指针(这里是指针的原因是,局部变量出了作用域就销毁,而我们需要一个变量来遍历数组,就需要用一级指针来实现)代码如下:
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
if(*pi < n)
{
if (*(a + *pi) == '#')
{
++(*pi);
return NULL;
}
BTNode* node = BuyNode(*(a + *pi));
++(*pi);
node->left = BinaryTreeCreate(a, n, pi);
node->right = BinaryTreeCreate(a, n, pi);
return node;
}
}
最后还有个二叉树的销毁,用的是后序遍历(为的是递归到孩子节点后,在销毁当前节点)。代码如下:
void BinaryTreeDestory(BTNode** root)
{
assert(root);
if ((*root) == NULL)
return;
BinaryTreeDestory(&(*root)->left);
BinaryTreeDestory(&(*root)->right);
(*root)->left = NULL;
(*root)->right = NULL;
free(*root);
*root = NULL;
}
这里也可以使用结构体的一级指针,需要使用函数的人,自己将维护二叉树的指针设置为空指针。
这就是二叉树的基础的相关操作,要是有不对的地方望指正。