【数据结构】初等二叉树(下)

简介: 【数据结构】初等二叉树(下)

二叉树的叶子个数


求二叉树的叶子个数也是采用分治思想,求 该树的左子树的叶子个数加上该树的右子树的叶子个数,然后该树的左子树和右子树又可以分为 求 该树的左子树的叶子个数加上该树的右子树的叶子个数,依此继续细分,最终计算完后返回的就是每棵子树的叶子结点个数。


判断是否是叶子结点只需判断该结点的左孩子和右孩子是不是都为NULL即可,如果是就返回1。


当然,如果该结点为NULL,就直接返回0。


图示理解:

5d0c75f7b888475a8e9b8db90879e21c.gif


颜色出现的先后顺序可以看作递归的过程,上树的叶子结点为3

相关函数接口代码实现:

// 二叉树的叶子个数
int TreeLeafSize(BTNode* root)
{
  // 如果该结点为 NULL , 说明不是叶子结点,直接返回 0
  if (root == NULL) return 0;
  // 如果该结点的左孩子和右孩子都为 NULL ,说明该结点为叶子结点,返回 1
  if (root->left == NULL && root->right == NULL) return 1;
  // 递归左子树和右子树,统计左右子树的叶子结点
  return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}


求出二叉树的高度


求二叉树的高度也是采用分治思想:先求出该树的左子树的高度,再求出该树的右子树高度,然后比较得到高度高的那个加上自己(就是加一)。该树的左子树和右子树又是这样的一个操作:先求出该树的左子树的高度,再求出该树的右子树高度,然后比较得到高度高的那个加上自己(就是加一)。依次细分,最终递归返回得到的就是整棵树的高度(深度)。


图示理解:


5d4b6ac93ec34c079cfc186623c2cfd8.png

相关函数接口代码实现:

// 二叉树的高度(深度)
int TreeHeight(BTNode* root)
{
  // 分治思想
  // 如果该节点为空,没有高度,返回0
  if (root == NULL) return 0;
  // 递归统计左右子树的高度
  int leftheight = TreeHeight(root->left);
  int rightheight = TreeHeight(root->right);
  // 返回左右子树高度大小大的那个并加一,加一是加上该结点的高度1
  return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}


第K层的结点个数


求第K层的结点个数,如果K是1,就是根节点,答案为1;如果K是2,就是第二层的结点个数,此时我们在递归的时候,每向下递归一层,就把K视为K - 1一次,所以到达第二层的时候,K为1。由上不难发现,当K为1所在的层就是需要统计结点个数的层。因此有了判断:当该结点所在的层数有 K == 1 ,返回1。如果在第K层前或在第K层遇到NULL就返回0。


同样的,这里采用递归,分别递归左和右去寻找处在第K层的结点。


图示理解:


b5bb6fb1d6c749409cb9b7a827f2f686.gif

相关函数接口代码实现:

// 二叉树第K层的结点个数
int TKLevelNodeSize(BTNode* root, int k)
{
  // 如果到达第K层之前或者到达第K层该结点为NULL,直接返回0
  //(说明不是第K层的结点或者在第K层为NULL)
  if (root == NULL) return 0;
  // 如果k减到1了,说明到达第K层了,此时该结点有效,应计数1
  if (k == 1) return 1;
  return TKLevelNodeSize(root->left, k - 1) + TKLevelNodeSize(root->right, k - 1);
}


查找值为X的结点


查找值为X的结点,先在该树的左子树找,如果找到了就直接返回,如果找不到,再到该树的右子树去找,如果右子树找到了也直接返回,如果找不到,左右子树都没有,就返回NULL。


同样的,将上一条继续细分,这样递归下去就只有找到和找不到两种情况,如果找到了就返回找到的那个结点的指针,如果找不到就返回NULL。


图示理解:

58ec2a3d0bc345d98068ae054264c3a0.gif


af908c18dbd948968e67d99890251610.png




相关函数接口代码实现:

// 二叉树结点数据的查找
BTNode* TreeDataFind(BTNode* root, BTDataType x)
{
  // 如果该结点为NULL, 返回NULL
  if (root == NULL) return NULL;
  // 找到了就返回指向该结点的指针
  if (root->data == x) return root;
  // 先在左边找
  BTNode* lret = TreeDataFind(root->left, x);
  // 如果寻找不到返回NULL,这里if就不进去
  if (lret) return lret;
  // 如果左没找到就找右
  BTNode* rret = TreeDataFind(root->right, x);
  // 如果寻找不到返会NULL,这里if就不进去
  if (rret) return rret;
  // 如果左右都没找到就返回NULL
  return NULL;
}


二叉树的层序遍历


层序遍历就是一层一层的从左到右打印结点的数据,这里递归就办不到了,那么如何来实现呢?


首先我们需要一个队列,这个队列存储的数据是树结点的指针,我们先将根结点入队列(如果根结点为NULL就不入),然后取队头数据存下来(front) 并 出队列操作,接着打印front指向的结点的数据(front->data),最后将front指向的结点的左孩子和右孩子分别入队列(如果为NULL就不入)。整个操作是一个循环,当队列为空时结束遍历。


队列的代码可以直接从之前写的队列那一章拷贝过来(这里就不放了):-> 队列传送门 <-


图示理解:



699a554b73be44aeb2c708eab0829298.gif


相关函数接口代码实现:

// 层序遍历,利用队列来操作
void LevelOrder(BTNode* root)
{
  // 队列的每一个数据是一个树的结点
  Que q;
  QInit(&q);
  // 如果根节点不为NULL就入队列
  if (root) QPush(&q, root);
  // 队列不为空就继续
  while (!QEmpty(&q))
  {
    // 取队头元素(树的结点)
    BTNode* front = QFront(&q);
    // 出队列是释放队头的空间,在队头存放的树的结点没有被释放
    QPop(&q);
    // 打印树结点中的数据
    printf("%d ", front->data);
    // 如果该队头树结点的左孩子不为空就入队列
    if (front->left) QPush(&q, front->left);
    // 如果该对头树节点的右孩子不为空就入队列
    if (front->right) QPush(&q, front->right);
  }
  printf("\n");
  // 记得销毁队列噢,防止内存泄漏
  QDestroy(&q);
}


是否为完全二叉树


  • 判断一棵二叉树是否为完全二叉树,首先要知道什么是完全二叉树,前面 树的介绍 这一章详细讲解了完全二叉树,这里就不做过多探讨。
  • 我们需要知道的是,一棵完全二叉树,我们以层序的视角去看待它,会发现它是连续的:


60acdefba22d44b581f01edb43163b2f.png


所以根据此性质,这里可以运用层序遍历的思想,用一个队列来解决。


但与层序遍历不同的是,这里的入队列,是NULL也入,每次出队列就同时入队列这个出队列的数据的左孩子和右孩子,当出队列的这个树结点的指针为NULL的时候,说明前面的连续的有效结点组成的树是一颗完全二叉树,此时跳出循环。


跳出循环后,此时队列里面还有数据,如果队列里的数据全是NULL就说明这是一棵完全二叉树,如果队列里的数据有一个不是NULL,它就不是一棵完全二叉树。


图示理解:

30626b3ec2744a93b9d44892064e5ed2.png



相关函数接口代码实现:

// 判断该二叉树是否为完全二叉树
bool BTComplate(BTNode* root)
{
  Que q;
  QInit(&q);
  // 如果root不为NULL就入队列
  if (root) QPush(&q, root);
  while (!QEmpty(&q))
  {
    // 取队头元素,就是一个树的结点
    BTNode* front = QFront(&q);
    QPop(&q);
    // 如果当前结点是NULL,说明前面每层连续的有效结点都出队列了,直接跳出循环
    if (front == NULL) break;
    // 入front结点的左孩子和右孩子,NULL也入
    QPush(&q, front->left);
    QPush(&q, front->right);
  }
  // 如果队列不为NULL,接下来就是判断的重要一步了
  // 根据完全二叉树的性质,当前面每层连续的有效结点都走完时
  // 剩下队列里的元素如果都是NULL就说明该二叉树是一棵完全二叉树
  // 如果有一个结点不为NULL,那么就说明该二叉树不是完全二叉树
  while (!QEmpty(&q))
  {
    BTNode* front = QFront(&q);
    QPop(&q);
    if (front)
    {
      // 如果找到一个有效结点,就说明该二叉树不是完全二叉树,直接返回false
      // 返回前记得销毁队列噢
      QDestroy(&q);
      return false;
    }
  }
  // 如果前面循环走完,说明符合完全二叉树的性质,最后返回true
  // 返回前记得销毁队列噢
  QDestroy(&q);
  return true;
}


关于二叉树的销毁


  • 二叉树的销毁也是一个递归的过层,这里采用的是后序遍历来依次销毁,因为如果采用前序或中序的话,递归就回不去了。

图示理解:


f0aa5585c4314fdfa4289cecbac01563.gif

相关函数接口代码实现:

// 二叉树的销毁
// 注意一定是要通过后续遍历来销毁
// 因为后序遍历是 左 右 根 ,销毁左右再销毁根
// 这样能够递归返回回去
void BTDestroy(BTNode* root)
{
  if (root == NULL) return;
  BTDestroy(root->left);
  BTDestroy(root->right);
  // 释放(返还)root指向的结点的空间
  free(root);
}


完整的二叉树代码


▶️队列的代码可以直接从之前写的队列那一章拷贝过来(这里就不放了):->队列传送门<-。注意:拷贝过来后,要改一下队列存放的数据的类型(改为一个指向树的结点的指针)。


初等二叉树的完整代码BinaryTree.c


