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

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

二叉树的叶子个数


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


判断是否是叶子结点只需判断该结点的左孩子和右孩子是不是都为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;
}


☑️写在最后


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

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


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

相关文章
|
26天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
87 8
|
2月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
24 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
2月前
|
存储 算法 搜索推荐
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
这篇文章主要介绍了顺序存储二叉树和线索化二叉树的概念、特点、实现方式以及应用场景。
27 0
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
|
2月前
|
Java
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
28 1
|
2月前
|
算法 Java C语言
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
25 1
|
2月前
|
存储
【数据结构】二叉树链式结构——感受递归的暴力美学
【数据结构】二叉树链式结构——感受递归的暴力美学
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
2月前
|
存储 算法
数据结构与算法学习十六:树的知识、二叉树、二叉树的遍历(前序、中序、后序、层次)、二叉树的查找(前序、中序、后序、层次)、二叉树的删除
这篇文章主要介绍了树和二叉树的基础知识,包括树的存储方式、二叉树的定义、遍历方法(前序、中序、后序、层次遍历),以及二叉树的查找和删除操作。
28 0
|
2月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
2月前
|
存储
【数据结构】二叉树零基础无压力上手,超详解
【数据结构】二叉树零基础无压力上手,超详解
30 0