算法day12

算法day12

  • 二叉树理论基础
  • 114 二叉树的前序遍历
  • 145 二叉树的后序遍历
  • 94 二叉树的中序遍历
  • 迭代法

二叉树理论基础

直接看代码随想录就完事了,之前考研也学过,大概都能理解
我这里就说说代码层面的。
二叉树的存储:
1、链式存储:这个就是我们平时用的左指针,右指针那种写法的二叉树存储方式。
2、顺序存储:这个就是利用数组来存二叉树,值得一提的是,结点与结点的孩子如何表示,这个是通过下标直接来表示的,如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

二叉树遍历
深度优先遍历
前序遍历(递归法,迭代法)
中序遍历(递归法,迭代法)
后序遍历(递归法,迭代法)
广度优先遍历
层次遍历(迭代法)
一个技巧:前中后这里指的是访问根节点的顺序。
前中后遍历的逻辑其实都可以借助栈使用递归的方式来实现。

二叉树的定义

type TreeNode struct {
    Val int
    Left *TreeNode
    Right *TreeNode
}

这简直和C++一个样。


二叉树的递归遍历

下面的题目都是涉及二叉树的递归遍历。递归的流程我觉得是很简单的,但是这个代码有时候容易搞混。所以这里对递归的写法做一个总结。

每次写递归都按照这三要素来写
1.确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型
2.确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
3.确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。


114二叉树的前序遍历

我知道逻辑就是先根后左右。c++能写但是go写不出来,看到go语言的代码我就傻掉了。
根本写不出
所以这里好好学学go语言怎么写的。
首先树结点的结构体这个必须要会写,就和c++的实现一样的。

func preorderTraversal(root *TreeNode) (res []int) {
    var traversal func(node *TreeNode)
    traversal = func(node *TreeNode) {
	if node == nil {
            return
	}
	res = append(res,node.Val)
	traversal(node.Left)
	traversal(node.Right)
    }
    traversal(root)
    return res
}

代码逻辑:
1.在这个函数中,先定义一个递归函数traversal ,注意这个函数是直接在内部定义的,这个语法我说实话我还是学的太少了,第一次见。这里把这个语法学了。
函数是可以在另一个函数的内部定义的,定义的方式如下:

traversal = func(node *TreeNode) {
	if node == nil {
            return
	}
	res = append(res,node.Val)
	traversal(node.Left)
	traversal(node.Right)
    }

我总结就是先声明然后进行实现,这个语法格式了解之后多写写就有记忆了。

这个题我只能说已经结束了,这其实就是利用了go语言的一个性质和leetcode题意要求,由于leetcode要求返回的是结果切片,所以这里我就要在这个函数里面调用一个函数,从而直接返回我要的结果。所以说这个题也并非要按题解这么写。这个函数我可以放到外面定义的,只是go语言有这个方便的性质。

这里我再写一个版本

func traversal(root *TreeNode,res *[]int){
    if root == nil {
        return 
    }

    *res = append(*res,root.Val)
    traversal(root.Left,res)
    traversal(root.Right,res)
}


func preorderTraversal(root *TreeNode) []int {
    res := []int{}
    traversal(root,&res)
    return res
}

这个我感觉更适合我理解的写法。
但是这个写法我自己写的时候也遇到问题:
1.我第一次写的时候没有意思到,res切片是引用类型,所以我在函数调用的时候,如果要对res产生影响,那么我就必须要传指针,所以在定义函数的时候我要传的是*[]int,然后传参的时候传的肯定是&res。
2.还有一个同样的问题发生在append,我在对res调用append的时候,这个res必须是引用类型,所以我就要加一个取值符号*,对里面的参数一样如此。

总结:注意引用类型。

自己写了这个版本之后我才知道题解这么用有啥好处,题解这么写还用到了一个语法:闭包

这里通过这个题再学学闭包:
闭包是一种特殊的函数,它可以捕获其创建时所在的作用域中的变量。在go语言中,闭包是通过定义在另一个函数内部的匿名函数实现的。这些匿名函数能够访问并操作其外部函数的变量。

我相信看完这个闭包的定义,肯定有很大的疑惑,我这个代码里面不是有名字吗?这里其实很多教程又省略了,我又去查了资料。

这里我建议看我理解的这个:
上面代码中,严格来说我实现闭包的这个函数并不是匿名函数,然而,即使它被命名了,它仍然可以访问并修改它的外部作用域(PreorferTraversal函数)中的变量res。它仍然符合闭包的行为特征。

