二叉树广度优先搜索、深度优先搜索(前序、中序、后序)遍历,动图详解-Java/Kotlin双版本代码

news2024/11/20 13:34:01

自古逢秋悲寂寥,我言秋日胜春朝

二叉树结构说明

本博客使用树节点结构,如下所示:

Kotlin 版本

class TreeNode(var value: String, var leftNode: TreeNode? = null, var rightNode: TreeNode? = null)

Java 版本

class TreeNode(){
    public int value;
    public TreeNode rightNode;
    public TreeNode leftNode;
}

定义:树(Tree)是n(n>=0)个节点的有限集合。当n=0时,它为空树,否则为非空树。

对于非空树:

  • 有且只有一个根节点
  • 除根结点以外的其余结点可分为k(k>0)个互不相交的有限集T1,T2,…,T,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

树的基本术语

在这里插入图片描述

  • 结点:树中的一个独立单元。包含一个数据元素及若干指向其子树的分支,如上图中的A、B、C、D等。
  • 结点的度:结点拥有的子树数。例如,A的度为3,B的度为2,F的度为1,G的度为0。
  • 树的度:树的度是树内各结点度的最大值。上图中所示的树的度为3
  • 叶子:度为0的结点称为叶子或终端结点。结点K、L、G、M、I、J都是树的叶子。
  • 非终端结点:度不为0的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点。
  • 双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,上图的E节点和K节点。
  • 兄弟:同一个双亲的孩子之间互称兄弟。例如,H、I和J互为兄弟。
  • 堂兄弟:双亲在同一层的结点。例如,结点F、G、H互为堂兄弟。
  • 祖先:从根到该结点所经分支上的所有结点。例如,M的祖先为A、D和H.
  • 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。如B的子孙为E、K、L和F。
  • 层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加1。
  • 树的深度:树中结点的最大层次称为树的深度或高度。图中所示的树的深度为4
  • 有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。

上述树的概念了解一下即可,主要是二叉树以及红黑树。

二叉树

介绍

二叉树:是每个节点最多有两颗子树的树结构。通常被称作左右子树。

性质:

  • 二叉树的每个节点至多只有两颗子树(不存在度数大于2的结点)。其子树有左右之分。
  • 二叉树第i层最多有2^(i-1)个节点。
  • 深度为k的二叉树最多有2^k-1个节点。

满二叉树:一颗深度为K的二叉树且有2^k-1个节点。

完全二叉树:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

在这里插入图片描述

二叉树深度优先搜索

对于二叉树来说,常用深度优先搜索进行遍历。其又可以细分为前序遍历、中序遍历以及后序遍历。

对于如下一颗二叉树,其前序、中序、后序遍历结果如下:

在这里插入图片描述

下面对上述前序、中序、后序遍历进行详细解析

前序:

动图演示

在这里插入图片描述

遍历顺序为:跟左右

在整棵树的遍历过程中,先获取其根节点的值,接着为其左节点的值,之后为其右节点的值。

对于上述的树来说,所以我们先获取到根节点的值为A,接下来遍历A的左节点,B节点,所以我们可以获取到B节点的值B;但是对于结点B、D、E来说,节点B是它们的根节点,所以根据跟左右接下需要遍历的是B节点的左节点,D节点,可以获取到值D。如果我们把D节点当作根节点来看的话,因为D节点没有左右节点,我们可以默认D节点的跟左右已经走完了。所以我们回到D节点的父节点B节点。对于B节点来说,此时已经遍历了根左,接下来就是其右节点E节点。我们遍历E节点获取其值E。此时对于A节点来说,其左子树已经遍历完了,根据跟左右,我们遍历器右子树,和前面遍历类似,我们可以获取到值C、F、G

所以最终前序遍历的结果为:A、B、D、E、C、F、G

代码说明

遍历二叉树之前,我们先构造一个二叉树,后续不同的遍历方式,均调用该函数构造一个上图所示的二叉树。

Kotlin 版本

