C语言数据结构基础——二叉树学习笔记(三)链式二叉树以及初步认识递归思想

news2024/9/22 21:22:08

1.链式二叉树概念及其逻辑

每个树都要看成:根,左子树,右子树

        链表、顺序表中的遍历方式有正序遍历和逆序遍历,而我们在二叉树中,有前序遍历、中序遍历、后序遍历、层序等多种遍历方法。

       所谓 二叉树遍历 (Traversal) 是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次 。访问结点所做的操作依赖于具体的应用问题。 遍历 是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

                                               

我们用以下二叉树为例(接下来所有的测试用例都是他):

                                           

前序(根在最前访问的顺序):现在的树为

根:1   左子树:2....   右子树:4.....

先访问根1,再访问左子树。

访问左子树后,现在的树变成

根: 2  左子树:3.....  右子树:NULL 

依次类推。 


中序(根在中间访问的遍历方法):第一个访问的是3的左子树

现在的树为

根:1   左子树:2....   右子树:4.....

先访问左子树,树变为

 根: 2  左子树:3.....  右子树:NULL 

先访问左子树,树变为

根:3 左子树:NULL  右子树:NULL 

(空树为最小访问单位,不用再继续递归,可直接访问) 

3为根的树作为2的左子树被访问完后,树变回为

 根: 2  左子树:3.....  右子树:NULL 

接下来,访问 2作为根的树 的根(也就是2自己),再访问2的右子树NULL。

依此类推。


后序(根在最后访问的方法)同理,需要遍历计数时,多用后序

层序就是逻辑上很简单的一层一层遍历(堆就是完全二叉树以层序的方法放进数组的),层序是非递归的,不作为此处学习重点

                                          

下图为前中后三序以及层序的访问顺序(N代表NULL)

2.代码实现以及基础练习题

二叉树的重点一定不是增删查改。

否则二叉树的使用价值便毫无意义,不如链表和数组,二叉树的重点一定是他的结构,所以我们优先学习二叉树的结构,为以后的学习打下基础。

简易实现强中后序的遍历以及求二叉树和大小和深度的接口

                              

2.1前序、中序、后序 

因为我们的知识储备不足,所以手写一个弱智二叉树:

typedef struct BinTreeNode {
	struct BinTreeNode* left;
	struct BinTreeNode* right;
	int val;
}BTNode;

BTNode* BuyBTNode(int val) {
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL) {
		perror("malloc failed!");
		exit(1);
	}
	newnode->val = val;
	newnode->left = NULL;
	newnode->right = NULL;

	return newnode;
}

BTNode* CreateTree()
{
	BTNode* n1 = BuyBTNode(1);
	BTNode* n2 = BuyBTNode(2);
	BTNode* n3 = BuyBTNode(3);
	BTNode* n4 = BuyBTNode(4);
	BTNode* n5 = BuyBTNode(5);
	BTNode* n6 = BuyBTNode(6);
//每一个都是malloc出来的堆区变量,不会被销毁
	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	n4->left = n5;
	n4->right = n6;

	return n1;
}

我们以前序为例,展开调用过程

                      

从物理结构上来说,调用3的左边时(NULL)和3的右边时(NULL)使用的是同一块空间,在之前讲解时间复杂度的博文中对这一点有记录

因为空间是可以复用的,由此也能说明递归的空间复杂度就是深度

“空”是最小规模的子问题,因此每当遇到空时,就应该直接返回。

后序、中序同理,此部分逻辑较简单,直接给出代码。

void Preorder(BTNode* root) {//根 左子树 右子树
	if (root == NULL) {
		printf("N ");
		return;
	}
	printf("%d ", root->val);
	Preorder(root->left);
	Preorder(root->right);
}

void Inorder(BTNode* Node) {//左子树 根 右子树
	if (Node == NULL) {
		printf("N ");
		return;
	}
		Inorder(Node->left);
	printf("%d ", Node->val);
	Inorder(Node->right);
}

void Backorder(BTNode* root) {//左子树 右子树 根
	if (root == NULL) {
		printf("N ");
		return;
	}
	Backorder(root->left);
	Backorder(root->right);
	printf("%d ", root->val);
}

2.2链式二叉树中节点的个数TreeSize

        在以往的思路中,面对链表组成的结构,我们希望使用一个变量充当计数器的作用,每遍历一个节点,计数器就+1,以此达到计数的目的。

于是聪明的你写出如下代码:

                                  

  问题在哪? 

           由函数栈帧中的知识可知,size是一个局部变量,他没有被作为参数,每次出函数都会被销毁。所以size一直无法计数,他总是++size之后就被销毁了。

