二叉树的遍历(完结)

二叉树的三种常用遍历方式

     学习过数据结构的同学都清楚,除了层序遍历外,二叉树主要有三种遍历方式:
         1. 先序遍历
         2. 中序遍历
         3. 后序遍历
     关于这三种遍历方式,我不做赘述,** 网上或者书本上,大家可以去了解一下。 ** 这次我们主要总结一下二叉树遍历的算法的递归实现和非递归实现。而重点又落在非递归实现上,因为,递归实现,其实也是书本上的基础知识,只要去查阅机会有所理解。

递归实现

先序遍历的递归实现

     我们先定义一个二叉树的节点结构作为本博文中的通用节点。

public class Node{
    public int value;
    public Node left;
    public Node right;
    
    public Node(int data){
        this.value = data;
    }
}

     我们知道,二叉树的递归遍历无非是对一颗非空的二叉树的每一个节点,做相同的操作(先序、中序、后序)。二叉树的先序遍历就是对每一个节点来先序遍历的动作。代码如下:

public void preOrder(Node head){
    if(head == null)return;
    
    System.out.print(head.value + " ");
    preOrder(head.left);
    preOrder(head.right);
}

中序遍历的递归实现

     同样的,二叉树中序遍历的递归实现,也是建立在同样的思想上。

public void midOrder(Node head){
    if(head == null)return;
    
    midOrder(head.left);
    System.out.print(head.value);
    midOrder(head.right);
}

后序遍历的递归实现

     ** The same idea , not the same recipe. **

public void posOrder(Node head){
    if(head == null)return;
    
    posOrder(head.left);
    posOrder(head.right);
    System.out.print(head.value);
}

关于递归实现的简单总结

     从上面的代码,我们发现,在递归实现上,三种遍历方式的主要区别就是输出当前头结点的值的时机,从而让遍历的顺序产生区别。

非递归实现

用递归方法解决的问题都可以用非递归来实现,因为递归方法无非是实用函数栈来保存信息,如果用自己定义的栈来代替函数栈,就可以实现相同的功能。

先序遍历的非递归实现

     先来大致分析一下非递归方式实现先序遍历的具体过程:
         1. 申请一个新的stack,然后把头结点打入stack中
         2. 从stack中弹出栈顶节点,记为 head ,然后打印 head 的值,然后 ** 把 head 的右孩子压入栈中,再把 head 的左孩子压入栈中(注意先后顺序)。 ** 当然在做这些操作之前,都需要一个 ** 非空判断. ** 。
         3. 重复步骤2,直到 stack 为空。

     到这里,我们贴上一段代码:

public void preOrderNoRecur(Node head){
    if(head != null){
    //新建一个栈空间,存储节点,对应过程中的第一步
        Stack<Node> stack = new Stack<Node>();
        //把头结点压入栈中
        stack.add(head);
        while(! stack.isEmpty()){
            head = stack.pop();
            System.out.print(head.value + " ");
            //分别对弹出节点的右孩子、左孩子判空,按照顺序压入栈中
            if(head.right != null)stack.add(head.right);
            if(head.left != null)stack.add(head.left)
        }
    }
}

** 需要重点理解的就是弹出节点的左右孩子被压入栈的时机。 **建议大家自行画一个图,模拟一下程序运行的过程。

中序遍历的非递归实现

     同样的,我们先分析一下中序遍历非递归实现的具体过程:
         1. 申请一个新的 stack 。** 跟先序遍历不同的是 ** ,我们需要两个值,一个cur,记录当前遍历到的节点的值,一个head,记录当前二叉树的根节点。** 初始时,cur = head ** 。
         2. 把cur代表的节点压入栈中,让cur = cur.left
         3. 重复步骤2,直到 cur 为空时,从 stack 中弹出一个节点,记为 outNode ,打印 outNode 的值。然后让 cur = outNode.right,继续重复步骤2。
         4. 当 stack 为空且 cur 为空时,整个遍历完成。闲话不多说,上代码。

public void midOrderNoRecur(Node head){
    if(head != null){
        Stack<Node> stack = new Stack<Node>();
        Node cur = head;
        while(!stack.isEmpty() || cur != null){
            if(cur != null){
            //步骤二,不停的把以 cur 的左孩子压入栈中
                stack.push(head);
                cur = cur.left;
            }else{
            //当 cur 为空时,我们弹出栈顶元素,然后对其右孩子做相同的事
                Node outNode = stack.pop();
                System.out.print(outNode.value + " ");
                cur = outNode.right;
            }
        }
    }
}

     ** 实际上, ** 我们可以把这段程序中的 cur , outNode都替换成head,这样做可以节约空间,但是在代码清晰度上个人觉得会有所降低。因为是用来总结,所以觉得还是把各个东西写清晰的好。但是也附上都替换成 head 后的代码。

public void midOrderNoRecur(Node head){
    if(head != null){
        Stack<Node> stack = new Stack<Node>();
        while(!stack.isEmpty() || head != null){
            if(head != null){
            //步骤二,不停的把以 head 的左孩子压入栈中
                stack.push(head);
                head = head.left;
            }else{
            //当 head 为空时,我们弹出栈顶元素,然后对其右孩子做相同的事
                head = stack.pop();
                System.out.print(head.value + " ");
                head = head.right;
            }
        }
    }
}

     此处预留一个位置,今天实在太晚了,困的不行,接下面的明天继续更。Good Night.
     更新......

