算法day12

news2024/10/7 10:16:48

算法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.coloradmin.cn/o/1437982.html

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

相关文章

简单实验 spring cloud gateWay 路由实验 实验

1.概要 1.1 说明 微服务统一网关实验&#xff0c;这里简单实验一下路由的功能 1.2 实验步骤&#xff0c;使用下面这个工程作为基础工程添加了一个gateWay做如下使用 简单实践 spring cloud nacos nacos-server-2.3.0-CSDN博客 2 代码 2.1 工程文件 <?xml version&quo…

【Linux取经路】探寻shell的实现原理

文章目录 一、打印命令行提示符二、读取键盘输入的指令三、指令切割四、普通命令的执行五、内建指令执行5.1 cd指令5.2 export指令5.3 echo指令 六、结语 一、打印命令行提示符 const char* getusername() // 获取用户名 {return getenv("USER"); }const char* geth…

生成式学习,特别是生成对抗网络(GANs),存在哪些优点和缺点,在使用时需要注意哪些注意事项?

生成对抗网络&#xff08;GANs&#xff09; 1. 生成对抗网络&#xff08;GANs&#xff09;的优点&#xff1a;2. 生成对抗网络&#xff08;GANs&#xff09;的缺点&#xff1a;3. 使用生成对抗网络&#xff08;GANs&#xff09;需要注意的问题 1. 生成对抗网络&#xff08;GANs…

学生管理系统(javaSE第一阶段项目)

JavaSE第一阶段项目_学生管理系统 1.项目介绍 此项目是JavaSE第一阶段的项目,主要完成学生对象在数组中的增删改查,大家可以在此项目中发挥自己的想象力做完善,添加其他功能等操作,但是重点仍然是咱们前9个模块的知识点2.项目展示 2.1.添加功能 2.2.查看功能 2.3.修改功能 2…

第二证券:大涨5%,这一指数爆发!

A股商场今日上午进一步上行&#xff0c;各大指数持续上涨&#xff0c;其间上证指数克复2800点。小市值股票体现更佳&#xff0c;中证1000指数上午大涨5%。 港股商场方面&#xff0c;今日上午一度大幅上涨&#xff0c;后涨幅有所回落。港股百胜我国今日上午体现抢眼&#xff0c…

jvm垃圾收集器之七种武器

1.回收算法 1.1 标记-清除算法(Mark-Sweep) 分为两个阶段&#xff0c;标注和清除。标记阶段标记出所有需要回收的对象&#xff0c;清除阶段回收被标记的对象所占用的空间。 该算法最大的问题是内存碎片化严重&#xff0c;后续可能发生大对象不能找到可利用空间的问题。 1.2 …

10.0 Zookeeper 权限控制 ACL

zookeeper 的 ACL&#xff08;Access Control List&#xff0c;访问控制表&#xff09;权限在生产环境是特别重要的&#xff0c;所以本章节特别介绍一下。 ACL 权限可以针对节点设置相关读写等权限&#xff0c;保障数据安全性。 permissions 可以指定不同的权限范围及角色。 …

Topaz Photo AI for Mac v2.3.1 补丁版人工智能降噪软件无损放大

想要将模糊的图片变得更加清晰&#xff1f;不妨试试Topaz Photo AI for Mac 这款人工智能、无损放大软件。Topaz Photo AI for Mac 一款强大的人工智能降噪软件&#xff0c;允许用户使用复杂的锐化算法来提高图像清晰度&#xff0c;还包括肖像编辑选项&#xff0c;如面部重塑、…

Verilog刷题笔记20

题目&#xff1a; Case statements in Verilog are nearly equivalent to a sequence of if-elseif-else that compares one expression to a list of others. Its syntax and functionality differs from the switch statement in C. 解题&#xff1a; module top_module ( …

RabbitMQ-3.发送者的可靠性

发送者的可靠性 3.发送者的可靠性3.1.生产者重试机制3.2.生产者确认机制3.3.实现生产者确认3.3.1.开启生产者确认3.3.2.定义ReturnCallback3.3.3.定义ConfirmCallback 3.发送者的可靠性 首先&#xff0c;我们一起分析一下消息丢失的可能性有哪些。 消息从发送者发送消息&#…