object Helper {
    /**
     * 创建二叉树
     */
    fun createBinaryTree(): TreeNode {
        return TreeNode(
            value = "A",
            leftNode = TreeNode(
                value = "B",
                leftNode = TreeNode(value = "D"),
                rightNode = TreeNode(value = "E")
            ),
            rightNode = TreeNode(
                value = "C",
                leftNode = TreeNode(value = "F"),
                rightNode = TreeNode(value = "G")
            )
        )
    }
}

Java 版本

class Helper {
    /**
     * 创建二叉树
     */
    public static TreeNode createBinaryTree() {
        return new TreeNode(
                "A",
                new TreeNode(
                        "B",
                        new TreeNode("D", null, null),
                        new TreeNode("E", null, null)
                ),
                new TreeNode(
                        "C",
                        new TreeNode("F", null, null),
                        new TreeNode("G", null, null)
                )
        );
    }
}

递归写法

Kotlin 版本

fun binaryTreePreIterator(node: TreeNode?) {
    if (node != null) {
        println(node.value)
        binaryTreePreIterator(node.leftNode)
        binaryTreePreIterator(node.rightNode)
    }
}

//使用
binaryTreePreIterator(Helper.createBinaryTree())
//结果
A B D E C F G

Java 版本

    void binaryTreePreIterator(TreeNode node) {
        if (node != null) {
            System.out.printf(node.getValue());
            binaryTreePreIterator(node.getLeftNode());
            binaryTreePreIterator(node.getRightNode());
        }
    }

前序遍历的递归写法非常简单,根据跟左右的形式,依次进行递归调用即可。

非递归写法

那么如何将上述递归代码改造成非递归的写法呢?

递归很好理解在于,比如此时将父节点的左子树遍历完成之后,可以自己回到父节点遍历的函数处,接着执行遍历其右节点。

所以如果想要改造成非递归函数,则必须有一个数据结构用来记录节点的遍历信息,需要将未遍历完的节点按照执行顺序存起来。这里我们使用栈来进行保存。

fun binaryTreePreIteratorByStack(node: TreeNode?): ArrayList<String> {
    val result = ArrayList<String>()

    val stack = java.util.ArrayDeque<TreeNode>()
    var currentNode = node;
    while (currentNode != null || !stack.isEmpty()) {
        while (currentNode != null) {
            result.add(currentNode.value)
            stack.push(currentNode)
            currentNode = currentNode.leftNode
        }
        currentNode = stack.pop().rightNode
    }

    return result
}

//使用
binaryTreePreIteratorByStack(Helper.createBinaryTree()).forEach {
    println(it)
}
//结果
A B D E C F G

Java 版本

ArrayList<String> binaryTreePreIteratorByStack(TreeNode node) {
        ArrayList<String> result = new ArrayList<>();

        ArrayDeque<TreeNode> stack = new ArrayDeque <> ()
        TreeNode currentNode = node;
        while (currentNode != null || !stack.isEmpty()) {
            while (currentNode != null) {
                result.add(currentNode.getValue());
                stack.push(currentNode);
                currentNode = currentNode.getLeftNode();
            }
            currentNode = stack.pop().getRightNode();
        }

        return result;
    }

中序:

在这里插入图片描述

遍历顺序为:左根右

在整棵树的遍历过程中,先遍历其左子树,之后获取自身的值,接着遍历右子树。

对于上述的树来说,A的左节点为B,对于结点B、D、E来说,节点B是它们的根节点,继续根据左根右,我们获取到D节点,接着左根右D节点的左节点为null,接着遍历其根节点也就是自己,此时我们获取到值DD的右节点也为null,所以对于B节点来说,其左子树也就遍历完了,接着遍历其自身可以获取到值B,接着遍历右节点E。同理对于A节点来说,其左子树遍历完毕,接着遍历其自身,获取到值A。同理遍历其右子树即可。

所以最终前序遍历的结果为:D、B、E、A、F、C、G

代码说明

递归写法

