`
qindongliang1922
  • 浏览: 2180717 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
7265517b-f87e-3137-b62c-5c6e30e26109
证道Lucene4
浏览量:117396
097be4a0-491e-39c0-89ff-3456fadf8262
证道Hadoop
浏览量:125814
41c37529-f6d8-32e4-8563-3b42b2712a50
证道shell编程
浏览量:59779
43832365-bc15-3f5d-b3cd-c9161722a70c
ELK修真
浏览量:71222
社区版块
存档分类
最新评论

如何轻松理解二叉树的深度遍历策略

    博客分类:
  • JAVA
阅读更多





我们知道普通的线性数据结构如链表,数组等,遍历方式单一,都是从头到尾遍历就行,但树这种数据结构却不一样,我们从一个节点出发,下一个节点却有可能遇到多个分支路径,所以为了遍历树的全部节点,我们需要借助一个临时容器,通常是栈这种数据结构,来存储当遇到多个分叉路径时的,存暂时没走的其他路径,等走过的路径遍历完之后,再继续返回到原来没走的路径进行遍历,这一点不论在递归中的遍历还是迭代中的遍历中其实都是一样的,只不过递归方法的栈是隐式的,而我们自己迭代遍历的栈需要显式的声明。


树遍历的思想总体分为两种思路:

(一)深度优先遍历(Depth-First-Search=>DFS)

1,前序遍历(Pre-order Traversal)

遍历规则:先根节点,然后左子树,最后右子树

2,中序遍历(In-order Traversal)

遍历规则:先左子树,然后根节点,最后右子树

3,后序遍历(Post-order Traversal)

遍历规则:先左子树,然后右子树,最后根节点。

(二)广度优先遍历(Breadth-First-Search=>BFS)

1, 层级遍历(level order traversal)


我们来看一个普通二叉树:







这里简单说下为什么拿二叉树举例,这是因为在实际开发中,我们使用的树形结构里面,二叉树是最为广泛的,比如AVL树,红黑树,堆等,这些是非常高效的面向内存的数据结构,比较易于理解和使用。
除了二叉树之外,这里还有面向磁盘的多叉树数据结构B树,通常用在文件系统或者数据库系统的索引。
我们来看一下二叉树的代码表示:

```
    class Node<T>{
        public T data;
        public Node<T> left;
        public Node<T> right;
        }
```





上面的代码是一个基于泛型的二叉树模型表示,代表二叉树的一个节点,里面有一个data字段,用来保存该节点的数据,此外还有左孩子节点和右孩子节点,分别表示二叉树的两个分支,这里我要提醒各位一下,虽然在代码上表示是单独的左右两个节点,但是正确的理解策略是把这两个节点,分别看成是一棵树,也即左子树和右子树,理解这一点至关重要,因为这是一个嵌套的定义,每个左,右子节点下面都可能还有无数个类似于这个节点本身的结构,所以不能将其仅仅看成一个节点,而应该看成是一颗树,只有这样思考,才能更加容易的看清二叉树的遍历策略,否则有可能陷入误区,导致很难理解各种遍历策略,尤其还是在递归的遍历的算法中。

递归虽然可以写出非常简洁的代码,但是想要理解和看清递归的本质可不是一件容易事。




在二叉树的深度遍历的三种遍历策略里面,有一个共性,那就是无论哪种策略,都是先遍历左子树,然后再右子树。

这里以中序遍历为例子,来思考下遍历的过程。首先在深度遍历优先的思想里,我们从抽象的角度,总是可以把一颗二叉树给切分成三部分,分别是根节点,左子树,右子树。

如下图:




如果是中序遍历,那么这里的遍历规则就是,先左子树,然后根节点,最后是右子树。

按照这个顺序,我们把这三部分数据,放在一个栈里,如下图所示:





然后,遍历的过程就是出栈的过程,但是大家不要忘记一点,如果栈里的结构仍然是一颗子树,我们就仍然需要按照中序遍历的规则,来继续拆分数据入栈,直到这颗子树,被分解成一个个叶子节点。在上图的栈里,我们发现栈顶部分仍然是一颗子树,所以我需要继续拆分,按照先左,再根,最后右来拆分,继续入栈,最后图示如下:





这时候,整棵树都拆分成了叶子节点,我们直接出栈每一个节点,就完成了中序的遍历:


```
5 -> 12 -> 6 -> 1 -> 9
```


同理,对于前序遍历和后序遍历也是如此,这里就不再详细介绍了,感兴趣的朋友,可以自己画画图来帮助理解。

下面我们来看看如何在Java中实现,三种深度遍历方式:

递归版本:

前序遍历:
```
    public static void myPreOder(Node<Integer> root) {
        if (root == null) {
            return;
        }
        System.out.print(root.data+" ");//根节点
        myPreOder(root.left);//全部遍历完左子树
        myPreOder(root.right);//全部遍历完右子树
    }