所以这里就要进行更正:
闭包的本质
闭包的核心特征是它能够捕获并使用其外部作用域中的变量。**在go语言中,这通常是通过在一个函数内部定义另一个函数实现的。**这个内部函数可以是匿名的,也可以是命名的。关键是前面加粗的这句话。

题解的做法完全就是利用了闭包的性质。


后面两个题很简单

145二叉树的后续遍历

func postorderTraversal(root *TreeNode) []int {
    res := []int{}
    var traversal func(root *TreeNode)
    traversal = func(root *TreeNode){
        if root == nil {
            return 
        }
        
        traversal(root.Left)
        traversal(root.Right)
        res = append(res,root.Val)
    }
    traversal(root)
    return res
}

就是左右根


94 二叉树的中序遍历

左根右

func inorderTraversal(root *TreeNode) []int {
    res := []int{}
    var traversal func(root *TreeNode)
    traversal = func(root *TreeNode){
        if root == nil {
            return 
        }
        
        traversal(root.Left)
        res = append(res,root.Val)
        traversal(root.Right)
    }
    traversal(root)
    return res
}

迭代法(非递归实现遍历)

思路:非递归的实现其实就是用栈来模拟递归,因为递归的实现就是每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
所以非递归的代码实现就是用栈来实现。所以下面的代码我都会先开一个栈来辅助。

前序遍历

思路:前序遍历是中左右,每次先处理中间结点,那么就先将根结点放入栈中,然后将右孩子加入栈,再加入左孩子(有人可能觉得这里我搞返了)。
为什么要先加入右孩子再加入左孩子?因为这样出栈的时候才是中左右的顺序。
可以看看这个动画模拟实现的过程,看看过程中遍历到不同结点时,栈的变化
请添加图片描述
我对这个过程解读以下:
一开始5先入栈,5出栈,5的右孩子(6)入栈,然后左孩子(4)入栈。
此时栈的状态的结果 前序遍历输出5 栈中 6 4.
4出栈,然后4的右孩子2入栈,左孩子1入栈,此时 前序遍历输出 5 4 ,栈中6 2 1
1 出栈,由于1没有左右孩子,那继续2出栈,2也没有左右孩子,然后6出栈,6也没有左右孩子,所以这个时候栈为空,结束。

代码逻辑:
1.既然要用到栈,那就先把栈实现了,这里要注意这栈的元素是什么,是值?还是树结点?回答是树结点,这里需要注意。在上面的图中可能显得稍微简化了一点,但实际上是把树结点加入栈中。就像我们写递归写法一样,我们进行递归下一层同样是对结点操作。
2.实现前序遍历的非递归解法:
1.先创建一个刚刚实现的空栈,再创建一个结果集装结果。
2.先将根结点root先入栈,然后开始循环
3.for 循环是针对这个栈的,只要栈不空就不会停止。
将栈顶元素出栈,然后把这个栈顶元素的值加入结果集。然后先加入右结点,再加入左结点

代码实现

type Stack []*TreeNode

func (s *Stack) Push(node *TreeNode) {
    *s = append(*s, node)
}

func (s *Stack) Pop() *TreeNode {
    if s.IsEmpty() {
        return nil
    }
    index := len(*s) - 1
    element := (*s)[index]
    *s = (*s)[:index]
    return element
}

func (s *Stack) IsEmpty() bool {
    return len(*s) == 0
}

func preorderTraversal(root *TreeNode) []int {
    if root == nil {
        return nil
    }

    stack := Stack{}
    stack.Push(root)
    result := []int{}

    for !stack.IsEmpty() {
        node := stack.Pop()
        result = append(result, node.Val)

        if node.Right != nil {
            stack.Push(node.Right)
        }
        if node.Left != nil {
            stack.Push(node.Left)
        }
    }

    return result
}

中序遍历迭代法

注意有人可能想像递归写法一样,我在前序遍历非递归的写法,改以下就得到中序遍历迭代法。这是实现不了的,因为不是一个逻辑。前序遍历的逻辑无法直接用到中序遍历上。
为什么中序写法和前序的写法不通用?
因为前序的遍历顺序是根左右,先访问的元素是中间结点,而且要处理的元素也是中间结点,所以才能写出相对简洁的代吗,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
再看看中序:中序是左根右,先访问的是二叉树顶部的结点然后一层一层的往下层访问,直到到达树的最左下,然后才开始处理结点(也就是访问结点),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