新版MQL语言程序设计:键盘快捷键交易的设计与实现

文章目录 一、什么是快捷键交易二、使用快捷键交易的好处三、键盘快捷键交易程序设计思路四、键盘快捷键交易程序具体实现1.界面设计2.键盘交易事件机制的代码实现 一、什么是快捷键交易 操盘中按快捷键交易是指在股票或期货交易中&#xff0c;通过使用快捷键来进行交易操作的…

L1-071 前世档案

一、题目 二、解题思路 三、代码 #include<iostream> using namespace std; #include<cmath> int main() {int n,m;cin>>n>>m;while(m--){string str;cin>>str;int x1;for(int i0;i<n;i){if(str[i]n){xpow(2,n-(i1));}}cout<<x<<…

Linux网络配置及进程管理

一、网络配置 1、网络配置原理图 2、查看网络IP和网关 3、查看windows环境的中VMnet8网络配置&#xff08;ipconfig 指令&#xff09; 4、查看Linux网络配置&#xff08;ifconfig指令&#xff09; 5、Linux网络环境配置 5.1、自动获取 5.2、指定IP 直接修改配置文件来制定IP…

阿里云游戏服务器收费价格表,一年和1个月报价

阿里云游戏服务器租用价格表&#xff1a;4核16G服务器26元1个月、146元半年&#xff0c;游戏专业服务器8核32G配置90元一个月、271元3个月&#xff0c;阿里云服务器网aliyunfuwuqi.com分享阿里云游戏专用服务器详细配置和精准报价&#xff1a; 阿里云游戏服务器租用价格表 阿…

Tomcat之虚拟主机

1.创建存放网页的目录 mkdir -p /web/{a,b} 2.添加jsp文件 vi /web/a/index.jsp <% page language"java" import"java.util.*" pageEncoding"UTF-8"%> <html> <head><title>JSP a page</title> </head> …

IAR报错:Error[Pa045]: function “halUartInit“ has no prototype

在IAR工程.c文件末尾添加一个自己的函数&#xff0c;出现了报错Error[Pa045]: function "halUartInit" has no prototype 意思是没有在开头添加函数声明&#xff0c;即void halUartInit(void); 这个问题我们在keil中不会遇到&#xff0c;这是因为IAR编译器规则的一…

堆结构的解读

对于数据结构堆来说,堆事一种特定的数据结构,其与二叉树非常类似,但是又与二叉树有所不同,其不同点在于堆不需要左右指针指向孩子节点,而给定一个数组,将数组中的元素进行特定排序之后,就可以得到一个堆,如图是一个数组 添加图片注释,不超过 140 字(可选) 该数组的…

鸿蒙开发系列教程(十四)--组件导航:Tabs 导航

Tabs 导航 Tabs组件的页面组成包含两个部分&#xff0c;分别是TabContent和TabBar。TabContent是内容页&#xff0c;TabBar是导航页签栏 每一个TabContent对应的内容需要有一个页签&#xff0c;可以通过TabContent的tabBar属性进行配置 设置多个内容时&#xff0c;需在Tabs…

牛客网SQL:查询每个日期新用户的次日留存率

官网链接&#xff1a; 牛客每个人最近的登录日期(五)_牛客题霸_牛客网牛客每天有很多人登录&#xff0c;请你统计一下牛客每个日期新用户的次日留存率。 有一个登录(login。题目来自【牛客题霸】https://www.nowcoder.com/practice/ea0c56cd700344b590182aad03cc61b8?tpId82 …

为什么Mac电脑需要装系统优化清理软件?

为什么Mac电脑需要装系统优化清理软件? 依照我个人多年使用Mac 的经验&#xff0c;Mac 系统用起来比起Windows 系统稳定不少&#xff0c;软件性能也优化得很好 &#xff0c;并且不容易中毒。 但我 还是推荐大家在你的Mac 上装一套系统优化、清理软件 。 接下来就以垃圾文件、中…