如何花最少的资源遍历二叉树

news2024/11/19 7:29:22

文章目录

    • 一、递归遍历二叉树
      • 1.1 前序遍历
      • 1.2 中序遍历
      • 1.3 后序遍历
    • 二、非递归遍历二叉树
      • 2.1 前序遍历
      • 2.2 中序遍历
      • 2.3 后序遍历
    • 三、高效的 Morris 遍历
      • 3.1 前序遍历
      • 3.2 中序遍历
      • 3.3 后序遍历

关于二叉树的遍历也是面试过程中非常有可能考的话题。

常见的简单的递归遍历二叉树,非常的基础,显而易见,这并不会是面试官想要的看的遍历二叉树的方法。一般来说,非递归实现二叉树的遍历才是考得相对较多的,时间复杂度 O(N),空间复杂度 O(N)

如果我们能够在空间复杂度为 O(1)的条件下实现二叉树的遍历,这将是一个很好的加分项,这就是文章后面介绍的 Morris 遍历
在这里插入图片描述

那么接下来我们先回顾一下递归和非递归的方法遍历二叉树的做法,然后介绍 Morris 遍历

二叉树节点(TreeNode)

class TreeNode {
  int val = 0; //值
  TreeNode left = null;  //左节点
  TreeNode right = null;  //右节点
  public TreeNode(int val) {
    this.val = val;
  }
}

一、递归遍历二叉树

按照如图所示进行二叉树的遍历,遍历的节点顺序如图所示。

在这里插入图片描述

我们会发现每个节点都会遍历到三次,先序遍历的结果就是节点被第一次遇见的顺序,中序遍历的结果就是节点被第二次遇见的顺序,后序遍历的结果就是节点被第三次遇见的顺序

1.1 前序遍历

前序遍历二叉树的顺序是,根——》左——》右

basecase

当节点 root 为null 时,即遇见了空节点,就可以直接返回了

递归过程

获取到一棵二叉树后,但凡遇见的节点不是空节点,那么我们就将其放到结果列表中,然后分别遍历该节点的左子树和右子树。那么对于子树处理方式也是一样的

public class Solution1 {
    public List<Integer> list = new ArrayList<>(); //返回的结果列表
    public List<Integer> preorderTraversal (TreeNode root) {
        if(root == null) {
            return list; 
        }
        func1(root);
        return list;
    }
    public void func1(TreeNode root) {
        if(root == null) {
            return;  //basecase
        }
        list.add(root.val);//添加到结果列表中
        func1(root.left); //遍历左子树
        func1(root.right);//遍历右子树
    }
}

1.2 中序遍历

中序遍历二叉树的顺序是,左——》根——》右

本质上和前序遍历的没有什么不同,不过是写在了同一个方法中

public class Solution2 {
    List<Integer> list = new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null) 
            return list;
        inorderTraversal(root.left); //遍历左子树
        list.add(root.val);  //添加到结果列表中
        inorderTraversal(root.right); //遍历右子树
        return list; 
    }
}

1.3 后序遍历

中序遍历二叉树的顺序是,左——》右——》根

public class Solution3 {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root == null) return list;
        
        List<Integer> leftTree = postorderTraversal(root.left);
        list.addAll(leftTree);//将左子树的后序遍历结果放到结果列表中

        List<Integer> rightTree = postorderTraversal(root.right);
        list.addAll(rightTree);//将右子树的后序遍历结果放到结果列表中

        list.add(root.val);
        //此时左子树和右子树后序遍历的结果放好了,就将当前根节点值放到结果列表中
        return list;
    }
}

二、非递归遍历二叉树

非递归遍历实际上将就是将压栈这个步骤自己进行实现

2.1 前序遍历

前序遍历是 根、左、右 这样的顺序,那么就应该先把根节点入栈

在这里插入图片描述

然后弹出一个节点,即当前的根节点,保存到结果列表 list 中。并且只要当前的根节点的左右节点不为空,就将他们压入栈。先压右节点,再压左节点。因为栈是先进后出的,为了先处理左子树,就应该让左节点在右节点后入栈

在这里插入图片描述

只要栈不为空,我们就继续弹出一个节点,进行保存,当前弹出的是栈顶的 B 节点,说明开始处理左子树了

在这里插入图片描述

等到下一轮 D 节点都弹出时,说明以 B 节点为根节点的子树已经处理完毕了,接下来要开始处理以 C 节点为根节点的子树,依次类推,直到栈为空,循环结束