思路:
中序遍历的顺序是左中右
1.初始化栈和当前结点:创建一个存待访问的结点,然后设置当前结点为根结点。

看懂这个就看懂了这个题的逻辑:
什么是当前结点当前结点是正在处理的元素,为什么需要当前结点,在非递归的实现中,需要一个指针或引用来追踪当前正在处理的结点,这个指针就被称为当前结点。
当前结点如何用? 遍历时不断地讲当前结点移动到其左子节点,直到到达最左侧的结点。这个过程中,沿途的结点都被推入栈中,以备后续访问。
当到达最左侧结点且该结点没有左子结点时,开始从栈中弹出结点,每次从栈中弹出一个结点,这个结点就称为新的”当前结点”,然后处理这个结点,并将当前结点移动到其右子结点。

2.遍历左子树:当前节点非空时,将其推入栈中,并将当前节点设置为其左子节点。这个过程一直持续到最左端,即没有左子节点为止。

3.访问节点和遍历右子树:当前节点为空时(表示到达最左端),从栈中弹出一个元素(这是当前最左侧的节点),访问这个节点(将节点值加入结果数组),将“当前节点”设置为弹出节点的右子节点(开始处理右子树)

4.重复过程:重复上述过程,直到栈为空且当前节点也为空。这表示所有节点都已按中序遍历的顺序访问完毕。

核心:
1.栈的使用: 栈用来暂存还未访问的节点。由于栈的后进先出特性,它能够保证在处理完左子树后能够回到相应的父节点,再去处理右子树。

2.遍历到最左侧: 一开始,需要遍历到树的最左侧,这是中序遍历开始访问节点的地方。

3.从栈中弹出节点: 在没有左子节点可访问时,从栈中弹出节点,表示该节点的左子树已经处理完毕,可以访问该节点了。

4.转向右子树: 访问完节点后,转向处理右子树,即将当前节点设置为右子节点。

代码:

func inorderTraversal(root *TreeNode) []int {
    var result []int
    stack := []*TreeNode{}
    currentNode := root

    for currentNode != nil || len(stack) > 0 {
        // 遍历到最左节点
        for currentNode != nil {
            stack = append(stack, currentNode)
            currentNode = currentNode.Left
        }

        // 当前节点为空,说明左边遍历到底了,开始处理栈顶节点
        currentNode = stack[len(stack)-1]
        stack = stack[:len(stack)-1]  // 弹出栈顶节点
        result = append(result, currentNode.Val)  // 添加到结果中

        // 转向右子树
        currentNode = currentNode.Right
    }

    return result
}

后序遍历非递归实现:

后序遍历只需要在前序遍历的基础是做调整就可以实现,为什么?
先序遍历是根左右,后续遍历是左右根,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了。

可能这样看着还是有点不太懂。先看代码逻辑,我来解读:


func postorderTraversal(root *TreeNode) []int {
    if root == nil {
        return nil
    }

    stack := []*TreeNode{root} // 创建一个栈并初始化为包含根节点
    result := []int{} // 结果数组,用于存储遍历结果

    // 继续遍历直到栈为空
    for len(stack) > 0 {
        node := stack[len(stack)-1] // 取出栈顶元素
        stack = stack[:len(stack)-1] // 弹出栈顶元素

        // 将当前节点的值添加到结果数组的前面
        // 这样做是为了在最后反转遍历的顺序
        result = append([]int{node.Val}, result...)

        // 如果存在左子节点,将其推入栈中
        // 注意这里先处理左子节点,因为我们希望它在右子节点之后被处理
        if node.Left != nil {
            stack = append(stack, node.Left)
        }

        // 如果存在右子节点,将其推入栈中
        if node.Right != nil {
            stack = append(stack, node.Right)
        }
    }

    // 返回结果数组,这个数组是“左-右-根”的顺序
    return result
}

1.栈还是和前序一样的用法,用于存储待访问的解读,首先将根节点压入栈中。
2.根-右-左的遍历顺序,我们从栈中弹出一个节点,首先访问这个节点(根),然后,如果存在左子节点,我们将其压入栈中,接下来,如果存在右子节点,我们也将其压入栈中。由于栈是后进先出的,这意味着右子节点将在左子节点之前被处理。
3.结果数组的构建:每次从栈中弹出一个节点时,我们将其值插入到结果数组的前端,这样做实际上是在创建一个“根-右-左”的顺序的数组。但是,因为我们总是在数组的前端插入新元素,所以最终的数组实际上是“左-右-根”的顺序。

