【算法通关村 Day7】递归与二叉树遍历

递归与二叉树遍历青铜挑战

理解递归

递归算法是指一个方法在其执行过程中调用自身。它通常用于将一个问题分解为更小的子问题,通过重复调用相同的方法来解决这些子问题,直到达到基准情况(终止条件)。

递归算法通常包括两个主要部分:

  1. 基准情况(也叫递归终止条件):当问题规模足够小,递归可以停止,通常返回一个简单的结果。
  2. 递归部分:将问题分解成更小的子问题,并在递归过程中调用自身。

为了更清晰地说明递归,我给你一个经典的例子:阶乘计算。阶乘是一个整数和它以下所有整数的乘积。记作:n! = n * (n-1) * ... * 1,而递归的数学定义是:

  • n! = n * (n-1)!
  • 基本情况:1! = 10! = 1

下面是一个使用Java编写的递归算法来计算阶乘的示例代码:

class Factorial {public static int factorial(int n){//基准情况if(n == 0 || n == 1){return 1;}//递归部分return n * factorial(n - 1);} public static void main(String[] args){int num = 5;int result = factorial(num);System.err.println(num + "! = " + result);}
}

我们之前进行链表反转使用的是迭代法,回顾一下:

public ListNode reverseList(ListNode head) {ListNode prev = null;ListNode curr = head;while (curr != null) {ListNode nextTemp = curr.next; // 临时保存下一个节点curr.next = prev;              // 反转当前节点的指针prev = curr;                   // 前移 prev 和 curr 指针curr = nextTemp;}return prev;                       // 返回新的头结点
}
  • 时间复杂度:O(N),需要遍历整个链表一次。
  • 空间复杂度:O(1),仅使用了固定数量的额外空间。

链表反转同样可以通过递归法实现,

public ListNode reverseList(ListNode head) {// 基准情况if (head == null || head.next == null) {return head;}// 递归调用ListNode newHead = reverseList(head.next);// 反转当前节点和下一个节点的指向head.next.next = head;  // 当前节点的下一个节点指向当前节点head.next = null;       // 当前节点的 next 指向 null// 返回新的头节点return newHead;
}

通过递归方法反转链表简洁且易于理解,但需注意其空间复杂度较高(O(n)),因为每次递归都会增加调用栈的空间消耗。相比之下,迭代法的空间复杂度更低(O(1)),但在代码可读性上稍逊于递归法。

递归与二叉树遍历白银挑战

二叉树遍历的递归写法

递归实现二叉树的前序、中序、后序遍历的思路是基于树的深度优先搜索(DFS)。以下是递归实现这三种遍历方式的代码,并附有解释:

1. 二叉树节点定义

首先,定义一个二叉树节点(TreeNode)类:

static class TreeNode {int val;TreeNode left;TreeNode right;TreeNode(int val) {this.val = val;this.left = null;this.right = null;}
}

2. 前序遍历(Preorder Traversal)

前序遍历的顺序是:根节点 → 左子树 → 右子树

public void preorderTraversal(TreeNode root) {if (root == null) {return;  // 递归终止条件}System.out.print(root.val + " ");  // 访问根节点preorderTraversal(root.left);      // 递归遍历左子树preorderTraversal(root.right);     // 递归遍历右子树
}

解释:

  • 首先访问根节点,然后递归遍历左子树,再递归遍历右子树。

3. 中序遍历(Inorder Traversal)

中序遍历的顺序是:左子树 → 根节点 → 右子树

public void inorderTraversal(TreeNode root) {if (root == null) {return;  // 递归终止条件}inorderTraversal(root.left);       // 递归遍历左子树System.out.print(root.val + " ");  // 访问根节点inorderTraversal(root.right);      // 递归遍历右子树
}

解释:

  • 先递归遍历左子树,然后访问根节点,最后递归遍历右子树。

4. 后序遍历(Postorder Traversal)

后序遍历的顺序是:左子树 → 右子树 → 根节点

public void postorderTraversal(TreeNode root) {if (root == null) {return;  // 递归终止条件}postorderTraversal(root.left);     // 递归遍历左子树postorderTraversal(root.right);    // 递归遍历右子树System.out.print(root.val + " ");  // 访问根节点
}

总结

递归实现的核心在于每次对树的左右子树进行递归操作,递归的终止条件是节点为空。当节点不为空时,根据遍历顺序访问当前节点的值。

假设我们有以下的二叉树:

        1/ \2   3/ \ 4   5

用以下代码来测试遍历:

public class BinaryTreeTraversal {public static void main(String[] args) {// 创建二叉树TreeNode root = new TreeNode(1);root.left = new TreeNode(2);root.right = new TreeNode(3);root.left.left = new TreeNode(4);root.left.right = new TreeNode(5);BinaryTreeTraversal tree = new BinaryTreeTraversal();System.out.println("Preorder Traversal:");tree.preorderTraversal(root);System.out.println("\nInorder Traversal:");tree.inorderTraversal(root);System.out.println("\nPostorder Traversal:");tree.postorderTraversal(root);}
}

输出结果:

Preorder Traversal:
1 2 4 5 3 Inorder Traversal:
4 2 5 1 3 Postorder Traversal:
4 5 2 3 1 

递归与二叉树遍历黄金挑战

二叉树遍历的迭代写法

  • 前序遍历:通过栈控制顺序,根节点先访问,再左子树,最后右子树。
  • 中序遍历:使用栈模拟递归,把左子树入栈后访问根节点,再访问右子树。
  • 后序遍历:使用两个栈,第一个栈负责遍历节点,第二个栈记录节点的访问顺序,最后输出。

这三种迭代实现都利用栈来模拟递归过程,栈的先进后出特性在遍历过程中起到了关键作用。

1. 前序遍历的迭代实现

前序遍历顺序: 根节点 → 左子树 → 右子树

前序遍历的迭代实现我们使用栈来模拟递归过程,下面是详细步骤。

  • 初始化栈: 我们首先将根节点入栈,因为我们从根节点开始遍历。
  • 循环遍历:
    1. 每次从栈中弹出一个节点,访问它的值。
    2. 访问节点之后,需要按照前序遍历的规则,先将右子树入栈,再将左子树入栈。这样做的目的是保证左子树会先被访问到。
    3. 如果节点有右子树或左子树,就按顺序将它们入栈,栈是先进后出的结构,所以下次弹出的节点会先访问到左子树。
public void preorderTraversal(TreeNode root) {if (root == null) {return;  // 如果树为空,直接返回}Stack<TreeNode> stack = new Stack<>();  // 创建一个栈来存储节点stack.push(root);  // 将根节点入栈while (!stack.isEmpty()) {  // 当栈不为空时,继续循环TreeNode node = stack.pop();  // 弹出栈顶元素(当前节点)System.out.print(node.val + " ");  // 访问当前节点// 先右子树入栈,再左子树入栈,保证左子树先被访问if (node.right != null) {stack.push(node.right);  // 如果右子树不为空,先将右子树入栈}if (node.left != null) {stack.push(node.left);  // 如果左子树不为空,再将左子树入栈}}
}

2. 中序遍历的迭代实现

中序遍历顺序: 左子树 → 根节点 → 右子树

中序遍历的迭代实现使用一个栈来模拟递归过程,具体过程如下。

  • 步骤 1: 我们从根节点开始,逐层将左子树的节点入栈。栈会保存当前节点,并且我们一直往左走,直到遇到最左的节点。
  • 步骤 2: 如果当前节点为空(说明已经到达叶子节点的左子树),就弹出栈顶元素并访问它,访问完后转到右子树。
  • 步骤 3: 访问完当前节点后,将指针转向其右子树,继续执行类似的过程。
  • 栈的作用: 栈帮助我们记录从根到最左叶节点的路径,并确保访问完左子树后再访问根节点,再访问右子树。
public void inorderTraversal(TreeNode root) {Stack<TreeNode> stack = new Stack<>();TreeNode current = root;  // 从根节点开始while (current != null || !stack.isEmpty()) {  // 当栈不为空,或者当前节点不为空时,继续遍历// 1. 将当前节点及其所有左子树入栈while (current != null) {stack.push(current);  // 将当前节点入栈current = current.left;  // 然后将当前节点移到左子节点}// 2. 弹出栈顶元素并访问current = stack.pop();  // 弹出栈顶元素System.out.print(current.val + " ");  // 访问当前节点// 3. 转到右子树current = current.right;  // 处理右子树}
}

3. 后序遍历的迭代实现

后序遍历顺序: 左子树 → 右子树 → 根节点

后序遍历的迭代实现稍微复杂一些,因为我们需要逆序访问根、右子树、左子树。为了实现这一点,我们可以使用两个栈来模拟递归过程。

  • 栈 1(stack1): 用来存储节点,遍历顺序是根 → 右子树 → 左子树。我们先将根节点入栈,然后每次弹出栈顶节点并将其左右子树入栈(右子树先入栈)。
  • 栈 2(stack2): 用来存储节点的访问顺序。因为栈是后进先出的,所以访问的顺序是根 → 右子树 → 左子树。最终,我们需要从 stack2 中弹出节点,才能得到正确的后序遍历顺序(左子树 → 右子树 → 根节点)。
  • 两个栈的作用: 第一个栈负责遍历,第二个栈负责记录节点的访问顺序,最终通过第二个栈实现后序遍历的输出。
public void postorderTraversal(TreeNode root) {if (root == null) {return;  // 如果树为空,直接返回}Stack<TreeNode> stack1 = new Stack<>();  // 用于存储遍历的节点Stack<TreeNode> stack2 = new Stack<>();  // 用于存储节点的访问顺序stack1.push(root);  // 将根节点入栈while (!stack1.isEmpty()) {  // 当 stack1 不为空时继续循环TreeNode node = stack1.pop();  // 弹出栈顶元素stack2.push(node);  // 将该节点放入 stack2// 先左子树入栈,再右子树入栈if (node.left != null) {stack1.push(node.left);}if (node.right != null) {stack1.push(node.right);}}// stack2 中存放的是根、右子树、左子树的顺序,我们需要反转输出while (!stack2.isEmpty()) {System.out.print(stack2.pop().val + " ");  // 弹出 stack2 中的元素并访问}
}

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

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

相关文章

朴素贝叶斯法

文章目录 贝叶斯定理朴素贝叶斯法的学习与分类条件独立假设朴素贝叶斯的后验概率最大化准则朴素贝叶斯的基本公式 朴素贝叶斯法的参数估计极大似然估计 贝叶斯定理 前置知识&#xff1a;条件概率、全概率、贝叶斯公式 推荐视频&#xff0c;看完视频后搜索博客了解先验概率、后…

《A++ 敏捷开发》- 20 从 AI 到最佳设计

“我们现在推行AIGC&#xff0c;服务端不需要UI交互设计的用AI自动产出代码&#xff0c;你建议的结对编程、TDD等是否还适用&#xff1f;” 这两年AI确实很火&#xff0c;是报纸、杂志的热门话题。例如&#xff0c;HBR杂志从2024年9月至2025年二月份3期&#xff0c;里面有接近一…

GO系列-IO 文件操作

os io 判断文件是否存在 func fileExist(filePath string) (bool, error) {_, err : os.Stat(filePath)if err nil {return true, nil}if os.IsNotExist(err) {return false, nil}return false, &CheckFileExistError{filePath} } 读取文件内容 func readFileContext(…

rs485协议、电路详解(保姆级)

起源 RS-485即Recommended Standard 485 协议的简写。1983年被电子工业协会(EIA)批准为一种通讯接口标准. 数据在通信双方之间传输&#xff0c;本质是传输物理的电平&#xff0c;比方说传输5V的电压 -1V的电压信号&#xff0c;这些物理信号在传输过程中会受到很多干扰&#x…

JavaWeb-Tomcat服务器

文章目录 Web服务器存在的意义关于Web服务器软件Tomcat服务器简介安装Tomcat服务器Tomcat服务器源文件解析配置Tomcat的环境变量启动Tomcat服务器一个最简单的webapp(不涉及Java) Web服务器存在的意义 我们之前介绍过Web服务器进行通信的原理, 但是我们当时忘记了一点, 服务器…

【愚公系列】《Python网络爬虫从入门到精通》008-正则表达式基础

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…

视觉分析之边缘检测算法

9.1 Roberts算子 Roberts算子又称为交叉微分算法&#xff0c;是基于交叉差分的梯度算法&#xff0c;通过局部差分计算检测边缘线条。 常用来处理具有陡峭的低噪声图像&#xff0c;当图像边缘接近于正45度或负45度时&#xff0c;该算法处理效果更理想。 其缺点是对边缘的定位…

DuodooBMS源码解读之 sale_change模块

销售变更模块用户使用手册 一、模块概述 本扩展模块主要包含两个主要的 Python 文件&#xff1a;sale_change/report/sale_change_report.py 和 sale_change/wizard/sale_change_download.py&#xff0c;提供了销售变更报表查看和销售变更单下载的功能。以下是详细的使用说明…

OpenCV形态学操作

1.1. 形态学操作介绍 初识&#xff1a; 形态学操作是一种基于图像形状的处理方法&#xff0c;主要用于分析和处理图像中的几何结构。其核心是通过结构元素&#xff08;卷积核&#xff09;对图像进行扫描和操作&#xff0c;从而改变图像的形状和特征。例如&#xff1a; 腐蚀&…

力扣算法-1

力扣算法 1 两数之和 给定一个整数数组nums和一个整数目标值target&#xff0c;请你在数组中找出和为目标值target的那两个整数&#xff0c;返回他们的数组下标。 &#xff08;1&#xff09;暴力枚举 &#xff08;枚举数组每一个数x&#xff0c;再寻找数组中是否存在 targe…

pyside6学习专栏(三):自定义QLabel标签扩展类QLabelEx

标签是界面设计中最常用的控件&#xff0c;本文演示了如何基于PySide6的QLabex控件类扩展定义QLabelEX类&#xff0c;以实现更少的编码完成各种图像、彩色文本、动画的加载和显示&#xff0c;丰富界面显示 本示例演示了QLabel和其扩展类QLabelEx分别显示文本、图像、动画的使用…

从0到1:固件分析

固件分析 0x01 固件提取 1、从厂商官网下载 例如D-link的固件&#xff1a; https://support.dlink.com/resource/products/ 2、代理或镜像设备更新时的流量 发起中间人攻击MITM #启用IP转发功能 echo 1 > /proc/sys/net/ipv4/ip_forward#配置iptables&#xff0c;将目…

使用 Spring Boot 和 Canal 实现 MySQL 数据库同步

文章目录 前言一、背景二、Canal 简介三、主库数据库配置1.主库配置2.创建 Canal 用户并授予权限 四.配置 Canal Server1.Canal Server 配置文件2.启动 Canal Server 五.开发 Spring Boot 客户端1. 引入依赖2. 配置 Canal 客户端3. 实现数据同步逻辑 六.启动并测试七.注意事项八…

Linux系统配置阿里云yum源,安装docker

配置阿里云yum源 需要保证能够访问阿里云网站 可以先ping一下看看&#xff08;阿里云可能禁ping&#xff0c;只要能够解析为正常的ip地址即可&#xff09; ping mirrors.aliyun.com脚本 #!/bin/bash mkdir /etc/yum.repos.d/bak mv /etc/yum.repos.d/*.repo /etc/yum.repos…

后端开发:开启技术世界的新大门

在互联网的广阔天地中&#xff0c;后端开发宛如一座大厦的基石&#xff0c;虽不直接与用户 “面对面” 交流&#xff0c;却默默地支撑着整个互联网产品的稳定运行。它是服务器端编程的核心领域&#xff0c;负责处理数据、执行业务逻辑以及与数据库和其他后端服务进行交互。在当…

银河麒麟系统安装mysql5.7【亲测可行】

一、安装环境 cpu&#xff1a;I5-10代&#xff1b; 主板&#xff1a;华硕&#xff1b; OS&#xff1a;银河麒麟V10&#xff08;SP1&#xff09;未激活 架构&#xff1a;Linux 5.10.0-9-generic x86_64 GNU/Linux mysql版本&#xff1a;mysql-5.7.34-linux-glibc2.12-x86_64.ta…

从零开始学习PX4源码9(部署px4源码到gitee)

目录 文章目录 目录摘要1.gitee上创建仓库1.1 gitee上创建仓库PX4代码仓库1.2 gitee上创建子仓库2.固件在gitee部署过程2.1下载固件到本地2.2切换本地分支2.3修改.gitmodules内容2.4同步子模块仓库地址2.5同步子模块仓库地址更新(下载)子模块3.一级子模块和二级子模块的映射关…

【回溯算法2】

力扣17.电话号码的字母组合 链接: link 思路 这道题容易想到用嵌套的for循环实现&#xff0c;但是如果输入的数字变多&#xff0c;嵌套的for循环也会变长&#xff0c;所以暴力破解的方法不合适。 可以定义一个map将数字和字母对应&#xff0c;这样就可以获得数字字母的映射了…

科普:“Docker Desktop”和“Docker”以及“WSL”

“Docker Desktop”和“Docker”这两个概念既有紧密联系&#xff0c;又存在一定区别&#xff1a; 一、联系 核心功能同源&#xff1a;Docker Desktop 本质上是基于 Docker 核心技术构建的。Docker 是一个用于开发、部署和运行应用程序的开源平台&#xff0c;它利用容器化技术…

Flutter 网络请求与数据处理:从基础到单例封装

Flutter 网络请求与数据处理&#xff1a;从基础到单例封装 在 Flutter 开发中&#xff0c;网络请求是一个非常常见的需求&#xff0c;比如获取 API 数据、上传文件、处理分页加载等。为了高效地处理网络请求和数据管理&#xff0c;我们需要选择合适的工具并进行合理的封装。 …