fun binaryTreeMiddleIterator(node: TreeNode?) {
    if (node != null) {
        binaryTreeMiddleIterator(node.leftNode)
        println(node.value)
        binaryTreeMiddleIterator(node.rightNode)
    }
}
//使用
binaryTreeMiddleIterator(Helper.createBinaryTree())
//结果
D B E A F C G

Java 版本

    void binaryTreeMiddleIterator(TreeNode node) {
        if (node != null) {
            binaryTreeMiddleIterator(node.getLeftNode());
            System.out.printf(node.getValue());
            binaryTreeMiddleIterator(node.getRightNode());
        }
    }

类似于前序遍历的写法,根据左根右的形式,依次进行递归调用即可。

非递归写法

fun binaryTreeMiddleIteratorByStack(node: TreeNode?): ArrayList<String> {
    val result = ArrayList<String>()

    val stack = java.util.ArrayDeque<TreeNode>()
    var currentNode = node
    while (currentNode != null || !stack.isEmpty()) {
        while (currentNode != null) {
            stack.push(currentNode)
            currentNode = currentNode.leftNode
        }
        val treeNode = stack.pop()
        result.add(treeNode.value)
        currentNode = treeNode.rightNode
    }

    return result
}
//使用
binaryTreeMiddleIteratorByStack(Helper.createBinaryTree()).forEach {
	println(it)
}
//结果
D B E A F C G
ArrayList<String> binaryTreeMiddleIteratorByStack(TreeNode node) {
        ArrayList<String> result = new ArrayList<>();

        ArrayDeque<TreeNode> stack = new ArrayDeque <> ()
        TreeNode currentNode = node;
        while (currentNode != null || !stack.isEmpty()) {
            while (currentNode != null) {
                stack.push(currentNode);
                currentNode = currentNode.getLeftNode();
            }
         	TreeNode treeNode = stack.pop();
       	 	result.add(treeNode.value);
        	currentNode = treeNode.rightNode;
        }

        return result;
    }

同前序遍历一样,我们使用栈保存调用顺序。之后严格根据左根右执行即可。

后序:

在这里插入图片描述

遍历顺序为:左右根

在整棵树的遍历过程中,先遍历其左子树,接着遍历右子树,之后获取自身的值。

对于上述的树来说,A的左节点为B,对于结点B、D、E来说,节点B是它们的根节点,继续根据左根右,我们获取到D节点,接着左根右D节点的左右节点为null,接着遍历其根节点也就是自己,此时我们获取到值D,所以对于B节点来说,其左子树也就遍历完了,接着遍历其右子树可以获取到E,此时对于B来说其左右子树均遍历完毕,所以我们遍历其自身即获取B。同理对于A节点来说,其左子树遍历完毕,接着遍历其右子树,可以获取到F、G、C,最后遍历自己,获取到A

所以最终前序遍历的结果为:D、E、B、F、G、C、A

代码说明

递归写法

fun binaryTreeAfterIterator(node: TreeNode?) {
    if (node != null) {
        binaryTreeAfterIterator(node.leftNode)
        binaryTreeAfterIterator(node.rightNode)
        println(node.value)
    }
}
//使用
binaryTreeAfterIterator(Helper.createBinaryTree())
//结果
D E B F G C A

Java 版本

    void binaryTreeAfterIterator(TreeNode node) {
        if (node != null) {
            binaryTreeAfterIterator(node.getLeftNode());
            binaryTreeAfterIterator(node.getRightNode());
            System.out.printf(node.getValue());
        }
    }

类似于前面的写法,根据左右根的形式,依次进行递归调用即可。

非递归写法

后序遍历的迭代代码要复杂一点。当达到某个节点时,如果之前还没有遍历过它的右子树就得前往它的右子节点,如果之前已经遍历过它的右子树那么就可以遍历这个节点。所以说,此时要根据它的右子树此前有没有遍历过来确定是否应该遍历当前的节点。如果此前右子树已经遍历过,那么在右子树中最后一个遍历的节点应该是右子树的根节点,也就是当前节点的右子节点。可以记录遍历的前一个节点。如果一个节点存在右子节点并且右子节点正好是前一个被遍历的节点,那么它的右子树已经遍历过,现在是时候遍历当前的节点了。