于是你决定使用static定义静态变量

问题又出在哪?

                            

貌似没有问题,但倘若我掏出如下 主程序代码,不知阁下又该如何应对呢?


//..............
   TreeSize(root);
   TreeSize(root);
   TreeSize(root);
//..............

当该函数被连续调用三次,每一次的值都会增加(如我们上文中手搓的二叉树,第一次返回6,第二次返回12,第三次返回18) 

    静态成员变量只会被初始化一次。 

    因此,再次调用该函数时,size会保留已经拥有的数值,直到程序结束。

尽管利用指针或者返回size等方法都能解决, 

 但既然链式二叉树以递归而出名,我们就应该考虑用递归的方法来获取个数。

类似于斐波那契数列,我们给出如下方法:

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

2.3链式二叉树中的最大深度

104. 二叉树的最大深度 - 力扣(LeetCode)

 思路非常类似于节点个数:

递归的 核心逻辑就是“分而治之”,我们希望得到现在的高度,就需要知道左右子树的最大深度再加上自己(+1),要想知道左右子树的最大深度,就要将左右子树看做新的根新的左右子树...................

有了思路,对于初学者们来说,任然有可能跑不过,甄别以下代码: 

        

                       

两种方法的思想是一样的,但是第一种方法一共调用了四次函数,也就是从头到尾共走了四次完整的树,而第二种方法只走了两次完整的树,效率更高 。


首先做到理解第一种方法更慢,但是并不是简单调用了四次函数那么简单。

(以下内容仅作参考性了解) :

先说结论,我们认为右边的算法每个节点遍历了一遍,共n个节点,消耗为O(N).

那么左边的时间复杂度是O(2^N),原因如下:

    对于左边的代码,我们可以认为每一个节点都有“健忘症”,每当比较完一次自己的左右子树谁大之后,只能记住谁更大,但是具体多大记不住,因此,他只能再向下调用一次大的那个节点

   我们从下往上看(按照函数的递归原理,进入函数后会先maxDepth到尾部,也就是叶子节点的左右子树)。由于叶子节点的左右子树都为空,不满足

maxDepth(root->left)>maxDepth(root->right)

因此执行maxdepth(root->right)+1,又会向下调用一次空来return NULL。

当叶子传给他的上级“小领导”之后, 小领导又是只能记住谁大谁小,让大的叶子节点(两个目前是一样大的)向下再遍历一次,而叶子节点的向下遍历又会调用一次空来return NULL

...........................

从根的角度来说,根忘记了一次,第一代儿子们就会忘记两次,  第一次告诉他谁大谁小,第二次是大的那个儿子要去再遍历一遍他的后代。而第一代儿子们每一次的忘记对于第二代儿子来说都意味着遍历两遍,又因为第一代孩子一共忘记两次,所以第二代儿子就得遍历四次,对于第三代儿子就是8次..........

对于最坏的情况:

                             

共遍历的次数就是2为公比的等比数列求和,故总次数是2^n次方(此时的节点个数都拿去堆积层数了)。

2.4链式二叉树中第k层节点个数

首先认识到:

    对于第一层来说,第三层是第三层;对于第二层来说,第三层是第二层;对于第三层来说,第三层是第一层。

于是(以k=3而言),求第三层个数就是求第二层的第二层,就是求第三层的第一层。

为什么要如此麻烦的来思考这个问题?

递归问题中,大致分为两个部分需要我们考虑,一个是确定需要继续深层递归的子问题,另外一个是确定返回的条件,也叫作最小子问题。我们通过上述方法,让需要递归的子问题接近我们的返回条件,以达到返回的目的

       二叉树中的返回条件而言,空一定是一个最小子问题 ,但是就这个具体问题而言,还有什么情况是最小子问题呢?任然以k=3为例,我们已经递归到:求第三层个数就是求第三层的第一层(k=1),那么还能继续递归吗?此时不应该只要节点存在就返回1了吗?

由此,我们实现代码如下:

int TreeKLevel(BTNode* root, int k) {
	if (root == NULL) {
		return 0;
	}
	if (k == 1 ) {
		return 1;
	}

	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}

2.5链式二叉树中的查找问题

先分析最小子问题:

由上文可知,一般情况下为空都是二叉树的一个最小子问题,此处一样。其次,若是找到了对应的val,也需要直接返回,也是最小子问题之一。

再分析子问题:

     不为空也不相等,需要以该节点为新的根继续向下查找的办法。

实现如下: 

                  

这样真的能返回吗?

