链式二叉树

简介: 链式二叉树

一、结构体变量的声明


typedef char BTDataType;
typedef struct BinaryTreeNode
{
  struct BinaryTreeNode* left;
  struct BinaryTreeNode* right;
  BTDataType val;
}BTNode;


二、 通过前序遍历的数组构建二叉树


给定你一个通过前序遍历字符数组你将如何把它构建成一颗二叉树呢(字符串中‘#’代表NULL)?


这里最好的方法肯定是递归,因为这个是前序遍历的数组,是典型的DFS(深度优先),而深度优先最适合的方法就是递归。


BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
  assert(a);
  //如果遇到‘#’,那说明遇到NULL了,pi指向的下标需要++
  //跳过这个字符‘#’,然后返回一颗NULL树
  if (a[*pi] == '#')
  {
    (*pi)++;
    return NULL;
  }
  //动态开辟一颗子树
  BTNode* root = (BTNode*)malloc(sizeof(BTNode));
  if (root == NULL)
  {
    perror("malloc fail");
    return NULL;
  }
  //给这棵树赋值数组对应的字符
  root->val = a[*pi];
  (*pi)++;
  //再递归构造棵树的左子树和右子树
  root->left = BinaryTreeCreate(a, pi);
  root->right = BinaryTreeCreate(a, pi);
  //最后返回这颗树
  return root;
}


三、二叉树的销毁


void BinaryTreeDestory(BTNode* root)
{
  //如果遇到空树就返回
  if (root == NULL)
  {
    return;
  }
  //递归销毁这棵树的左子树和右子树
  BinaryTreeDestory(root->left);
  BinaryTreeDestory(root->right);
  //最后把这棵树的根销毁
  free(root);
  root = NULL;
}


四、统计二叉树结点的个数


int BinaryTreeSize(BTNode* root)
{
  //如果遇到空树,就返回0
  if (root == NULL)
  {
    return 0;
  }
  //否则就返回左子树的结点数+右子树的结点数+自己本身
  return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}


五、统计二叉树叶子节点(无左右孩子的节点)的个数


int BinaryTreeLeafSize(BTNode* root)
{
  //如果遇到空树就返回,那证明后面就没有子树了,也就没有叶子节点,返回0
  if (root == NULL)
  {
    return 0;
  }
  //如果这棵树的左子树和右子树都为NULL,那证明这个结点就是叶子节点(根据叶子节点的定义),返回1
  if (root->left == NULL && root->right == NULL)
  {
    return 1;
  }
  //否则就返回左子树的叶子节点的个数+右子树叶子节点的个数
  else
  {
    return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
  }
}


六、求二叉树第K层的节点的个数


这个函数的实现思路是:求一颗二叉树的第K层的节点的个数可以看作是求左右子树的第K-1层的节点的个数。,这也是一种分治的思想,可以用递归来实现。


int BinaryTreeLevelKSize(BTNode* root, int k)
{
  //如果遇到空树,那就证明要么这一层就是所求的第K层的,要么就是
  //还没递归到第K层就出现了空树,无论是哪一种情况都说明这一层和
  //后面都不会有位于第K层的节点了,所以返回0
  //
  if (root == NULL)
  {
    return 0;
  }
  //如果K递减到,说明这一个跟节点就位于第K层,返回1
  if (k == 1)
  {
    return 1;
  }
  //否则就返回左子树的第K-1层的节点+右子树第K-1层的节点树
  return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}


七、查找值为x的节点并返回节点的地址


BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
  //如果遇到空树就证明这一颗子树找不到x
  if (root == NULL)
  {
    return NULL;
  }
  //如果这棵树的根节点的值就是查找的x,就返回这个根节点
  //返回上一层调用这个函数的栈帧
  if (root->val == x)
  {
    return root;
  }
  //如果根节点不是要找的,就递归到左子树查找x
  BTNode* leftFind = BinaryTreeFind(root->left, x);
  //找到了就返回
  if (leftFind)
  {
    return leftFind;
  }
  //左子树找不到就递归到右子树查找
  BTNode* rightFind = BinaryTreeFind(root->right, x);
  //找到了就返回
  if (rightFind)
  {
    return rightFind;
  }
  //如果根不是,左子树找不到,右子树找不到,那就不可能找到这个值了
  //这里返回NULL是返回到上一层调用这个函数的栈帧中,并不是直接得出
  //结果,等到返回到第一层调用这个函数的栈帧后依然找不到这个值才是
  //真正的在这一整棵树都找不到这个值,建议话递归展开图理解
  return NULL;
  // 不能写成
  // return BinaryTreeFind(root->left, x) || BinaryTreeFind(root->right, x);
  // 因为这里要求的是返回节点的地址,
  //“||”是逻辑运算符,只能得到逻辑结果,即真或者加(0为假,非0为真)
  // 所以这样写的话直接返回的逻辑结果是布尔值,并不符合要求
  //不建议写成
  /*return BinaryTreeFind(root->left, x)
    ? BinaryTreeFind(root->left, x)
    : BinaryTreeFind(root->right, x);*/
    //因为如果在左子树找到了这个节点,但是由于没有保存
    //导致返回的时候又要再找一遍,如果这棵树的深度越深,
    //那么再找一遍的话越往下的子树呗反复调用的次数就越多
    //大大降低了效率
}


