二叉树的常见操作

简介: 二叉树的常见操作

二叉树的常见操作

注:二叉树的结构如下:

typedef char BinaryTreeDataType;
typedef struct BinaryTreeNode
{
  struct BinaryTreeNode* left;
  struct BinaryTreeNode* right;
  BinaryTreeDataType data;
}BTNode;

以下所有操作都以下面的树为例:

1. 求各类节点个数

1.1. 求二叉树的节点个数

错误写法1

int TreeSize(BTNode* root)
{
  static int size = 0;    //定义静态变量(也可以是全局变量),确保每一次调用函数size不会清零
  if (!root)
    return size;
  //利用前序遍历的方式计算总结点数
  size++;         
  TreeSize(root->left);
  TreeSize(root->right);
  return size;
}

计算一棵树时好像结果也没有问题:

但计算两棵树的总结点时,却会出现以下情况:

我们可以看到B这棵树的节点数确实是3,但A这棵树的节点怎么就为8了呢。原来,我们将size定义为了静态变量,当计算完B这棵树的节点再调用函数计算A树的结点树,size的大小已经为3了,这样最后得到的size就为8,显然错误。

错误写法2

那,我们将size变为全局变量并每次调用函数前将size归零不就行了吗?

int size = 0;
int TreeSize(BTNode* root)
{
   if (!root)
    return size;
   size++;
   TreeSize(root->left);
   TreeSize(root->right);
   return size;
}

可以得到如下结果:

这么做好像是可以,但如果考虑到多线程的情况,如果两个线程同时调用这个函数,那size的值必定会被改变,因此将size定义为静态变量是行不通的

正确写法

遍历思想
void TreeSize(BTNode* root,int *size)   //因为要对size的值进行修改,因此要传地址
{
   if (!root)
    return;
   (*size)++;
   TreeSize(root->left,size);
   TreeSize(root->right,size);
   return;
}

可以发现,结果正确:

分治思想
  • 将大问题分解成多个小问题。举个例子:树A的左子树是树B,右子树是树E,此时size为3,树B的左子树是树C,右子树是树D,此时size为5,C、D、E的左右子树都为空树,因此size为5
  • 采用了后序遍历的思想,先求左右子树的节点个数,再求根节点的节点个数
int TreeSize(BTNode* root)
{
  return  root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
    //如果为空树,则返回0,如果不为空树,则先对左子树进行递归操作,再对右子树进行递归操作,递归完成后,返回1,因为这棵树不是空树
}

基本过程:

递归分解图

1.2 求二叉树叶子节点的个数

叶子节点即度为零的节点,满足的条件即:

assert(root);
root->left == NULL && root->right == NULL;

知道了这一基本常识,我们就可以利用后序遍历的思想来求得一棵二叉树叶子节点的个数了:

int BinaryTreeLeafSize(BTNode* root)
{
  if (root == NULL)
    return 0;
  return (root->left == NULL && root->right == NULL) ? 1 : BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

基本过程:

1.3 求第k层节点的个数

我们将整数k也纳入函数形参,则函数原型为:

int BinaryTreeLevelKSize(BTNode* root, int k)

这里的第k层,即相较于根节点的第k层;即相较于第二层的(k-1)层;即相较于第三层的(k-2)层;即相较于第n层的(k-n+1)层,n为当前的层数。因此,当k-n+1 = 1,即k = n时,第k层节点的个数就是第n层节点的个数。

所以,要得到第k层节点的个数,只需要利用递归的思想,每一次递归都将k减一,当k == 1时,即到达第k层,节点数加1。

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);
}

2. 通过前序遍历序列和#号创建二叉树

题录描述:

编写一个函数,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树。例如如下的先序遍历字符串:ABC##DE#G##F###其中“#”表示空格,空格字符代表空树NULL

由于要利用一个字符串数组来构造一棵二叉树,为了能够方便读取数组的元素,作为函数参数,除了要将字符串传入,还应该传入一个指针,方便定位数组位置:

BTNode* BinaryTreeCreate(BTDataType* a, int* pi)

这里我们要用前序遍历的思想,先创建二叉树的根节点,再递归创建二叉树的左右子树:

BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
  if (a[*pi] == '#')
  { 
    (*pi)++;
    return NULL;
  }
    //创建节点
  BTNode* root = (BTNode*)malloc(sizeof(BTNode));
  if (NULL == root)
  {
    perror("malloc");
    exit(-1);
  }
  root->data = a[*pi];
  (*pi)++;
  root->left = BinaryTreeCreate(a, pi);
  root->right = BinaryTreeCreate(a, pi);
  return root;
}

基本过程:

3. 查找二叉树值为x的节点,并返回该节点

利用前序遍历的思想,先判断根节点的的值是否为x,如果是,则直接返回根节点即可,否则再遍历左右子树继续查找

需要注意,利用递归查找左右子树是否有满足条件的节点时,应该对返回值进行记录,如果找到了直接返回找到的节点即可,不用再继续遍历。

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
    //先判断根节点
  if (root == NULL)
    return NULL;
  if (root->data == x)
    return root;
  BTNode* ret = NULL; //用来记录返回值
    //判断左子树是否有符合条件的节点
  ret = BinaryTreeFind(root->left, x);
  if (ret)  //如果找到了,直接返回
    return ret;
    //判断右子树是否有符合条件的节点
  ret = BinaryTreeFind(root->right, x);
  if (ret)  //如果找到了,直接返回
    return ret;
    //如果都没找到,那么返回空
    return NULL;
}