返回是返回给函数调用的地方,此处函数的返回值没有被任何变量接受,是无效返回。

一些严格的编译器可能会直接报错 (如力扣等)

我们稍加修改:

          

这是前面我们计算二叉树节点个数时出现的“健忘症”问题,复杂度会提升很多。 

正确实现: 

//查找函数
BTNode* TreeFind(BTNode* root, int x) {
	if (root == NULL) {
		return NULL;
	}
	if (root->val == x) {
		return root;
	}

	//更深层递归问题
	BTNode* nodel = TreeFind(root->left, x);
	if (nodel!= NULL) {
		return nodel;
	}
	//...............
	BTNode* noder = TreeFind(root->right, x);
	if (noder != NULL) {
		return noder;
	}

	return NULL;
}

检测一下:

2.6判定是否为相同的树

100. 相同的树 - 力扣(LeetCode) 

任然是递归的思想,不过要注意根节点是否为空

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p==NULL&&q==NULL){
        return true;
    }
    if(p==NULL||q==NULL){
        return false;
    }
    if(p->val!=q->val){
        return false;
    }
    //都存在且相同
    return isSameTree(p->left,q->left)&&
           isSameTree(p->right,q->right);
}

类似的,还有题目判断是否是相同的树

101. 对称二叉树 - 力扣(LeetCode)

bool _isSymmetric(struct TreeNode* broleft,struct TreeNode* broright){
     if ( broleft == NULL && broright == NULL ){
        return true;
    }
    if(broleft==NULL || broright==NULL){
        return false;
    }
    if(broleft->val != broright->val){
        return false;
    }

    return _isSymmetric(broleft->left,broright->right)&&
            _isSymmetric(broleft->right,broright->left);
}