八、二叉树的遍历


二叉树的前中后序遍历其实都是一样的递归思路的,只不过访问节点的时间不同。三个遍历都是按照一下这张图片这样走的。



8.1 前序遍历


先访问根节点,再递归访问左子树,最后递归访问右子树。


void BinaryTreePrevOrder(BTNode* root)
{
  //如果遇到空树就打印一个‘#’,再返回,
  // 这样能够更加直观地反映这棵树的真实的形状
  if (root == NULL)
  {
    printf("%c ",'#');
    return;
  }
  //否则先访问它的根节点,再递归访问它的左子树,最后递归访问它的右子树(这里的访问是打印)
  printf("%c ", root->val);
  BinaryTreePrevOrder(root->left);
  BinaryTreePrevOrder(root->right);
}


8.2 中序遍历


和前序遍历是一样的道理,只是访问根节点的时机不同,先递归访问左子树,再访问根节点,最后递归访问右子树。


void BinaryTreeInOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("%c ", '#');
    return;
  }
  BinaryTreeInOrder(root->left);
  printf("%c ", root->val);
  BinaryTreeInOrder(root->right);
}


8.3 后序遍历


先递归访问左子树,再访问右子树,最后递归访问根节点。


void BinaryTreePostOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("%c ", '#');
    return;
  }
  BinaryTreePostOrder(root->left);
  BinaryTreePostOrder(root->right);
  printf("%c ", root->val);
}


8.4 层序遍历


这个层序遍历就是一层一层的往下遍历,直到叶子节点。



这里的层序遍历是典型的BFS(广度优先)。


这里我们需要用到一个队列来协助这里的遍历才能使问题更加简化一些。


思路:首先是根节点先进队列,访问这个根节点,然后根节点出队列,在节点出队列的同时判断这个节点的左孩子节点是否为空,不为空就入队列,为空则不入,再判断右孩子节点是否为空,不为空就入队列,否则不入,循环以上的动作(根节点出队列的同时把左右孩子(不为空)的带进队列)当这个队列为空的时候,就证明这棵树遍历完了。



void BinaryTreeLevelOrder(BTNode* root)
{
  //如果传过来的是空树,那就直接返回了
  if (root == NULL)
  {
    return;
  }
  //否则创建一个队列
  Queue q;
  //初始化队列
  QueueInit(&q);
  //先把根节点入队列
  QueuePush(&q, root);
  //当队列为空的时候循环就结束了
  while (!QueueEmpty(&q))
  {
    //取队列头的元素
    BTNode* front = QueueFront(&q);
    //打印代表访问
    printf("%c ", front->val);
    //如果根节点的左孩子不为空,就把左孩子带进队列
    if (front->left)
    {
      QueuePush(&q, front->left);
    }
    //如果右孩子不为空,就把右孩子带进队列
    if (front->right)
    {
      QueuePush(&q, front->right);
    }
    //队列头的元素出队列
    QueuePop(&q);
  }
  //销毁队列
  QueueDestroy(&q);
}


九、判断一棵树是否是完全二叉树


这里要判断一棵树是否为完全二叉树,首先清楚什么叫完全二叉树,完全二叉树其实是所有的有效节点(不为NULL的节点)都是连续不间断的二叉树就是完全二叉树。





通过观察以上的几棵树可发现,完全二叉树的所有的有效节点一定是连续的,我们就可以利用这个作为突破点来判断一颗树是否为完全二叉树了。


其实你会发现,这个判断一棵树是否为完全二叉树完全可以沿用二叉树的层序遍历的方法,因为完全二叉树的全部有效节点都是连续的,那么如果我们把二叉树的层序遍历的代码稍微修改一下,就是无论根节点的左子树和右子树是否为空都把它入队列,那么节点出队列的时候一旦遇到了NULL。如果队列中的这个空节点往后的节点中再出现有效节点(即不为NULL的节点),那么这棵树一定不是完全二叉树(由二叉树的性质决定的),如果队列一直出节点,直到队列为空也没有出现有效节点,那么这棵树就一定是完全二叉树。