```


中序遍历:

```
    public static void myInOder(Node<Integer> root) {
        if (root == null) {
            return;
        }
        myInOder(root.left);//全部遍历完左子树
        System.out.print(root.data+" ");//根节点
        myInOder(root.right);//全部遍历完右子树
    }
```


后序遍历:

```
    public static void myPostOder(Node<Integer> root) {
        if (root == null) {
            return;
        }
        myPostOder(root.left);//全部遍历完左子树
        myPostOder(root.right);//全部遍历完右子树
        System.out.print(root.data+" ");//根节点
    }
```


输出的结果分别是:

```
1 12 5 6 9(前序) 
5 12 6 1 9(中序)
5 6 12 9 1(后序)
```



从上面的能够看到,递归的代码非常简洁,但是如果不明白原理只会看的一头雾水。递归能够工作的前提是编程语言为递归的方法,隐式的创建了栈容器,每一次方法的递归调用都相当于作了一次压栈操作(递),而当条件不满足时会进行出栈操作(归)。


为了帮助理解递归的工作方式,我们接着用循环迭代+创建显式的栈容器来实现同样的前,中,后序遍历策略:

迭代版本:


前序遍历:


```
    public static void myIterativePreOder(Node<Integer> root) {
        if (root == null) {
            return;
        }
        Stack<Node<Integer>> stack = new Stack();
        stack.add(root);
        while (!stack.isEmpty()) {
            Node<Integer> temp = stack.pop();
            System.out.println(temp);
            if (temp.right != null) {
                stack.push(temp.right);
            }
            if (temp.left != null) {
                stack.push(temp.left);
            }
        }
    }
```


中序遍历:

```
    public static void myIterativeInOder(Node<Integer> root) {
        if (root == null) {
            return;
        }
        Stack<Node<Integer>> stack = new Stack();
        Node<Integer> curr = root;
        while (!stack.isEmpty() || curr != null) {

            if (curr != null) {
                stack.push(curr);
                curr = curr.left;
            } else {
                curr = stack.pop();
                System.out.println(curr);
                curr = curr.right;
            }

        }
    }

```


后序遍历:

```
   public static void myIterativePostOder(Node<Integer> root) {
        if (root == null) {
            return;
        }
        Stack<Node<Integer>> stack = new Stack();
        stack.push(root);

        LinkedList<Integer> list = new LinkedList<>();
        while (!stack.empty()) {

            Node<Integer> curr = stack.pop();
            list.addFirst(curr.data);

            if (curr.left != null) {
                stack.push(curr.left);
            }

            if (curr.right != null) {
                stack.push(curr.right);
            }

        }
        System.out.println(list);

    }