fun binaryTreeAfterIteratorByStack(node: TreeNode?): ArrayList<String> {
    val result = ArrayList<String>()

    val stack = java.util.ArrayDeque<TreeNode>()
    var currentNode: TreeNode? = node
    var preNode: TreeNode? = null
    while (currentNode != null || !stack.isEmpty()) {
        while (currentNode != null) {
            stack.push(currentNode)
            currentNode = currentNode.leftNode
        }
        currentNode = stack.peek()
        if (currentNode.rightNode != null && currentNode.rightNode != preNode) {
            //遍历右子树
            currentNode = currentNode.rightNode
        } else {
            stack.pop()
            result.add(currentNode.value)
            preNode = currentNode
            currentNode = null
        }
    }

    return result
}
     ArrayList<String> binaryTreeAfterIteratorByStack(TreeNode node) {
        ArrayList<String> result = new ArrayList<>();

        ArrayDeque<TreeNode> stack = new ArrayDeque<>()
        TreeNode currentNode = node;
        TreeNode preNode = null
        while (currentNode != null || !stack.isEmpty()) {
            while (currentNode != null) {
                result.add(currentNode.getValue());
                stack.push(currentNode);
                currentNode = currentNode.getLeftNode();
            }
            currentNode = stack.peek();
            if (currentNode.getRightNode() != null && currentNode.getRightNode() != preNode) {
                //遍历右子树
                currentNode = currentNode.getRightNode();
            } else {
                stack.pop();
                result.add(currentNode.getValue());
                preNode = currentNode;
                currentNode = null;
            }
        }

        return result;
    }

二叉树广度优先搜索

广度优先搜索,根据层级进行遍历。

可以依靠队列进行遍历。

比如对于上图所示的数据结构,广度优先搜索的遍历结果即为:A B C D E F G

代码如下:

fun binaryTreeBreadth(node: TreeNode): ArrayList<String> {
    val result = ArrayList<String>()

    val arrayDeque1 = java.util.ArrayDeque<TreeNode>()
    val arrayDeque2 = java.util.ArrayDeque<TreeNode>()
    arrayDeque1.offer(node)
    while (!arrayDeque1.isEmpty() || !arrayDeque2.isEmpty()) {
        while (!arrayDeque1.isEmpty()) {
            val treeNode = arrayDeque1.poll()
            if (treeNode != null) {
                result.add(treeNode.value)
                val leftNode = treeNode.leftNode
                if (leftNode != null) {
                    arrayDeque2.offer(leftNode)
                }
                val rightNode = treeNode.rightNode
                if (rightNode != null) {
                    arrayDeque2.offer(rightNode)
                }
            }
        }
        while (!arrayDeque2.isEmpty()) {
            val treeNode = arrayDeque2.poll()
            if (treeNode != null) {
                result.add(treeNode.value)
                val leftNode = treeNode.leftNode
                if (leftNode != null) {
                    arrayDeque1.offer(leftNode)
                }
                val rightNode = treeNode.rightNode
                if (rightNode != null) {
                    arrayDeque1.offer(rightNode)
                }
            }
        }
    }
    
    return result
}
//使用
binaryTreeBreadth(Helper.createBinaryTree()).forEach {
	println(it)
}
//结果
A B C D E F G

下一节预告,红黑树,以及 TreeSet/TreeMap 的应用。

🙆‍♀️。欢迎技术探讨噢!

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

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

相关文章

经典排序之插入排序

目录 直接插入排序&#xff1a; 基本思路 图解过程 代码 复杂度分析 希尔排序 基本思想 图解过程 代码 复杂度分析 总结 参赛话题&#xff1a;学习笔记 直接插入排序&#xff1a; 基本思路 直接插入排序的工作方式像许多人排序一手扑克牌。开始时&#xff0c;我们的左手…