为什么这样做可以得到后序遍历的结果?
在后序遍历中,我们希望遵循“左-右-根”的顺序。由于我们是在结果数组的前端插入每个新访问的节点,所以最后访问的节点(根节点)将出现在数组的最前面,而第一个访问的节点(最左侧的节点)将出现在数组的最后面。这样,即使我们的遍历顺序是“根-右-左”,数组中元素的顺序却是“左-右-根”。

递归写法总结:算法实现还是非常的好理解的
主要还是对go语言的语法学习。今天这个语法我也是头一次体会到了闭包的作用。当时学语法的时候由于没有使用场景,所以学起来还是有很大差别的。

应用场景总结:
在函数调用的时候,对于一个引用类型,如果你想通过函数调用对这个引用类型产生影响,这个时候要么传指针,要么用闭包。而且对于有些函数调用,比如append,我还需要对传递的指针通过取值符号*进行取值后才能调用这个函数。

递归写法和非递归写法哪个比较好?好在哪里?
递归写法:
优点:代码好写,对于要解决的问题来说,这种写法与问题直接的关系就显得很直观。
缺点:递归可能会导致调用堆栈过深,尤其是处理大规模数据的时候,可能会出现堆栈溢出。而且每次递归调用都会增加额外的堆栈帧开销,影响性能。

非递归写法:
优点:通常比递归写法有更好的性能表现,而且无堆栈溢出风险。
缺点:代码不好写,代码的可读性下降,设计比较困难。

总结:递归好写性能差,非递归难写性能好。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

内容检索(2024.02.12)

随着创作数量的增加,博客文章所涉及的内容越来越庞杂,为了更为方便地阅读,后续更新发布的文章将陆续在此做简介并附上原文链接,感兴趣的小伙伴们可持续关注文章发布动态: 信号仿真类话题如何看待频域与时域的仿真差别-…

猫头虎分享已解决Bug || Python Error: ImportError: No module named ‘module_name‘

博主猫头虎的技术世界 🌟 欢迎来到猫头虎的博客 — 探索技术的无限可能! 专栏链接: 🔗 精选专栏: 《面试题大全》 — 面试准备的宝典!《IDEA开发秘籍》 — 提升你的IDEA技能!《100天精通鸿蒙》 …

数学建模:EWM – TOPSIS 超强讲义! 原理、应用、代码

目录 一、综合评价指标预处理 1.定量指标的一致化处理(正向化处理) 2.定量指标的无量纲化处理 二、熵权法(EWM) 三、TOPSIS法 四、熵权法-TOPSIS的使用流程 案例:熵权法-TOPSIS的案例分析:水质评价 …

贪吃蛇的实现,基于windows操作系统

前言: 贪吃蛇从学习到真正实现花了9天实现,第一二天第一次学习,第三四五天第二次学习,第六七八天一边实现一边思考,才完成了贪吃蛇的代码。实现了贪吃蛇以后已经接近过年,我想自己再根据掌握的知识制作烟花…

leetcode 461. 汉明距离

比较简单的一题,先对两个整数进行异或操作,会将两个整数二进制形式中各个数字进行异或操作,不同的数字则为1,再通过移位操作统计得到的二进制数中为1的个数,即为所求。 Java代码如下: class Solution {pub…

STM32 STD/HAL库驱动W25Q64模块读写字库数据+OLED0.96显示例程

STM32 STD/HAL库驱动W25Q64 模块读写字库数据OLED0.96显示例程 🎬原创作者对W25Q64保存汉字字库演示: W25Q64保存汉字字库 🎞测试字体显示效果: 📑功能实现说明 利用W25Q64保存汉字字库,OLED显示汉字的时…

SVD奇异值分解

一、奇异值 奇异值(Singular Values)是线性代数中矩阵的重要性质之一,与奇异值分解(SVD)密切相关。让我们来更详细地了解一下奇异值的概念: 定义: 对于一个矩阵 ( A ),它的奇异值是…

【Chrono Engine学习总结】4-vehicle-4.1-vehicle的基本概念

由于Chrono的官方教程在一些细节方面解释的并不清楚,自己做了一些尝试,做学习总结。 1、基本介绍 Vehicle Overview Vehicle Mannel Vehicle的官方demo 1.1 Vehicle的构型 一个车辆由许多子系统构成:悬挂、转向、轮子/履带、刹车/油门、动…

双场板功率GaN HEMT电容模型以精确模拟开关行为

标题:Capacitance Modeling in Dual Field-Plate Power GaN HEMT for Accurate Switching Behavior(TED.16年) 摘要 本文提出了一种基于表面电位的紧凑模型,用于模拟具有栅极和源极场板(FP)结构的AlGaN/G…

JMM(Java内存模型)

Java内存模型(Java Memory Model,简称JMM)是Java语言规范中定义的一个抽象概念,它描述了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)在并发环境下的访问规则和一致性保证。JMM的主要目标是…