public class Solution1 {    
    public ArrayList<Integer> list = new ArrayList<>();//结果列表
    public void func2(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);//先将根节点入栈
        //只要栈不为空就一直循环
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();//弹出元素,当前的根
            list.add(cur.val);//保存
            //先压右节点,再压左节点
            if (cur.right != null) {
                stack.push(cur.right);
            }
            if (cur.left != null) {
                stack.push(cur.left);
            }
        }
    }
}

2.2 中序遍历

中序遍历的顺序是 左、根、右

只要 root 节点不为空,就不断的将其左子节点压入栈中,root 在走到 root 的左节点上

在这里插入图片描述

遇见空节点,那么该空节点必然是其父节点的左节点(左子树处理完毕),此时从栈中弹出一个节点便是该子树的根节点,进行保存。然后让 root 走到方才弹出的根节点的右子树上,继续处理右子树的内容,处理的方式同上

在这里插入图片描述

显而易见,此时 root 为空,那么只要 root 不为空或者栈不为空循环都会继续,继续从栈中弹出节点,进行保存。

直到弹出节点 A,进行保存,此时栈为空了。但是 root 不为空,指向节点 A 的右子树上的根节点 C 呢,循环继续。也就是说,此时节点 A 的左子树处理完毕,根处理完毕,开始处理右子树。

以此类推,直到栈中没有节点了,root 也为空了,一切就真的结束了

public class Solution2 {
    public ArrayList<Integer> list = new ArrayList<>();//结果列表
    public void func2(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        while (root != null || !stack.isEmpty()) {
            //一股脑将左边界都添加到栈中,直到遇见null
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            //此时的root为null了,弹出一个元素,走到其右子树中
            TreeNode node = stack.pop();
            list.add(node.val);
            root = node.right;//对于右子树的处理方法同上
        }
    }
}

2.3 后序遍历

后序遍历的思想和前序遍历的思想非常的像。

已知后序遍历的顺序是 左、右、根。栈的特点就是先进后出,那么如果我们遍历二叉树以根、右、左 的顺序使用一个收集栈保存节点,是可以做到的(模仿前序遍历,区别就是先压左节点再压右节点)。

最后再将收集栈中的节点一个个弹出,就是左、右、根的顺序

public class Solution3 {
    public ArrayList<Integer> list = new ArrayList<>();//结果列表
    public void func2(TreeNode root) {
        Stack<TreeNode> stack1 = new Stack<>();//压栈
        Stack<Integer> stack2 = new Stack<>();//收集栈
        stack1.push(root);//先压入根节点
        while (!stack1.isEmpty()) {
            TreeNode node = stack1.pop();//弹出一个元素,将其值放到收集栈中
            stack2.push(node.val);
            //方才弹出的元素,有左先压左,有右再压右
            if (node.left != null) {
                stack1.push(node.left);
            }
            if (node.right != null) {
                stack1.push(node.right);
            }
            //周而复始
        }
        //将收集栈中的元素倒出来
        //stack1的出栈顺序是根右左,压入到stack2中再弹出来,就是左右根
        while (!stack2.isEmpty()) {
            list.add(stack2.pop());
        }
    }
}

三、高效的 Morris 遍历

在上面的遍历方式中时间复杂度为 O(N),因为要遍历每个节点。空间复杂度为 O(N),因为使用到了栈,无论是递归使用的系统的栈还是非递归自己手动创建的栈。

Morris 遍历是一种和上面的遍历方式不太一样的遍历方式,通过利用树中的空节点,来达到节省空间的目的,空间复杂度为 O(1)

遍历介绍

有一个 TreeNode 类型的变量 cur 此时正在根节点处

  1. 如果 cur 节点没有左孩子,cur 直接移动到右孩子上
  2. 如果 cur 节点有左孩子,那就找到左子树上最右的节点 Rightmost
    • 若 Rightmost 节点的右指针指向空,那就让右指针指向 cur,然后 cur 向左边移动
    • 若 Rightmost 节点的右指针已经指向 cur 了,那就让它指向 null,然后cur 往右移
  3. cur 为空,遍历结束

遍历过程

此时的 cur 指向 A 节点,有左孩子,且左子树上的最右节点是 B 节点,那就让 B 的右指针指向 cur,cur 往左子树上移动

在这里插入图片描述

发现 cur 依旧有左孩子,按照之前的做法进行操作。cur 即将移动到其左孩子 D 节点上

在这里插入图片描述

终于发现 cur 没有左孩子了,那就往其右子树移动
在这里插入图片描述

此时的 cur 没有左孩子,cur 就往 G 节点的右子树上移动,回到了 B 节点

在这里插入图片描述

cur 回到 B 节点后,B 节点有左子树,并且左子树的最右节点 Rightmost 正指向 cur 呢,那就让它给指回 null,恢复原样,然后 cur 往其右孩子上移动

