二叉树万字详解(遍历+oj)(上)

简介: 二叉树万字详解(遍历+oj)

一、二叉树的三种遍历


1.1前序遍历


对于二叉树的链式结构,常用的三种遍历方式是


1、前序遍历(PreOrder)--访问根节点的操作发生在遍历其左右子树之前。


2、中序遍历(InOrder)--访问根节点的操作发生在遍历其左右子树之间。


3、后序遍历(PostOrder)--访问根节点的操作发生在遍历其左右子树之后。


前序遍历,简单来说就是以根-->左子树-->右子树的形式访问遍历。也就是说:在访问任何一个节点时,都是根的优先级最高,优先访问根,然后是左子树,右子树。以递归的形式访问遍历。


注意:


可能我说的还是优点抽象,以图为例:


1669252158966.jpg


比如这样一个二叉树,在进行前序遍历的时候,可能有读者误解为这样遍历:


先访问1,然后访问它的左子树2,然后再访问1的右子树4,然后再访问3,以此类推。这里特别提醒,这样是错误的,原因有二:


1、虽然好像这样满足我说的前序遍历--根-->左子树-->右子树,但是这是不符合递归的核心思想的,虽然2作为1的左子树,但是同时它还是3的根,以根-->左子树-->右子树访问,根的优先级最高的话,应是先访问本身作为根的2的左子树3,然后3又作为根去访问它的左子树,直到遇到NULL返回。


2、还有一个致命的问题,就是访问完4之后如何找到2的左子树3呢?因此这样是不合理的。


对于上面这个二叉树,正确的遍历应该是:


1669252169263.jpg


通过这个相对潦草的图解,我们可以发现这样访问:


无论从整体还是局部上满足根-->左子树-->右子树的访问。


我们要写它的递归的话,满足这样一个规律:当从根访问到左子树,左子树本身又作为它的左右子树的根,它再访问它的左子树,当访问到NULL,就去访问右子树,而右子树本身也是作为它的左右子树的根,去访问它的左子树,以此类推,而这,就是前序遍历递归的本质,中序遍历和后序遍历与之类似。


代码实现:


我先把构造上面的树的代码放在下面,以便使用:


typedef int BTDataType;
typedef struct BTNode
{
  BTDataType data;
  struct BTNode* left;
  struct BTNode* right;
}BTNode;
BTNode* CreateBinaryTree()
{
  BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
  assert(n1);
  BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
  assert(n2);
  BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
  assert(n3);
  BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
  assert(n4);
  BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
  assert(n5);
  BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
  assert(n6);
  n1->data = 1;
  n2->data = 2;
  n3->data = 3;
  n4->data = 4;
  n5->data = 5;
  n6->data = 6;
  n1->left = n2;
  n1->right = n4;
  n2->left = n3;
  n2->right = NULL;
  n4->left = n5;
  n4->right = n6;
  n3->left = NULL;
  n3->right = NULL;
  n5->left = NULL;
  n5->right = NULL;
  n6->left = NULL;
  n6->right = NULL;
  return n1;
}


前序遍历代码:


void PreOrder(BTNode* root)//前序遍历
{
  if (root == NULL)
  {
  printf("NULL ");
  return;
  }
  printf("%d ", root->data);
  PreOrder(root->left);
  PreOrder(root->right);
}
int main()
{
  BTNode* root= CreateBinaryTree();
  PreOrder(root);//前序遍历
    printf("\n");
    return 0;
}


1669252198645.jpg


遍历运行的结果也是和我们所推测的一样。同时也可以看到,这个递归写起来也是比较简单的。


1.2中序遍历


中序遍历,就是以左子树-->根-->右子树的形式访问遍历。也就是说:在访问任何一个节点时,都是左子树的优先级最高,优先访问左子树,然后是根,右子树。以递归的形式访问遍历。


相信读者通过我对前序遍历介绍已经可以自己轻松写出中序遍历了,所以对于中序遍历和后序遍历,博主也是简单介绍,不多赘述了。