【Netty 从成神到升仙系列 大结局】全网一图流死磕解析 Netty 源码

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小黄&#xff0c;独角兽企业的Java开发工程师&#xff0c;Java领域新星创作者。&#x1f4dd;个人公众号&#xff1a;爱敲代码的小黄&#x1f4d5;系列专栏&#xff1a;Java设计模式、数据结构和算法&#x…

第八篇 python 面向对象编程

11 面向对象编程 面向对象编程——Object Oriented Programming&#xff0c;简称OOP&#xff0c;是一种程序设计思想。OOP把对象作为程序的基本单元&#xff0c;一个对象包含了数据和操作数据的函数。 面向过程的程序设计把计算机程序视为一系列的命令集合&#xff0c;即一组…

Python攻防-APK批量化Pull与自动化反编译

文章目录前言Pull APK根据包名列表根据手机路径逆向APK自动化反编译findstr检索…总结前言 日常工作过程中&#xff0c;经常会遇到发现新的攻击模式的情况下&#xff0c;需要全量排查手机上所有 APP 的代码是否存在该类代码缺陷。对于复杂的攻击模式而言&#xff0c;往往需要动…

【MyBatis框架】动态SQL

MyBatis之动态SQL 目录MyBatis之动态SQL1. < if > 元素2. < where >3. < choose >,< when >,< otherwise >元素4. < trim >元素5. < set >元素6. < foreach >元素6.1 添加批量数据6.2 批量删除数据7. < SQL >元素8. 小结…

LVS负载均衡群集

企业群集应用 1. 群集的含义 1.1Cluster&#xff0c;群集&#xff0c;集群 2.1由多台主机构成&#xff0c;但对外&#xff0c;只表现为一个整体&#xff0c;只提供一个访问入口&#xff08;域名或ip地址&#xff09;&#xff0c; 相当于一台大型计算机 2.问题出现 互联网…

Sentinel的学习

1、Sentinel控制台的下载 下载地址&#xff1a;https://github.com/alibaba/Sentinel/releases/tag/1.8.3 2、Sentinel控制台的启动 java -jar sentinel-dashboard-1.8.3.jar3、访问 浏览器输入&#xff1a;localhost:8080 账号密码&#xff1a; sentinel/sentinel 4.sprin…

SARScape中用sentinel-1数据做SBAS-InSAR完整流程(1/2)

SARScape中用sentinel-1数据做SBAS-InSAR完整流程1 SABA-InSAR原理简述2 数据采集和预设2.1 SAR数据采集2.2 DEM数据下载与放置2.3 精密轨道数据下载与放置2.4 制作研究区范围矢量2.5 SARscape Preferences预设3 SAR数据预处理3.1 导入数据3.2 optional files设置3.3 参数设置4…

【Git】Git使用的三个场景总结 | 远程仓库到本地 | 本地获取git仓库 | 远程仓库与本地相连接

&#x1f4ad;&#x1f4ad; ✨&#xff1a; git使用的三个场景总结 | 远程仓库到本地 | 本地获取git仓库 | 远程仓库与本地相连接   &#x1f49f;&#xff1a;东非不开森的主页   &#x1f49c;&#xff1a;学习的过程就是不断接触错误&#xff0c;不断提升自己&#xff0c…

Linux 卸载zabbix图文教程

Linux 卸载zabbix图文教程前言1.停止zabbix服务2.卸载zabbix服务2.1查找zabbix所有被安装的rpm包2.2卸载zabbix服务2.3删除所有与zabbix相关的文件&#xff08;配置项等&#xff09;3.卸载数据库3.1查找mariadb所有被安装的rpm包&#xff0c;并删除3.2删除mysql相关配置文件4.卸…

Source Insight4.0中文注释乱码解决方案