在这里插入图片描述

cur 往 B 节点的右孩子移动,就又回到了 A 节点,A 节点有左子树,并且左子树的最右节点正指向 cur(A 节点),就将其恢复原样,重新指向 null,然后 cur 往其右孩子上移动

在这里插入图片描述

这样子一来,这个二叉树的左子树以及根节点算是遍历完成了,接下来的右子树也是同样的规则进行遍历

在这里插入图片描述

直到 cur 为空,遍历就算结束了

在这里插入图片描述

我们可以发现,所有的拥有左子树的节点都遍历了两次,其余的节点只遍历了一次

代码实现

public class Solution5 {
    public static void Morris (TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        TreeNode rightMost = null;
        while (cur != null) {
            rightMost = cur.left;//开始找左子树中最右的节点
            if (rightMost == null) {
                //没有左孩子
                cur = cur.right;
            }else {
                //有左孩子
                //先让 rightMost 指向 cur 左子树的最右节点
                while (rightMost.right != null && rightMost.right != cur) {
                    rightMost = rightMost.right;
                }
                //到这里,rightMost 的 right 可能为 null,可能为 cur
                if (rightMost.right == cur) {
                    //说明是第二遍到 cur 节点
                    rightMost.right = null;//让其回归正常
                    cur = cur.right;
                }else {
                    //第一遍来到 cur 节点
                    rightMost.right = cur;
                    cur = cur.left;
                }
            }
        }
    }
}

3.1 前序遍历

对于前序遍历来说,如果某个节点没有左孩子,只会遍历到一次,就直接打印。如果有左孩子的,说明可以遍历到两次,那么第一次遍历到的时候打印

在这里插入图片描述

红色部分就是前序遍历的节点顺序,和预期的一模一样

public class Solution5 {
    public static void preMorris (TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        TreeNode rightMost = null;
        while (cur != null) {
            rightMost = cur.left;
            if (rightMost == null) {
                System.out.println(cur.val);//唯一一次遍历,打印
                cur = cur.right;
            }else {
                while (rightMost.right != null && rightMost.right !=cur) {
                    rightMost = rightMost.right;
                }
                if (rightMost.right == cur) {
                    //说明是第二遍到 cur 节点
                    rightMost.right = null;
                    cur = cur.right;
                }else {
                    //第一遍来到 cur 节点
                    System.out.println(cur.val);//第一次出现,打印
                    rightMost.right = cur;
                    cur = cur.left;
                }
            }
        }
    }
}

3.2 中序遍历

对于中序遍历来说,如果某个节点没有左孩子,只会遍历到一次,就直接打印。如果有左孩子的,说明可以遍历到两次,那么第二次遍历到的时候打印

在这里插入图片描述

public class Solution5 {
    public static void inMorris (TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        TreeNode rightMost = null;
        while (cur != null) {
            rightMost = cur.left;
            if (rightMost == null) {
                System.out.println(cur.val);//唯一一次遍历,打印
                cur = cur.right;
            }else {
                while (rightMost.right != null && rightMost.right !=cur) {
                    rightMost = rightMost.right;
                }
                if (rightMost.right == cur) {
                    //说明是第二遍到 cur 节点
                    System.out.println(cur.val);//第二次出现,打印
                    rightMost.right = null;
                    cur = cur.right;
                }else {
                    //第一遍来到 cur 节点
                    rightMost.right = cur;
                    cur = cur.left;
                }
            }
        }
    }
}

3.3 后序遍历

对于后序遍历来说,如果某个节点没有左孩子,那就不管了。

如果有左孩子并且是第二次到达该节点,那么就将该节点的左子树的右边界逆序打印

一切完成之后,将整棵树的右边界进行逆序打印

在这里插入图片描述

在这里插入图片描述

如图所示,在 Morris 遍历中, A 和 B 是拥有左子树的节点且是第一次遍历,跳过。D 和 G 都没有左子树,跳过。

又遍历到了 B 节点,这是第二次遍历到 B 节点,并且 B 节点还是第二次遍历到,所以就逆序打印 B 节点左子树的右边界 G D,后面的节点就依次根据这些内容来判断。

直到遍历完全之后,逆序打印整棵树的右边界

注:在第二遍遍历到该节点的时候,需要先进行还原二叉树,即将对应的 Rightmost 的 right 指向 null,然后在进行逆序打印右边界操作