1669252210099.jpg


void InOrder(BTNode* root)//中序遍历
{
  if (root == NULL)
  {
  printf("NULL ");
  return;
  }
  InOrder(root->left);
  printf("%d ", root->data);
  InOrder(root->right);
}
int main()
{
  BTNode* root= CreateBinaryTree();
  InOrder(root);
  printf("\n");
  return 0;
}

1669252235297.jpg


也是和推断的相吻合的。


1.3后序遍历


后序遍历,就是以左子树-->右子树-->根的形式访问遍历。也就是说:在访问任何一个节点时,都是左子树的优先级最高,优先访问左子树,然后是右子树,根。以递归的形式访问遍历。


1669252251873.jpg


代码:


void PostOrder(BTNode* root)//后序遍历
{
  if (root == NULL)
  {
  printf("NULL ");
  return;
  }
  PostOrder(root->left);
  PostOrder(root->right);
  printf("%d ", root->data);
}
int main()
{
  BTNode* root= CreateBinaryTree();
  PostOrder(root);//后序遍历
  printf("\n");
  return 0;
}


验证:


1669252267373.jpg


1.4遍历推倒遍历


在一些习题里,我们经常会遇到题目给出前序遍历和中序遍历来求后序遍历,或者由中序遍历和后序遍历来求前序遍历。初学者可能比较头疼,尽管我们由它的访问原则很容易就得出根,但是接下来就无从下手了。


今天博主就分享比较简单的方式去拆解,相信看完后再麻烦的遍历都难不倒你!


首先声明:仅有两种方式可以唯一确定二叉树。


1、已知前序遍历和中序遍历;


2、已知中序遍历和后序遍历.


对于第三种已知前序遍历和后序遍历是无法唯一确定二叉树的。


例题:已知某二叉树的中序遍历序列为J G D H K B A E L I M C F,后序遍历序列为 J G K H D B L M I E F C A,则其前序遍历为:?


其实这种题谨记一点:我们在遍历二叉树的时候使用了什么规则,就按这个规则去拆解。


1669252279836.jpg


通过后序遍历,我们确定最后一个节点一定是根,通过中序遍历,我们确定了根的左子树有哪些元素,根的右子树有哪些元素。然后对左子树进行这样的分析,对右子树也是如此。


左子树元素有:J G D H K B(中序遍历)


                        J G K H D B(后序遍历)


我们再次确认B是根,B的左子树为:J G D H K (中序遍历)


                                                          J G K H D (后序遍历)


B的右子树为NULL,再次分析根为D,D的左子树为:J G(中序遍历) J G(后序遍历)


                                                            D的右子树为: H K(中序遍历) K H(后序遍历)


至此,左子树可以画出来了:


1669252291304.jpg


右子树元素有:E L I M C F(中序遍历)


                        L M I E F C(后序遍历)


现在确定C是根,C的左子树为:E L I M (中序遍历)


                                                    L M I E (后序遍历)


C的右子树为F,再次分析根为E,E的左子树为:NULL


                                                     E的右子树为:L I M(中序遍历)  L M I(后序遍历)


现在,右子树可以画出来了:


1669252309963.jpg


合并一下:整体的树:


1669252319500.jpg


前序遍历:A B D G J H K C E I L M F.


最后;为什么前序和后序无法唯一确定呢?很简单,因为前序遍历第一个是根,后序遍历最后一个也是根,那么,哪一个是根呢?


二、二叉树的一般接口


2.1二叉树的层序遍历


层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。


1669252334435.jpg


像这样,就是层序遍历。这个接口在于计算节点的个数,我们如何通过一层一层的遍历实现统计节点的个数呢?


很明显,层序遍历与上文的前中后序遍历有着殊途同归的意味,我们也是通过递归来实现的。我们把这个问题缩小到一个根和它的左右子树这样一个范围内会有助于我们思考:


如果根不是空,那么算上根的个数之后我们就要去计算它的左右子树的个数,而左右子树又是作为它的左右子树的根,类似于套娃。


打个比方:这个问题就类似于,老师让班长去统计人数,然后班长又去让各科课代表去统计人数,各科课代表呢,又让小组长去统计人数,无限细分,当就剩一个人的时候,就返回。


int TreeSize(BTNode* root)
{
  return root == NULL ? 0 :
  TreeSize(root->left) + TreeSize(root->right) + 1;//加一是要算上根的个数
}
int main()
{
  BTNode* root= CreateBinaryTree();
  int size = TreeSize(root);
  printf("%d ", size);
  printf("\n");
  return 0;
}

以递归的方式来写是很简单的。我们可以画图来印证一下:

1669252361287.jpg


相关文章
|
3月前
|
机器学习/深度学习 JSON 算法
从“书呆子”到“高情商”:一文读懂大模型PPO与DPO
本文通俗解析大模型校准核心技术:PPO(需训练奖励模型、稳定性强)与DPO(直接偏好优化、流程简洁高效)。对比原理、数据格式、实操步骤及效果评估方法,助力开发者低成本打造“通情达理”的专属模型。
440 0
|
3月前
|
机器学习/深度学习 监控 算法
PPO与DPO:大模型对齐的两大核心算法,差异与选型全解析
本文深度解析大模型对齐核心算法PPO与DPO:PPO基于RLHF框架,需训练奖励模型,对齐精准、稳定性强,但流程繁琐、资源消耗大;DPO跳过奖励建模,直接优化偏好,轻量高效、易上手。对比原理、流程、优劣及适用场景,助你科学选型,提升对齐效率。
|
5月前
|
机器学习/深度学习 安全 算法
PPO最强,DPO一般?一文带你了解常见三种强化学习方法,文末推荐大模型微调神器!
大模型如何更懂人类?关键在于“对齐”。PPO、DPO、KTO是三大主流对齐方法:PPO效果强但复杂,DPO平衡高效,KTO低成本易上手。不同团队可根据资源选择路径。LLaMA-Factory Online让微调像浏览器操作一样简单,助力人人皆可训练专属模型。
1086 3
PPO最强,DPO一般?一文带你了解常见三种强化学习方法,文末推荐大模型微调神器!
|
5月前
|
SQL 分布式计算 大数据
别让大数据任务“互相等着死” ——聊聊任务依赖与 DAG 设计的江湖规矩
别让大数据任务“互相等着死” ——聊聊任务依赖与 DAG 设计的江湖规矩
232 6
|
5月前
|
存储 机器学习/深度学习 人工智能
GEO 优化必备:RAG 技术全解析(基于知识密集型 NLP 经典论文)
2020 年论文提出的 RAG(检索增强生成),专治大模型 “幻觉、知识过时” 等落地痛点。它将 “检索外部知识” 与 “生成回答” 深度绑定,先精准抓取相关知识片段,再让模型基于证据生成内容。通过端到端联合训练,检索与生成协同优化,事实准确率显著提升,幻觉率大降。无需重训模型即可更新知识,还能追溯答案来源。如今成企业客服、医疗法律等领域刚需,推动大模型从 “通用” 走向 “可信实用”。这让我们做GEO优化就有了基础理论和方法。
|
小程序 Java 数据库
8套三级医院应用的管理系统源码,直接上项目,HIS、LIS、PACS
8套应用于二级医院、三级医院医院管理系统源码,均有自主知识产权,应用案例,系统稳定运行中。
1923 2
8套三级医院应用的管理系统源码,直接上项目,HIS、LIS、PACS
|
存储 安全 Java
Java 中 ArrayList 和 HashSet 的区别
【8月更文挑战第23天】
471 2
|
消息中间件 监控 Java
使用Kafka实现分布式事件驱动架构
使用Kafka实现分布式事件驱动架构
数据结构——二叉树的遍历【前序、中序、后序】
数据结构——二叉树的遍历【前序、中序、后序】