一、Source Insight软件介绍 Source Insight是一个面向项目的编程编辑器、代码浏览器和分析器&#xff0c;可帮助您在工作和计划​​时分析代码&#xff0c;具有针对 C/C、C#、Java、Objective-C 等的内置动态分析&#xff0c;深受众多嵌入式软件开发者的喜爱。 二、中文乱码…

复旦-华盛顿大学EMBA 二十年20人丨徐欣:从外企转战民企的变身

复旦大学-华盛顿大学EMBA20周年校友系列访谈。      2008年堪称转折之年&#xff0c;中国举行北京奥运会向全世界展示“和而不同”的理念&#xff0c;入世7年让中国在贸易、金融领域与全球市场紧密相连&#xff0c;一大批最优秀的中国民营企业也加速踏上全球化之路。    …

Web APIs:PC 端网页特效--动画函数封装

动画原理 核心原理&#xff1a;通过定时器 setInterval() 不断移动盒子位置 实现步骤&#xff1a; 1. 获得盒子当前位置 2. 让盒子在当前位置加上1个移动距离 3. 利用定时器不断重复这个操作 4. 加一个结束定时器的条件 5. 注意此元素需要添加定位&#xff0c;才能使用e…

【C语言】三子棋小游戏

&#x1f680; 作者简介&#xff1a;一名在后端领域学习&#xff0c;并渴望能够学有所成的追梦人。 &#x1f40c; 个人主页&#xff1a;蜗牛牛啊 &#x1f525; 系列专栏&#xff1a;初出茅庐C语言 ☀️ 学习格言&#xff1a;眼泪终究流不成海洋&#xff0c;人总要不断成长&am…

Selenium基础 — iframe表单操作

1、什么是iframe表单 实际上就是HTML页面中使用iframe/frame标签&#xff0c;是在当前页面中引用了其他页面的链接&#xff0c;真正的页面数据并没有出现在当前页面源码中&#xff0c;但是在浏览器中我们时看到的。简单理解可以使页面中开了一个窗口显示另一个页面。 我们在We…

谷粒商城-支付业务

目录 商城业务-支付-支付宝沙箱&代码 商城业务-支付-RSA、加密加签、密钥等 商城业务-支付-内网穿透 商城业务-订单服务-整合支付前需要注意的问题 商城业务-订单服务-整合支付 商城业务-订单服务-支付成功同步回调 商城业务-订单服务-订单列表页渲染完成 商城业务…

网络请求+基于Node.js的WebSocket

目录 前言 网络访问配置 1.配置流程 注意事项 使用限制 网络请求详情API wx.request请求数据API ​编辑 wx.uploadFile文件上传API wx.downloadFile文件下载API WebSocket会话API 基于Node.js的WebSocket 为什么WebSocket连接可以实现全双工通信而HTTP连接不行呢&…

git命令记不住?可视化git操作平台Sourcetree入门教程

1、为什么要用Sourcetree 在应届生在参加实习或者工作的时候&#xff0c;往往需要配置各种各样的环境&#xff0c;git肯定是程序员必不可少的分布式版本控制系统&#xff0c;但刚出来工作时往往对git代码不熟悉&#xff0c;老是会忘掉一些命令&#xff0c;所以笔者在此推荐一个…

算法《第四版》笔记整理

算法第四版 先导例子&#xff1a;动态连通性 - 书中1.5 知识点&#xff1a;并查集-一种用于解决动态连通性问题的算法 描述&#xff1a;对于N个对象&#xff0c;有两种操作&#xff1a;1.连接两个对象 2.判断两个对象是否存在连接路径 如巨大的连通性问题&#xff1a; 在分析…

【力扣刷题】Day32——单调栈专题

文章目录单调栈1.每日温度2.下一个更大元素 I3.下一个更大元素II4. 接雨水5.柱状图中最大的矩形单调栈 单调栈基础知识回顾&#xff1a;单调栈与单调队列_塔塔开!!!的博客-CSDN博客_单调栈 单调队列 单调栈一般模板&#xff1a; int[] stk new int[N] //Stack<Integer>…