```


相比递归版本,迭代版本代码略冗长,但是非常容易理解。另外一个优点在于迭代版本的方式是不容易导致栈溢出的问题,而递归版本则很容易导致栈溢出。参考迭代版本的遍历方式再结合我们前面的分析策略,很容易就能理解二叉树的遍历思路和同样功能的递归算法。


深度遍历是遍历二叉树最常见的策略,本篇文章结合实际例子和图示,通俗易懂的介绍了深度遍历几种策略的思想,理解二叉树的遍历关键点在于,要把定义模型的左右节点,分别看成是两棵树,在遍历过程中,如果发现子节点仍然是棵树,我们就需要优先继续拆分这颗子树,直到找到叶子节点,找叶子节点的过程其实就是在递归的压栈,当找到叶子节点之后,就从开始原路返回,这个过程就是出栈,至于前,中,后序的遍历,无非是遍历的顺序的问题,掌握这一点,就能很容易理解遍历的过程,此外二叉树的遍历还有广度优先遍历算法,也称层级遍历,这个也非常容易理解,就是从顶开始,一层层顺序向底部遍历,相比深度遍历的弯弯绕绕,广度遍历更符合人的思考过程,一点都不烧脑,后面的文章,会单独介绍广度优先遍历的思想和实现,刚兴趣的同学,可以先研究一下。









  • 大小: 10.9 KB
  • 大小: 20.2 KB
  • 大小: 12.6 KB
  • 大小: 8.1 KB
  • 大小: 10.1 KB
  • 大小: 43.5 KB
0
0
分享到:
评论

相关推荐

    《数据结构课程设计》二叉树基本操作实验程序(10种基本操作)

    6. **查找结点**:在二叉树中查找特定值的节点通常采用递归或非递归的搜索算法,如二分查找(适用于有序二叉树)或简单的遍历策略。 7. **求某结点的子孙个数**:子孙是指一个节点的所有子节点以及它们的子节点,...

    数据结构:二叉树(链式存储)

    以上就是关于“数据结构:二叉树(链式存储)”的主要内容,包括了二叉树的链式存储结构,递归创建二叉树的方法,以及三种遍历策略的实现。这些知识对于理解和操作二叉树至关重要,也是数据结构和算法学习中的基础...

    数据结构——二叉树的实现.zip

    理解二叉树的不同实现方式有助于我们更好地设计和优化这些应用。在这个压缩包里,你可能找到包括但不限于以下内容:二叉链表和左子右兄弟的C++或Java代码实现、相关算法的伪代码、详细的解释文本,甚至可能有示例图...

    erchashu.zip_叶子节点

    1. 求宽度(层次遍历):宽度优先搜索(BFS)是一种遍历策略,从根节点开始,逐层访问所有节点。在BFS过程中,叶子节点会最后被访问,这使得我们可以很容易地计算出叶子节点的数量。 2. 求深度:深度优先搜索(DFS...

    图遍历的演示系统

    BFS 在寻找最短路径问题(如二叉树的层次遍历、最短路径问题)中非常有效。 在实现这些算法时,我们还需要考虑一些额外的问题,如: - 访问标记:为了防止重复访问同一个节点,我们需要为每个节点设置一个访问标记...

    腾讯校园招聘C语言笔试题.docx

    【知识点详解】 ...计算二叉树深度时,要掌握递归的运用;对于二进制计数,要熟悉位操作;层次遍历则需要掌握广度优先搜索策略。这些都是编程面试中常见的问题,体现了应聘者对基础数据结构和算法的掌握程度。

    表达式计算程序

    深度优先搜索(DFS)或广度优先搜索(BFS)是常见的遍历策略。在这个项目中,我们可能采用了前序遍历,因为这种方式便于处理运算符的优先级和括号。前序遍历的顺序是“根-左-右”,对于表达式树来说,这意味着先处理...

    数据结构课程设计指导书

    这些遍历方法有助于理解和操作二叉树结构。 4. **数组应用**: - 二维数组的表示和操作:数组是一种基本的数据结构,可以用于存储和处理多维数据。 - 图形化输出:将数组以图形方式显示,可以帮助可视化数据。 5...

    数据结构考研课件

    此外,对于特定的数据结构,如树和图,理解它们的遍历策略(如前序、中序、后序遍历和深度优先、广度优先遍历)也很关键。 在考研复习过程中,考生可以通过做习题、编写代码和分析实际问题来巩固和提升数据结构知识...

    第八 递归PPT学习教案.pptx

    3. **问题的解法满足递归的性质**:采用分治策略,将复杂问题分解为较小的子问题,如二叉树的遍历(前序遍历、中序遍历、后序遍历)。 递归在实际编程中有很多应用,例如: - **斐波那契数列**(Fibonacci ...

    leetcode285-CodingPractice:编码实践

    通过这些LeetCode挑战,开发者可以深入理解二叉树的各种操作,包括遍历、搜索、插入、删除以及特定属性的查找等。这对于系统设计和优化,尤其是处理大规模数据结构时,是至关重要的。同时,熟悉这些基本算法也是准备...

    throu.js:简单的树遍历库

    `throu.js` 是一个专注于树形数据结构遍历的JavaScript库,它提供了多种遍历策略,包括深度优先和广度优先,以及自定义的回调机制,让开发者能灵活地处理复杂的树形数据。通过简单的API,`throu.js` 能够轻松集成到...

    数据结构与算法分析(Java版)

    书中讲解了图的表示方法(邻接矩阵和邻接表)、图的遍历算法(深度优先搜索和广度优先搜索),以及图的相关算法,如最短路径算法和最小生成树算法。 - **加权图(Weighted Graphs)**:第十四章深入讨论了加权图,...

    JAVA TREE

    对于复杂的数据结构,如多叉树,我们可能需要自定义更复杂的遍历策略。此外,还有一些特定类型的树,如平衡二叉树(AVL树、红黑树等),它们保持了特定的平衡条件,以确保操作(如查找、插入和删除)的性能。 在...

    algorithm_python教程_源码.zip

    - 深度优先搜索(DFS):遍历或搜索图的一种方法,沿着树的深度遍历图的节点,尽可能深地搜索分支。 - 广度优先搜索(BFS):遍历图时,先访问所有离根节点近的节点,再访问远的节点。 - 最短路径算法:如...

    数据结构与算法分析C++ 描述(第3版)

    3. **树和二叉树**:具有层次关系的数据结构,特别是二叉树的遍历算法(前序、中序、后序、层序)和二叉搜索树的特性。 4. **平衡树和堆**:如AVL树、红黑树以及堆(Heap)的性质和堆排序,这些数据结构在保持特定...

    《算法导论》第三版英文版-带书签目录文字版1313页完整版

    8. **分治法和贪心策略**:理解这些高级算法设计策略,可以解决很多看似无从下手的问题。 9. **递归与回溯**:通过递归函数理解和解决递归问题,以及在约束条件下进行回溯搜索的策略。 10. **复杂度理论**:介绍...

Global site tag (gtag.js) - Google Analytics