// 引入队列头文件之后,因为队列头文件里有下面的库函数包含,所以这里就不必重复包含了
//#include <stdio.h>
//#include <assert.h>
//#include <stdlib.h>
//#include <stdbool.h>
#include "queue.h"
typedef int BTDataType;
typedef struct TreeNode
{
  // 存储数据
  BTDataType data;
  // 指向左结点的指针
  struct TreeNode* left;
  // 指向右结点的指针
  struct TreeNode* right;
}BTNode;
// 得到一个结点
BTNode* BuyTreeNode(BTDataType x)
{
  BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
  // 判断一下是否申请空间失败
  assert(newnode);
  newnode->data = x;
  // 两个指针都初始化 NULL
  newnode->left = NULL;
  newnode->right = NULL;
  // 返回指向这个结点空间的指针
  return newnode;
}
// 自行建一棵树,可随意建树,但必须要是二叉树
BTNode* BuyTree()
{
  // 依次得到一个结点
  BTNode* n1 = BuyTreeNode(1);
  BTNode* n2 = BuyTreeNode(2);
  BTNode* n3 = BuyTreeNode(3);
  BTNode* n4 = BuyTreeNode(4);
  BTNode* n5 = BuyTreeNode(5);
  BTNode* n6 = BuyTreeNode(6);
  BTNode* n7 = BuyTreeNode(7);
  // 通过每个结点内的指针进行树的连接
  n1->left = n2;
  n2->left = n3;
  n1->right = n4;
  n4->left = n5;
  n4->right = n6;
  n2->right = n7;
  // 返回所有结点的祖先结点
  return n1;
}
// 二叉树的前序遍历
void PreOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  // 根 左 右
  printf("%d ", root->data);
  PreOrder(root->left);
  PreOrder(root->right);
}
// 二叉树的中序遍历
void InOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  // 左 根 右
  InOrder(root->left);
  printf("%d ", root->data);
  InOrder(root->right);
}
// 二叉树的后序遍历
void PostOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  // 左 右 根
  PostOrder(root->left);
  PostOrder(root->right);
  printf("%d ", root->data);
}
// 二叉树的结点个数
int TreeNodeSize(BTNode* root)
{
  // +1 是算上自己为一个结点
  return root == NULL ? 0 : TreeNodeSize(root->left) + TreeNodeSize(root->right) + 1;
}
// 二叉树的叶子个数
int TreeLeafSize(BTNode* root)
{
  // 如果该结点为 NULL , 说明不是叶子结点,直接返回 0
  if (root == NULL) return 0;
  // 如果该结点的左孩子和右孩子都为 NULL ,说明该结点为叶子结点,返回 1
  if (root->left == NULL && root->right == NULL) return 1;
  // 递归左子树和右子树,统计左右子树的叶子结点
  return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
// 二叉树的高度(深度)
int TreeHeight(BTNode* root)
{
  // 分治思想
  // 如果该节点为空,没有高度,返回0
  if (root == NULL) return 0;
  // 递归统计左右子树的高度
  int leftheight = TreeHeight(root->left);
  int rightheight = TreeHeight(root->right);
  // 返回左右子树高度大小大的那个并加一,加一是加上该结点的高度1
  return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}
// 二叉树第K层的结点个数
int TKLevelNodeSize(BTNode* root, int k)
{
  // 如果到达第K层之前或者到达第K层该结点为NULL,直接返回0
  //(说明不是第K层的结点或者在第K层为NULL)
  if (root == NULL) return 0;
  // 如果k减到1了,说明到达第K层了,此时该结点有效,应计数1
  if (k == 1) return 1;
  return TKLevelNodeSize(root->left, k - 1) + TKLevelNodeSize(root->right, k - 1);
}
// 二叉树结点数据的查找
BTNode* TreeDataFind(BTNode* root, BTDataType x)
{
  if (root == NULL) return NULL;
  if (root->data == x) return root;
  BTNode* lret = TreeDataFind(root->left, x);
  if (lret) return lret;
  BTNode* rret = TreeDataFind(root->right, x);
  if (rret) return rret;
  return NULL;
}
// 层序遍历,利用队列来操作
void LevelOrder(BTNode* root)
{
  // 队列的每一个数据是一个树的结点
  Que q;
  QInit(&q);
  // 如果根节点不为NULL就入队列
  if (root) QPush(&q, root);
  // 队列不为空就继续
  while (!QEmpty(&q))
  {
    // 取队头元素(树的结点)
    BTNode* front = QFront(&q);
    // 出队列是释放队头的空间,在队头存放的树的结点没有被释放
    QPop(&q);
    // 打印树结点中的数据
    printf("%d ", front->data);
    // 如果该队头树结点的左孩子不为空就入队列
    if (front->left) QPush(&q, front->left);
    // 如果该对头树节点的右孩子不为空就入队列
    if (front->right) QPush(&q, front->right);
  }
  printf("\n");
  // 记得销毁队列噢,防止内存泄漏
  QDestroy(&q);
}
// 判断该二叉树是否为完全二叉树
bool BTComplate(BTNode* root)
{
  Que q;
  QInit(&q);
  // 如果root不为NULL就入队列
  if (root) QPush(&q, root);
  while (!QEmpty(&q))
  {
    // 取队头元素,就是一个树的结点
    BTNode* front = QFront(&q);
    QPop(&q);
    // 如果当前结点是NULL,说明前面每层连续的有效结点都出队列了,直接跳出循环
    if (front == NULL) break;
    // 入front结点的左孩子和右孩子,NULL也入
    QPush(&q, front->left);
    QPush(&q, front->right);
  }
  // 如果队列不为NULL,接下来就是判断的重要一步了
  // 根据完全二叉树的性质,当前面每层连续的有效结点都走完时
  // 剩下队列里的元素如果都是NULL就说明该二叉树是一棵完全二叉树
  // 如果有一个结点不为NULL,那么就说明该二叉树不是完全二叉树
  while (!QEmpty(&q))
  {
    BTNode* front = QFront(&q);
    QPop(&q);
    if (front)
    {
      // 如果找到一个有效结点,就说明该二叉树不是完全二叉树,直接返回false
      // 返回前记得销毁队列噢
      QDestroy(&q);
      return false;
    }
  }
  // 如果前面循环走完,说明符合完全二叉树的性质,最后返回true
  // 返回前记得销毁队列噢
  QDestroy(&q);
  return true;
}
// 二叉树的销毁
// 注意一定是要通过后续遍历来销毁
// 因为后序遍历是 左 右 根 ,销毁左右再销毁根
// 这样能够递归返回回去
void BTDestroy(BTNode* root)
{
  if (root == NULL) return;
  BTDestroy(root->left);
  BTDestroy(root->right);
  // 释放(返还)root指向的结点的空间
  free(root);
}
int main()
{
  BTNode* root = BuyTree();
  PreOrder(root);
  printf("\n");
  InOrder(root);
  printf("\n");
  PostOrder(root);
  printf("\n");
  printf("TreeNodeSize = %d\n", TreeNodeSize(root));
  printf("TreeLeafSize = %d\n", TreeLeafSize(root));
  printf("TreeHeight = %d\n", TreeHeight(root));
  printf("TreeKNodeSize = %d\n", TKLevelNodeSize(root, 3));
  printf("TreeFindData = %d\n", TreeDataFind(root, 6)->data);
  LevelOrder(root);
  if (BTComplate(root)) printf("Full binary tree!\n");
  else printf("Not full binary tree!\n");
  BTDestroy(root);
  return 0;
}


☑️写在最后


💝回顾上文,其中的分治思想值得我们探讨。普通二叉树学完之后,初阶的数据结构就告一段落了,整体来说,初阶数据结构还是不难的,掌握其之后,我相信应付学校的考试那是绰绰有余。后面我会给大家带来高阶数据结构的文章,大家准备好接招吧嘿嘿!

❤️‍🔥后续将会继续输出有关数据结构与算法的文章,你们的支持就是我写作的最大动力!


感谢阅读本小白的博客,错误的地方请严厉指出噢~

相关文章
|
23天前
|
算法
【算法与数据结构】二叉树(前中后)序遍历2
【算法与数据结构】二叉树(前中后)序遍历
|
8天前
二叉树和数据结构
二叉树和数据结构
16 0
|
9天前
|
算法 DataX
二叉树(中)+Leetcode每日一题——“数据结构与算法”“剑指Offer55-I. 二叉树的深度”“100.相同的树”“965.单值二叉树”
二叉树(中)+Leetcode每日一题——“数据结构与算法”“剑指Offer55-I. 二叉树的深度”“100.相同的树”“965.单值二叉树”
|
19天前
|
算法 索引
【算法与数据结构】深入二叉树实现超详解(全源码优化)
【算法与数据结构】深入二叉树实现超详解(全源码优化)
|
19天前
|
存储 算法
【算法与数据结构】深入解析二叉树(二)之堆结构实现
【算法与数据结构】深入解析二叉树(二)之堆结构实现
|
30天前
|
存储 算法 程序员
【数据结构】【版本2.0】【树形深渊】——二叉树入侵
【数据结构】【版本2.0】【树形深渊】——二叉树入侵
|
1月前
|
算法 C++ 开发者
【C/C++ 数据结构 】二叉树基本性质:具有n个结点的完全二叉树的深度为[log2n]+1或者[log2(n+1)]...
【C/C++ 数据结构 】二叉树基本性质:具有n个结点的完全二叉树的深度为[log2n]+1或者[log2(n+1)]...
12 0
|
1月前
|
存储 算法 C语言
【C/C++ 数据结构 树】探索C/C++中的二叉树:从理论到实践
【C/C++ 数据结构 树】探索C/C++中的二叉树:从理论到实践
60 0
|
1月前
|
存储 算法 Python
数据结构与算法——二叉树介绍(附代码)
数据结构与算法——二叉树介绍(附代码)
22 3
|
存储 算法
【链式二叉树】数据结构链式二叉树的(万字详解)
【链式二叉树】数据结构链式二叉树的(万字详解)