4. 判断一棵树是否为完全二叉树

我们先来看一棵完全二叉树和一棵非完全二叉树:

我们再来看看这两棵树的层序遍历序列:

完全二叉树:A B E C D NULL NULL NULL NULL NULL NULL

非完全二叉树:A B E NULL D NULL NULL NULL NULL

可以发现完全二叉树的层序遍历序列的空树都是连续的,而非完全二叉树的层序遍历序列的空树不是连续的,我们可以利用这一性质来判断一棵树是不是完全二叉树

注:如果对于二叉树的层序遍历操作不太了解,建议先看看👉二叉树层序遍历

/*
  QueueInit 表示初始化队列
  QueuePush 表示入队
  QueuePop 表示出队
  QueueEmpty 表示判断队列是否为空,如果为空返回true
  QueueFront 表示返回队头元素
*/
bool BinaryTreeComplete(BTNode* root)
{
  if (root == NULL)
    return true;
  Queue* q = (Queue*)malloc(sizeof(Queue));
  QueueInit(q);
  QueuePush(q, root);
    //层序遍历队列的节点
  while (!QueueEmpty(q))
  {
    BTNode* front = QueueFront(q);
    QueuePop(q);
        //如果此时队头元素为空,那么就可以退出循环,看队列剩余元素是否含有非空节点
    if (front == NULL)
      break;
    QueuePush(q, front->left);
    QueuePush(q, front->right);
  }
    //看队列剩余元素是否含有非空节点
  while (!QueueEmpty(q))
  {
    BTNode* front = QueueFront(q);
    QueuePop(q);
      //如果含有非空节点,就说明不是完全二叉树
    if (front != NULL)
    {
            //销毁队列
      QueueDestroy(q);
      return false;
    }
  }
  QueueDestroy(q);
  return true;
}


相关文章
|
存储 算法
数据结构— —栈的基本操作(顺序栈和链栈)
数据结构— —栈的基本操作(顺序栈和链栈)
|
9月前
|
自然语言处理 监控 物联网
大模型微调参数设置 —— 从入门到精通的调参指南
本文系统解析大模型微调核心参数:学习率、批次大小、训练轮次、权重衰减、LoRA秩等的作用机制与设置技巧,结合LLaMA-Factory实战演示,帮助初学者避开“黑箱”误区,在有限算力下实现高效、稳定微调。
|
10月前
|
JavaScript
JeecgBoot介绍
JeecgBoot是一款基于代码生成器的低代码开发平台,支持零代码快速开发。采用SpringBoot2.x、Ant Design&Vue前后端分离架构,集成Mybatis-plus、Shiro、JWT等技术,高效构建企业级应用。官网:http://www.jeecg.com/
235 1
|
缓存 算法 调度
数据结构之 - 双端队列数据结构详解: 从基础到实现
数据结构之 - 双端队列数据结构详解: 从基础到实现
1062 5
|
JavaScript 前端开发 UED
深入理解JavaScript中的节流与防抖技术
理解并合理运用节流与防抖技术,可以帮助我们优化事件处理函数的执行频率,从而提升应用的性能和用户体验。这两种技术通过减少不必要的计算和DOM操作,使得Web应用程序能够更加流畅地运行。 通过掌握防抖和节流的实现原理及应用场景,开发者可以更加灵活地编写高效且性能优化的代码,对于面对高频事件处理时尤其重要。在开发中合理选择使用防抖或节流,将直接影响到应用的响应性和效率。
507 1
|
Java 数据库
【YashanDB知识库】kettle同步大表提示java内存溢出
在数据导入导出场景中,使用Kettle进行大表数据同步时出现“ERROR:could not create the java virtual machine!”问题,原因为Java内存溢出。解决方法包括:1) 编辑Spoon.bat增大JVM堆内存至2GB;2) 优化Kettle转换流程,如调整批量大小、精简步骤;3) 合理设置并行线程数(PARALLELISM参数)。此问题影响所有版本,需根据实际需求调整相关参数以避免内存不足。
|
存储
数据结构:图文详解单链表的各种操作(头插法,尾插法,任意位置插入,删除节点,查询节点,求链表的长度,清空链表)
数据结构:图文详解单链表的各种操作(头插法,尾插法,任意位置插入,删除节点,查询节点,求链表的长度,清空链表)
3431 0
|
监控 调度
队列的深度解析:链式队列的实现
队列的深度解析:链式队列的实现
|
存储 弹性计算 网络协议
阿里云服务器经济型e、通用算力型u1与c7/g7/r7/c8y/g8y/r8y实例区别及选择参考
在阿里云目前的各个活动中,除了轻量应用服务器之外,活动内的云服务器实例规格主要以经济型e、通用算力型u1、计算型c7/c8y、通用型g7/g8y、内存型r7/r8y这几个实例规格为主,c7/c8y属于计算型实例,g7/g8y属于通用型实例,c7/r8y属于内存型实例,c7/g7/r7属于最新第七代云服务器实例,c8y/g8y/r8y属于倚天云服务器实例,不同的云服务器实例规格在性能、特点及适用场景上有所不同,本文大家介绍一下阿里云服务器经济型e、通用算力型u1与c7/g7/r7/c8y/g8y/r8y的区别,以供参考。
阿里云服务器经济型e、通用算力型u1与c7/g7/r7/c8y/g8y/r8y实例区别及选择参考