当前位置: 首页 > news >正文

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

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

二叉树结构说明

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

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 的应用。

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

相关文章:

  • 【解包裹】基于最小二乘法实现解包裹附matlab代码
  • vim如何进行批量化注释及取消,也在1024表明自己算十分之一的程序员
  • 1024程序员节|【MySQL从入门到精通】【高级篇】(二十七)外连接和内连接如何进行查询优化呢?join的原理了解一波
  • 57.(前端)删除用户操作
  • 手动搭建K8S环境
  • ESP32-C3入门教程 网络篇⑦——基于esp_http_client实现HTTP的POST/GET/PUT/DELETE请求服务器的第三种方法
  • linux Shell入门
  • div与表格应用实例——计算器布局
  • 爬虫的三大库
  • 基于Java+Springboot+Vue+elememt社区疫情防控系统设计和实现
  • 【Linux】yum 与 vim 的基本使用
  • SpringCloudAlibaba【六】微服务架构下的秒杀案例
  • 字节一面:TCP 三次握手,问的好细!
  • 一个功能齐全的,多用途管理后台模板
  • 【C语言】三子棋小游戏
  • 【翻译】Mashape是如何管理15000个API和微服务的(三)
  • 4. 路由到控制器 - Laravel从零开始教程
  • CSS中外联样式表代表的含义
  • E-HPC支持多队列管理和自动伸缩
  • ES6简单总结(搭配简单的讲解和小案例)
  • MobX
  • Nacos系列:Nacos的Java SDK使用
  • Solarized Scheme
  • storm drpc实例
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • Webpack 4x 之路 ( 四 )
  • 动态魔术使用DBMS_SQL
  • 码农张的Bug人生 - 见面之礼
  • # 飞书APP集成平台-数字化落地
  • (07)Hive——窗口函数详解
  • (10)Linux冯诺依曼结构操作系统的再次理解
  • (DenseNet)Densely Connected Convolutional Networks--Gao Huang
  • (二十五)admin-boot项目之集成消息队列Rabbitmq
  • (分布式缓存)Redis哨兵
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (十一)手动添加用户和文件的特殊权限
  • (转)http-server应用
  • (转)创业家杂志:UCWEB天使第一步
  • (状压dp)uva 10817 Headmaster's Headache
  • .mysql secret在哪_MySQL如何使用索引
  • .net core 3.0 linux,.NET Core 3.0 的新增功能
  • .net core 6 使用注解自动注入实例,无需构造注入 autowrite4net
  • .NET Framework与.NET Framework SDK有什么不同?
  • .net on S60 ---- Net60 1.1发布 支持VS2008以及新的特性
  • .NET/C# 编译期能确定的字符串会在字符串暂存池中不会被 GC 垃圾回收掉
  • .NET开源项目介绍及资源推荐:数据持久层
  • .sh 的运行
  • .xml 下拉列表_RecyclerView嵌套recyclerview实现二级下拉列表,包含自定义IOS对话框...
  • @RequestMapping处理请求异常
  • [ vulhub漏洞复现篇 ] Jetty WEB-INF 文件读取复现CVE-2021-34429
  • [AIGC codze] Kafka 的 rebalance 机制
  • [BSGS算法]纯水斐波那契数列
  • [BZOJ 4034][HAOI2015]T2 [树链剖分]
  • [C++]18:set和map的使用
  • [COGS 622] [NOIP2011] 玛雅游戏 模拟