bool isSymmetric(struct TreeNode* root) {
    struct TreeNode* broleft=root->left;
    struct TreeNode* broright=root->right;

    if ( broleft == NULL && broright == NULL ){
        return true;
    }
    if(broleft==NULL || broright==NULL){
        return false;
    }
    if(broleft->val != broright->val){
        return false;
    }
    return _isSymmetric(broleft,broright);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1534840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux网络固定ip的方式

1. 注意 默认情况下,我们linux操作系统 ip 获取的方式是自动获取的方式(DHCP),自动获取在我们需要进行集群配置的时候,IP会经常变化,需要将IP固定下来。 2. 第一步 编辑我们 linux 的网卡文件 这个网卡文件…

Springmvc---解读<url-pattern/>

&#xff08;1&#xff09; *.do&#xff1a; 在没有特殊要求的情况下&#xff0c;SpringMVC 的中央调度器 DispatcherServlet 的<url-pattern/>常使用后辍匹配方式&#xff0c;如写为*.do 或者 *.action, *.mvc等。 &#xff08;2&#xff09; / &#xff1a; Tomcat本身…

敢为天下先!深圳市全力推动鸿蒙生态发展……程序员

3月19日&#xff0c;鸿蒙生态创新中心揭幕仪式在深圳正式举行。鸿蒙生态创新中心的建立是为构建先进完整、自主研发的鸿蒙生态体系&#xff0c;将深圳打造为鸿蒙生态策源地、集聚区的具体举措&#xff0c;也是推动我国关键核心技术高水平自立自强、数字经济高质量发展、保障国家…

面试算法-62-盛最多水的容器

题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不能倾斜容器。…

7个方便快速使用的Tkinter控件源码分享,赶快收藏

文章目录 7个快速使用的Tkinter控件源码分享1. 按钮 Button2. 开关 Checkbutton3. 显示文本 Label4. 带名称、数值显示的划动条5. 带标签的复选框6. 带名称的输入框7. 带名称的微调框7个快速使用的Tkinter控件源码分享 tkinter 是一个简单入手,但是功能十分强大的GUI编程库,学…

谷歌DeepMind推出SIMA智能体,可以跟人一起玩游戏

谷歌 DeepMind 推出了 SIMA&#xff0c;这是一种通过训练学习游戏技能的人工智能代理&#xff0c;因此它玩起来更像人类&#xff0c;而不是一个只做自己事情的强大人工智能。 从早期与 Atari 游戏合作&#xff0c;到以人类大师级别玩《星际争霸 II》的 AlphaStar 系统&#xf…

算法设计与分析-动态规划算法的应用——沐雨先生

一、实验目的 1&#xff0e; 掌握动态规划算法的基本思想&#xff0c;包括最优子结构性质和基于表格的最优值计算方法。 2&#xff0e;熟练掌握分阶段的和递推的最优子结构分析方法。 3&#xff0e; 学会利用动态规划算法解决实际问题 。 二、实验内容 1. 问题描述 &#…

Open-Sora揭秘:这款开源视频生成神器能否超越OpenAI的Sora?

近日&#xff0c;我开始研究开源视频生成项目Open-Sora。 Open-Sora的核心思想&#xff0c;其实就是通过开源的方式&#xff0c;让先进的视频生成技术能够普及到大众手中。 而且&#xff0c;它还提供了精简又用户友好的工具和内容。 这样一来&#xff0c;视频制作的复杂性就…

Postman接口做关联测试的方法步骤

应用场景 假设下一个接口登录需要上一个接口的返回值&#xff0c;例如请求需要先登录获取到token&#xff0c;下一个请求要携带对应的token才能进行请求 方法&#xff1a;通过设置全局变量/环境变量 方法一&#xff1a;设置全局变量 1.先请求登录接口&#xff0c;请求成功之后…

解决微信小程序代码包大小限制方法

1 为什么微信小程序单个包的大小限制为 2MB 微信小程序单个包的大小限制为 2MB 是出于以下几个考虑&#xff1a; 保证小程序的启动速度&#xff1a;小程序的启动速度是影响用户体验的关键因素之一。如果包太大&#xff0c;会导致小程序启动时间过长&#xff0c;从而影响用户体…

2024年【熔化焊接与热切割】报名考试及熔化焊接与热切割复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 熔化焊接与热切割报名考试考前必练&#xff01;安全生产模拟考试一点通每个月更新熔化焊接与热切割复审考试题目及答案&#xff01;多做几遍&#xff0c;其实通过熔化焊接与热切割模拟考试题库很简单。 1、【单选题】…

Java反射机制的讲解及其示例说明

Java 反射机制是指在运行时动态地获取类的信息以及操作对象的方式。它允许程序在运行时检查和操作类、方法、属性等&#xff0c;而不需要在编译时就确定这些属性。通过反射机制&#xff0c;我们可以在运行时动态地创建对象、调用方法、获取属性等。 Java 反射机制提供了以下主…

LeetCode每日一题【24. 两两交换链表中的节点】

思路&#xff1a;先创建虚拟头结点&#xff0c;再用双指针&#xff0c;两两交换 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr…

2024.3.21 QT

思维导图 自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面。&#xff08;不要使用课堂上的图片和代码&#xff0c;自己发挥&#xff0c;有利于后面项目的完成&#xff09; 要求&#xff1a; 1. 需要使用Ui界面文件进行界面设计 2. ui界面上的组件相关设置&…

C++的vector类(一):vector类的常见操作

目录 前言 Vector类 遍历与初始化vector ​vector的扩容机制 vector的对象操作 find与insert 对象数组 前言 string类中还有一些内容需要注意&#xff1a; STL 的string类怎么啦&#xff1f; C面试中string类的一种正确写法 C STL string的Copy-On-Write技术 C的st…

java 继承(上)

在说继承之前&#xff0c;先看Student、Teacher这两个类&#xff0c;通过下面的代码可以看出什么&#xff1f; 细心的你可能已经有所发现了&#xff0c;那就是它们都含有相同的属性和方法。 如果把相同特征提取出来&#xff0c;放到一个类中&#xff0c;暂时先把这个类叫做 Tan…

【Linux】多线程概念 | POSIX线程库

文章目录 一、线程的概念1. 什么是线程Linux下并不存在真正的多线程&#xff0c;而是用进程模拟的&#xff01;Linux没有真正意义上的线程相关的系统调用&#xff01;原生线程库pthread 2. 线程和进程的联系和区别3. 线程的优点4. 线程的缺点5. 线程异常6. 线程用途 二、二级页…

25.删除链表中倒数第N个结点

题意&#xff1a;给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 class Solution { public:ListNode* removeNthFromEnd(ListNode* head, int n) {ListNode* dummyHeadnew ListNode(0); //定义虚拟头结点ListNode* fastdummyHead; //定…

32.网络游戏逆向分析与漏洞攻防-游戏网络通信数据解析-网络数据分析原理与依据

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果 内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;31.其它消息的实…

浅谈游戏地图中位置实时更新的技术方案

地图如今在游戏中发挥的作用越来越重要&#xff0c;随着电子竞技的兴起&#xff0c;地图逐渐成为了为玩家创造体验的直接舞台。希望本文能对有兴趣了解游戏地图背后实现原理的同学一些帮助。 什么是游戏地图 在游戏中可以通过3D场景虚拟一个完整的世界&#xff0c;当3D场景较为…