public static void postMorris (TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    TreeNode rightMost = null;
    while (cur != null) {
        rightMost = cur.left;//开始找左子树中最右的节点
        if (rightMost == null) {
            //没有左孩子
            cur = cur.right;
        }else {
            //有左孩子
            while (rightMost.right != null && rightMost.right !=cur) {
                rightMost = rightMost.right;
            }
            if (rightMost.right == cur) {
                //说明是第二遍到 cur 节点
                rightMost.right = null;//先让其回归正常
                printFunc(cur.left);//逆序打印
                cur = cur.right;
            }else {
                //第一遍来到 cur 节点
                rightMost.right = cur;
                cur = cur.left;
            }
        }
    }
    printFunc(root);//最后总的逆序打印整棵树的右边界
}
//提供了一个树的根节点,逆序打印这棵树的右边界
public static void printFunc(TreeNode root) {
    TreeNode tail = reverseRightEdge(root);
    TreeNode cur = tail;//进行一个反转
    while (cur != null) {
        System.out.println(cur.val);
        cur = cur.right;
    }
    reverseRightEdge(tail);//给它反转回去
}
//反转右边界(类似于反转单链表)
public static TreeNode reverseRightEdge(TreeNode root) {
    TreeNode cur = root;
    TreeNode prev = null;
    while (cur != null) {
        TreeNode curNext = cur.right;
        cur.right = prev;
        prev = cur;
        cur = curNext;
    }
    return prev;
}

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

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

相关文章

头条号权重高有什么优势?头条权重在线查询

头条号权重是根据你的关键词排名、预估流量等综合评估计算出的一个"权重值"&#xff0c;关键词指数越大&#xff0c;排名越好&#xff0c;预估的流量就越多&#xff0c;权重也就越高。 如果是查询头条号权重较少&#xff0c;可以看看这3个方法&#xff1a; 1、指…

flutter 环境搭建

一、简介 Flutter 是谷歌开发的一款开源、免费的&#xff0c;基于 Dart 语言的U1框架,可以快速在i0S和Android上构建高质量的原生应用。 它最大的特点就是跨平台和高性能。Dart是由谷歌&#xff0c;在2011 年开发的计算机编程语言&#xff0c;它可以被用于Web、服务器、移动应…

腾讯云轻量应用服务器安装和配置宝塔 Linux 面板腾讯云专享版

宝塔 Linux 面板腾讯云专享版由腾讯云与堡塔公司联合开发&#xff0c;专享版在已支持普通版所有功能的基础上&#xff0c;还默认集成腾讯云对象存储、文件存储、内容分发网络和 DNS 解析插件。插件具备如下功能&#xff1a; 支持将对象存储的存储桶挂载到轻量应用服务器实例&a…

IDEA技巧:如何根据注释生成swagger注解

相信大家在进行java项目开发&#xff0c;肯定会接触到swagger的&#xff0c;一款动态生成api文档的神奇&#xff0c;只需要在api上面加上注解&#xff0c;就可以生成文档&#xff0c;现在我简单介绍下swagger的快速入门&#xff0c;最后再说下如何根据注释快速生成这些烦人的注…

(模板)矩阵乘法:斐波那契数列问题

在数学上&#xff0c;斐波那契数列以如下被以递推的方法定义&#xff1a; F(1)1&#xff0c;F(2)1, F(n)F(n-1)F(n-2&#xff09;&#xff08;n>3&#xff0c;n∈N*&#xff09;。 由以上推理公式&#xff0c;可以求得任何一项的斐波那契数列值。 弊端&#xff1a;斐波那…

UI自动化测试-pytest框架

在进行UI自动化测试的时候&#xff0c;我们需要工具来对测试用例进行收集&#xff0c;执行&#xff0c;标记&#xff0c;参数化。pytest就是这样一个工具。 pytest实际是python的一个单元测试框架&#xff0c;其他还有如unittest等&#xff0c;它可以实现按照规则搜索测试用例…

国产化服务环境中使用gunicorn部署Flask应用并配置开机自启

背景 服务端由第三方部署了一个基于 darknet &#xff08;一个较为轻型的完全基于C与CUDA的开源深度学习框架&#xff09;的识别算法服务&#xff0c;通过 Flask 的 Web 服务对业务服务暴露 API 接口。作为测试&#xff0c;一开始是直接通过 python3 app.py 的命令行启动的服务…

Ubuntu安装Anaconda详细步骤

本文主要讲述了在Ubuntu中安装anaconda的具体步骤。 准备环境&#xff1a;Ubuntu&#xff0c;Anaconda3 一、安装Anaconda3 在清华镜像下载Linux版本的anaconda&#xff1a; https://mirrors.bfsu.edu.cn/anaconda/archive/我选择的是Anaconda3-2022.10-Linux-x86_64.sh 下…

数组(7)