python+flask+django医院预约挂号病历分时段管理系统snsj0

技术栈 后端:python 前端:vue.jselementui 框架:django/flask Python版本:python3.7 数据库:mysql5.7 数据库工具:Navicat 开发软件:PyCharm . 第一,研究分析python技术&#xff0c…

点云标注工具

目录 3d手势识别 c 3d关键点,Bounding Box Labels Rectangle Labels KITTI 3D Ground Truth Annotator c标注工具 3d手势识别 GitHub - 99xtaewoo/Automated-Hand-3D-pose-annotation-Tool: Automated Hand 3D pose annotation Tool c 3d关键点,Bou…

【Django】Django文件上传

文件上传 1 定义&场景 定义&#xff1a;用户可以通过浏览器将图片等文件上传至网站。 场景&#xff1a; 用户上传头像。 上传流程性的文档[pdf&#xff0c;txt等] 2 上传规范-前端[html] 文件上传必须为POST提交方式 表单 <form> 中文件上传时必须带有 enctype…

电视上如何下载软件

电视上如何下载软件&#xff0c;告诉大家一个简单的方法&#xff0c;可以用DT浏览器下载软件&#xff0c;然后会自动安装这个软件&#xff0c;如有技术问题&#xff0c;可以免费解答

【初中生讲机器学习】8. KNN 算法原理 实践一篇讲清!

创建时间&#xff1a;2024-02-11 最后编辑时间&#xff1a;2024-02-12 作者&#xff1a;Geeker_LStar 你好呀~这里是 Geeker_LStar 的人工智能学习专栏&#xff0c;很高兴遇见你~ 我是 Geeker_LStar&#xff0c;一名初三学生&#xff0c;热爱计算机和数学&#xff0c;我们一起加…

从零开始学howtoheap:fastbins的house_of_spirit攻击3

how2heap是由shellphish团队制作的堆利用教程&#xff0c;介绍了多种堆利用技术&#xff0c;后续系列实验我们就通过这个教程来学习。环境可参见从零开始配置pwn环境&#xff1a;从零开始配置pwn环境&#xff1a;从零开始配置pwn环境&#xff1a;优化pwn虚拟机配置支持libc等指…

Python:解析获取连续的重叠对pairwise

简介&#xff1a;pairwise函数&#xff0c;返回从输入迭代器获取的重叠对的迭代器&#xff0c;是Python 3.10 新特性&#xff0c;表示一个迭代器从对象中获取连续的重叠对&#xff0c;在某些场景中可以优化代码运行效率。pairwise 函数是一种用于处理列表中元素之间配对操作的通…

渗透专用虚拟机(公开版)

0x01 工具介绍 okfafu渗透虚拟机公开版。解压密码&#xff1a;Mrl64Miku&#xff0c;压缩包大小&#xff1a;15.5G&#xff0c;解压后大小&#xff1a;16.5G。安装的软件已分类并在桌面中体现&#xff0c;也可以使用everything进行查找。包含一些常用的渗透工具以及一些基本工…

腾讯云4核8G服务器多少钱?2024精准报价

腾讯云4核8G服务器S5和轻量应用服务器优惠价格表&#xff0c;轻量应用服务器和CVM云服务器均有活动&#xff0c;云服务器CVM标准型S5实例4核8G配置价格15个月1437.3元&#xff0c;5年6490.44元&#xff0c;标准型SA2服务器1444.8元一年&#xff0c;轻量应用服务器4核8G12M带宽一…

app逆向-android-studio安装使用教程

Android Studio 是谷歌推出的一个Android集成开发工具&#xff0c;基于IntelliJ IDEA. 类似 Eclipse ADT&#xff0c;Android Studio 提供了集成的 Android 开发工具用于开发和调试。 android-studio下载地址&#xff1a;https://developer.android.com/studio/archive androi…
最新文章