后序遍历的非递归实现

     关于二叉树后序遍历的非递归实现,我学习了两种方式。一种是利用两个栈,另一种是利用一个栈。

两个栈实现后序遍历

     依然从具体步骤开始:
         1. 申请两个 stack,分别记为 s1,s2 ,然后将头结点压入 s1 中
         2. 从 s1 中弹出一个节点,记为 cur ,然后 cur 的左孩子和右孩子按顺序压入栈中
         3. 把 cur 压入 s2 中
         4. 不断重复第二步、第三步、一直到 s1 为空时停止
         5. 从 s2 中依次弹出节点并打印值

public void posOrderNoRecur(Node head){
    if(head != null){
    //申请两个栈空间
        Stack<Node> s1 = new Stack<Node>();
        Stack<Node> s2 = new Stack<Node>();
        //把头结点压入栈s1中
        s1.push(head);
        while(!s1.isEmpty()){
        //从s1中弹出一个节点,并且分别对其左右孩子判空,压入s1
            Node cur = head.pop();
            //把弹出的节点压入s2
            s2.push(cur);
            if(cur.left != null) s1.push(cur.left);
            if(cur.right != null) s2.push(cur.right);
        }
        
        while(!s2.isEmpty())
            System.out.print(s2.pop().value + " ");
    }
}

** 以上,我们便利用两个栈完成了二叉树后序遍历的非递归实现,思想上很简单,就是对每个节点做一次先序遍历,然后再按照右孩子,左孩子的顺序压入 s2 中,因为我们知道栈的特性之一就是先进后出,按照前面所说的顺序把节点压入栈中,再遍历时就会得到一个正确的后序遍历。同样建议大家自己在稿纸上画图,模拟过程,帮助理解。 **

一个栈实现后序遍历

** 明天继续... **

     用一个栈来实现二叉树的后序遍历,多少会有一些麻烦,我决定采用先贴出代码的方式。

public void posOrderNoRecur(Node h){
    if(h != null){
        Stack<Node> stack = new Stack<Node>();
        stack.push(h);
        Node c = null;
        while(!stack.isEmpty()){
            c = stack.peek();
            if(c.left != null && h != c.left && h != c.right){
                stack.push(c.left);
            }else if(c.right != null && h != c.right){
                stack.push(c.right);
            }else{
                System.out.print(stack.pop().value + " ");
                h = c;
            }
        }
    }
}

     然后再来说代码的实现思路,主要是遵从这样的步骤:
         1. 首先,依然是申请一个栈,记为 stack , 并且设置两个变量 h 和 c。在整个过程中,h 代表最近一次弹出并打印的节点,c 代表栈顶节点,初始情况: h 为 head , c 为 null。
         2. 这个时候(h 为最新弹出节点,c 为栈顶节点< ** 并没有弹出 ** >)有三种情况:
             * 如果 c 的左孩子不为空,且 h 不是 c 的左孩子也不是 c 的右孩子,那么就把 c 的左孩子压入栈中。
             * 如果 c 的又孩子不为空,且 h 不是 c 的右孩子,那我们就把 c 的右孩子压入栈中。
             * 如果上述两种情况都不满足,说明 c 的左子树和右子树都已经打印完毕,然后让 h = c.
             * 结束条件: 栈 stack 为空

     接下来我们解释上述步骤的逻辑,首先,我们刚开始的时候把 h 代表的头结点压入 stack 中,此时,栈顶节点即为树的根,我们另 c = stack.peek(),这个时候,按照我们对后序遍历的理解,我们应该先遍历头结点的左子树,如果 c 的 ** 左孩子不为空 ** 的话,这个时候我们的 h 肯定不等于 c 的左孩子/右孩子,因为我们并没有把左孩子/右孩子压入栈中,所以我们把 c 的左孩子压入栈中,而之所以判断一下 h 所代表的刚弹出的节点是否为 c 的左孩子/右孩子,是因为我们要判断 c 的左右孩子是否有遍历。如果有遍历,那我们自然可以判断, c 的左右孩子已经遍历过,弹出 c 节点打印值,然后进入下一次循环即可。同理的,当 c 的左孩子为空,右孩子不为空时,我们自然要判断刚弹出的是否是 c 的右孩子,然后判断把 c 的右孩子压入栈中。
     我们讲了很多关于压入栈的操作,但是我们什么时候输出呢,也就是什么时候得到 h 的值呢。最后一个 else 包含的情况有哪些呢。很明显,就是遍历到叶子节点的时候。当我们碰到栈顶节点是叶子节点的时候,就会产生弹出。

总结

     本文主要讲了二叉树非常基础的三种遍历方式: 先序遍历、中序遍历、后序遍历。在实现方式上,我们主要区分递归的实现方式和非递归的实现方式。递归的方式是书本上的内容,非递归方式的话,其实都很好理解,主要应该注意的就是压入栈的时机,左右孩子压入栈的顺序。这些顺序的判断和分析,都是建立在先/中/后序遍历的特点上的。只要清楚所应该遵循的规则,相信就可以写出正确的程序。

     Ps: 花了四天才写完这篇博客。第一天太晚,第二天感冒,第三天出去浪了。是时候提高一波执行力了。
     ** 博客地址 :** 博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容