目录 1、一维数组 1、数组的创建 2、数组的初始化 3、一维数组的使用 4、一维数组在内存中的存储 2、二维数组 1、二维数组的创建 2、二维数组的初始化 3、二维数组的使用 4、二维数组在内存中的存储 3、数组越界 4、数组作为函数参数 1、冒泡排序&#xff1a; 5…

【学习笔记12.24】关于事务你必须知道的几件事

文章目录事务基础知识什么是事务&#xff1f;开启事务事务隔离级别事务基础知识 在MySQL中&#xff0c;只有InnoDB存储引擎是支持事务的。 什么是事务&#xff1f; 事务是逻辑操作的最小单元&#xff0c;使数据从一个状态转变为另一个状态。 也可以通过事务四大特性ACID来更…

SAP ERP 里的 Costing Sheet 成本核算表

有朋友在我的知识星球里向我提问&#xff1a; 请您帮忙讲一下这个AP0100的costing sheet rows这里都表示什么意思吗&#xff1f;比如row10、base Z010、overhead啥、描述、from、to row、credit都说明了什么&#xff0c;能够实现上面&#x1f446;&#x1f3fb;的目标吗&#x…

fpga实操训练(vga测试)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 我自己读书那会&#xff0c;买的电脑还是以台式机居多&#xff0c;平板显示器也是才刚刚流行、且价格较高&#xff0c;视频接口也是以VGA为主。不像…

Linux搭建TFTP服务

TFTP是简单文件传输协议,是一个基于UDP协议实现的用在客户及和服务器之间进行简单文件传输的协议,适用于开销不大,不复杂的应用场景。TFTP协议专门为小文件传输而设计,只能从服务器获取文件,或者客户端往服务器写入文件,但是不能进行认证也不能列出目录。 1、安装tftp服…

RV1126笔记十五:吸烟行为检测及部署<三>

若该文为原创文章,转载请注明原文出处。 训练并测试(windows) 一、yolov5安装 1、下载rk优化后的yolov5 git clone https://github.com/airockchip/yolov5.git 下载后,我是放到E:\miniconda3\envs目录下,miniconda3是安装miniconda的目录。可以放到其他地方,后续操作需要…

MySQL提高批量insert的性能

一. 使用批量插入&#xff0c;将多条单独的 insert 合并成一次操作 即&#xff1a;insert into table values (a1, b1, c1), (a2, b2, c2); 解析&#xff1a;将多条 insert 合并后&#xff0c;减少MySQL日志量(即MySQL的 binlog 和 innodb 的事务日志)&#xff0c;降低日志刷…

Python pandas有好几百个库函数,你都用过吗(1)

对Python的 pandas 库所有的内置元类、函数、子模块等全部浏览一遍&#xff0c;然后挑选一些重点学习一下。我安装的库版本号为1.3.5&#xff0c;如下&#xff1a; >>> import pandas as pd >>> pd.__version__ 1.3.5 >>> print(pd.__doc__)pandas…

54三数之和55 56有无重复元素的全排列

54 三数之和 首先想到的就是之前的两数之和&#xff0c;只要在外层遍历一遍&#xff0c;对每个元素用之前的两数之和的哈希做法&#xff0c;就刚好是O(n^2) 但是有坑的地方在于需要去重&#xff0c;并且输出的三元组也是需要顺序的&#xff01;&#xff01;然后我用set去重和重…

c语言复习之预编译(十四)

1.以#开头的行&#xff0c;都称为编译器指令 #define定义宏#if #else #elif #endif条件编译#ifdef #ifndef判断是否定义了某个宏#error错误#program设定状态或指定完成&#xff08;编译器&#xff09;#undef取消宏定义 2.预定义宏 __LINE__行号__FILE__源文件名__DATE__创建…

你以为架构师天天就画图写PPT吗,告诉你其他事儿多了去了~

V-xin&#xff1a;ruyuan0330 获得600页原创精品文章汇总PDF 目录 一、多系统订阅数据回顾二、核心数据的监控系统三、电商库存数据如何监控四、数据计算链路追踪五、百亿流量下的数据链路追踪六、自动化数据链路分析七、下篇预告 上篇文章《为什么我建议线上高并发量的代码&a…

rip综合实验

目录 1.拓扑图 2.要求 3.要求分析 4.主要配置 5.测试 6.实验总结 1.拓扑图 2.要求 R1代表运营商&#xff0c;R1远程登录R2实际登录R9R3访问R7的环回&#xff0c;实际走下面全网可达 3.要求分析 将R2包括右边所有设备理解为一个局域网&#xff0c;在R2的出接口上配置NAT…