bool BinaryTreeComplete(BTNode* root)
{
  //如果是空树,那么也认为是完全二叉树
  if (root == NULL)
  {
    return true;
  }
  //创建一个队列
  Queue q;
  QueueInit(&q);
  QueuePush(&q, root);
  while (!QueueEmpty(&q))
  {
    //取队列头的元素
    BTNode* front = QueueFront(&q);
    //队头元素出队列
    QueuePop(&q);
    //只要front不是NULL,就进它的左右孩子
    if (front)
    {
      QueuePush(&q, front->left);
      QueuePush(&q, front->right);
    }
    //遇到空就可以跳出循环去判断是否为完全二叉树了
    else
    {
      break;
    }
  }
  //只要队列不为空就一直判断是否存在有效节点
  while (!QueueEmpty(&q))
  {
    BTNode* front = QueueFront(&q);
    QueuePop(&q);
    //如果遇到不为空的节点,即有效节点,就判定不是完全二叉树
    //返回false
    if (front)
    {
      QueueDestroy(&q);
      return false;
    }
  }
  //来到这里证明队列为空都没找到有效节点,证明该树是完全二叉树
  QueueDestroy(&q);
  return true;
}


相关文章
|
15天前
|
人工智能 自然语言处理 安全
妙妙妙妙!公文、合同、标书…全妙闭环了
阿里云百炼|全妙,是面向政企、媒体等专业领域的智能创作平台,集“妙策、妙搜、妙笔、妙读”于一体,覆盖公文撰写、合同审查、标书生成、内容采编等高合规场景,助力用户降本增效,释放创造力。
197 25
|
9月前
|
人工智能 JSON Serverless
阿里云AI剧本生成与动画创作解决方案深度评测
阿里云AI剧本动画全链路解决方案基于函数计算FC、百炼大模型和ComfyUI技术架构,实现从剧本生成到动画渲染的自动化流程。方案在电商广告、知识科普等快速批产场景表现出色,大幅缩短创作时间(如30秒动画从9.5小时减至16.1分钟)。然而,在强剧情连续性和物理规则方面存在不足,建议结合人工审核优化。测试显示其商用级成熟度,推荐采用“AI初稿-人工润色”模式。
767 138
阿里云AI剧本生成与动画创作解决方案深度评测
|
4月前
|
人工智能 缓存 API
8大AI记忆优化策略助你突破智能体上下文限制
本文深入解析AI系统中的记忆管理策略,涵盖8种主流方案及工程实现,助你突破上下文限制,构建高效智能体。
840 0
|
7月前
|
前端开发 API 开发工具
一年撸完百万行代码,企业微信的全新鸿蒙NEXT客户端架构演进之路
本文将要分享的是企业微信的鸿蒙Next客户端架构的演进过程,面对代码移植和API不稳定的挑战,提出了DataList框架解决方案。通过结构化、动态和认知三重熵减机制,将业务逻辑与UI解耦,实现数据驱动开发。采用MVDM分层架构(业务实体层、逻辑层、UI数据层、表示层),屏蔽系统差异,确保业务代码稳定。
355 0
|
9月前
|
人工智能 Java 程序员
一文彻底搞定电阻元件
电阻元件是限流器件,通过其电流与两端电压成正比(V=IR),阻值受温度、材料等影响。按特性分为线性与非线性,材料上有碳膜、金属膜等,用途涵盖限流、分压、偏置、滤波等。标称阻值有允许偏差,额定功率和最高工作电压需注意。色标法和直接读取法可用于识别阻值,万用表测量时需关闭电源并选择合适量程。电阻在电路设计中不可或缺,掌握其特性和应用对电子工程师至关重要。
689 0
一文彻底搞定电阻元件
|
安全 网络安全 数据安全/隐私保护
Wi-Fi 保护访问(WPA)详解
【4月更文挑战第22天】
864 0
Wi-Fi 保护访问(WPA)详解
WK
|
数据安全/隐私保护
QTextEdit
QTextEdit是Qt框架中的高级文本编辑控件,支持富文本格式、图像、列表和表格的插入,优化处理大型文档,支持HTML和Markdown格式,提供段落和字符级别的格式控制,以及占位文本提示。常用成员函数包括设置和获取文本内容、文本格式设置等。QTextEdit还提供了多种信号和丰富的交互功能,适用于需要处理复杂文本的应用场景。
WK
444 1
|
XML JavaScript 数据格式
什么是 DOM?
DOM,即文档对象模型,是W3C制定的访问HTML和XML文档的标准,允许程序动态访问和更新文档的内容、结构和样式。它分为核心DOM、XML DOM和HTML DOM三部分,分别针对不同类型的文档提供标准化的操作接口。
|
监控 前端开发 安全
RPA机器人的工作原理?
【8月更文挑战第4天】RPA机器人的工作原理?
654 1
|
数据库 开发者 Python
"揭秘FastAPI异步编程魔法:解锁高性能Web应用的终极奥义,让你的并发处理能力飙升,秒杀同行就靠这一招!"
【8月更文挑战第31天】FastAPI是一款基于Python的现代化Web框架,内置异步编程支持,可充分利用多核CPU的并行处理能力,大幅提升Web应用的性能。本文探讨FastAPI的异步编程特性,通过示例代码展示其在处理并发请求时的优势。异步编程不仅提高了并发处理能力,还降低了资源消耗,使代码更简洁易读。无论对于初创企业还是大型企业级应用,FastAPI都是构建高性能Web服务的理想选择。
497 0