神级遍历二叉树遍历(递归、非递归和Morris遍历)

2 篇文章 0 订阅
1 篇文章 0 订阅

一文通吃二叉树遍历的恩怨情仇(递归、非递归和Morris遍历)

这里的讨论以节点的方式存储的二叉树

二叉树结构

    int val;
    Node left;
    Node right;

    Node(int data) {
        this.val = data;
    }   

一、递归

递归是我们最常用的遍历方法,也比较容易理解,我这里直接上先、中和后序的模板代码。

/**
     * 递归,先序遍历
     *
     * @param head
     */
    public void preOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        System.out.println(head.val + " ");
        preOrderRecur(head.left);
        preOrderRecur(head.right);
    }

    /**
     * 递归,中序遍历
     *
     * @param head
     */
    public void inOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        inOrderRecur(head.left);
        System.out.println(head.val + " ");
        inOrderRecur(head.right);
    }

    /**
     * 递归,后序遍历
     *
     * @param head
     */
    public void posOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        posOrderRecur(head.left);
        posOrderRecur(head.right);
        System.out.println(head.val + " ");
    }

二、非递归

能用递归实现的方法自然也可以用非递归的方法实现。这是因为递归方法无非就是利用函数栈来保存信息,如果用自己申请的数据结构来替代函数栈,也可以实现相同的功能。
非递归的方法同样也是很容易理解,特别是非递归后序遍历下面采取最容易理解的遍历方式——采用两个栈完遍历功能,其实也是把前序遍历倒过来而已,没有什么新奇的东西。

如果条件允许,自己可以着手从时间复杂度和空间复杂度下手。

 /**
     * 非递归:先序遍历
     *
     * @param head
     */
    public void preOrderUnRecur(Node head) {
        System.out.println("非递归:先序遍历:");
        if (head != null) {
            Stack<Node> stack = new Stack<Node>();
            stack.add(head);
            while (!stack.isEmpty()) {
                head = stack.pop();
                System.out.println(head.val + " ");
                if (head.right != null) {
                    stack.push(head.right);
                }
                if (head.left != null) {
                    stack.push(head.left);
                }
            }
        }
        System.out.println();
    }

    /**
     * 非递归:中序遍历
     *
     * @param head
     */
    public void inOrderUnRecur(Node head) {
        System.out.println("非递归:中序遍历:");
        if (head != null) {
            Stack<Node> stack = new Stack<Node>();
            while (!stack.isEmpty() || head != null) {
                while (!stack.isEmpty() || head != null) {
                    if (head != null) {
                        stack.push(head);
                        head = head.left;
                    } else {
                        head = stack.pop();
                    }
                    System.out.println(head.val + " ");
                    head = head.right;
                }
            }
        }
        System.out.println();
    }

    /**
     * 非递归:后序遍历,前序遍历倒过来,用两个栈
     *
     * @param head
     */
    public void posOrderUnRecur(Node head) {
        System.out.println("非递归:后序遍历:");
        if (head != null) {
            Stack<Node> stack1 = new Stack<Node>();
            Stack<Node> stack2 = new Stack<Node>();
            stack1.push(head);
            while (!stack1.isEmpty()) {
                head = stack1.pop();
                stack2.push(head);
                if (head.left != null) {
                    stack1.push(head.left);
                }
                if (head.right != null) {
                    stack1.push(head.right);
                }
            }
            while (!stack2.isEmpty()) {
                System.out.println(stack2.pop().val + " ");
            }
        }
        System.out.println();
    }

三、Morris巧妙的遍历

从头结点head开始完成二叉树的先序遍历、中序遍历和后序遍历。如果二叉树结点数为n,则要求时间复杂度为O(N),额外空间复杂度为O(1)

回顾:我们最容易理解的先、中、后遍历是递归方法,再者是利用栈记录结点的地址,来完成回到父节点。递归方法实际是利用了函数栈;栈则是利用了额外的空间。

首先来看普通的递归和非递归的方法,其实都利用了栈结构,在处理完二叉树某个节点后可以回到上层去。为什么从下层回到上层会如此之难?因为二叉树的结构使然,每个节点都有指向孩子节点的指针,所以从上层到下层容易,但是没有指向父节点的指针,所以从下层到上层需要用栈结构辅助完成。

Morris遍历的实质就是避免用栈结构, 而是让下层到上层有指针, 具体是通过让底层节点指向null的空闲指针指回上层的某个节点, 从而完成下层到上层的移动。我们知道, 二叉树上的很多节点都有大量的空闲指针, 比如, 某些节点没有右孩子节点, 那么这个节点的right指针就指向null, 我们称为空闲状态, Morris遍历正是利用了这些空闲指针。

假设当前节点为cur, 初始时cur就是整棵树的头节点, 根据以下标准让cur移动:

1.如果cur为null, 则过程停止, 否则继续下面的过程。

2.如果cur没有左子树, 则让cur向右移动, 即令cur=cur.right。

3.如果cur有左子树, 则找到cur左子树上最右的节点, 记为mostRight。

  1. 如果most Right的right指针指向null, 则令mostRight.right=cur,也就是让mostRight 的right指针指向当前节点, 然后让cur向左移动, 即令cur=cur.left。

  2. 如果mostRight的right指针指向cur, 则令mostRight.right=null, 也就是让mostRight 的right指针指向null, 然后让cur向右移动, 即令cur=cur.right。

下面上图1简单地解析一波。

在这里插入图片描述

​ 图1

  1. 初始时cur来到4节点,cur此时有左子树,所以根据刚才的描述,找到cur的左子树的最右节点(即3节点),发现节点3的右指针是指向空的,那么让其指向cur,树枝调整成如下图2,然后cur向左移动来到节点2。

在这里插入图片描述

​ 图2

2.cur来到节点2,cur此时有左子树,找到cur的左子树最右节点(即节点1),发现节点
1的右指针是指向空的,那么让其指向cur,被调整成如下图3,然后cur向左移动来到节点1。

在这里插入图片描述

​ 图3

3.cur来到节点1,cur此时没有左子树,根标准令cur向右指针方向移动,所以cur回
到了节点2。

4.cur来到节点2,cur此时有左子树,找cur的左子树最右节点,即节点1,发现节点
1的右指针是指向cur的,根据标准让其指向null,树被调整回如图2所示的样子,然后根据
标准,cur向右指针方向移动,所以cur来到了节点3。

5.cur来到节点3,cur此时没有左子树,根据标准令cur向右指针方向移动,所以cur回
到了节点4。

6.cur来到节点4,cur此时有左子树,找到cur的左子树最右节点,即节点3,发现节点
3的右指针是指向cur的,那么让其指向nu,树被调整回如图1所示的样子,然后根据标准
cur向右移动来到节点6。

7.cur来到节点6,cur此时有左子树,找到cur的左子树最右节点,即节点5,发现节点
5的右指针是指向null的,那么让其指向cur,被调整成如图4所示的样子,然后根据标准,
cur向左移动来到节点5。

在这里插入图片描述

​ 图4

8.cur来到节点5,cur此时没有左子树,根据标准令cur向右指针方向移动,所以cur回
到了节点6。

9.cur来到节点6,cur此时有左子树,找到cu的左子树最右节点,即节点5,发现节点
5的右指针是指向cur的,那么让其指向nul,树被调整回如图1所示的样子,然后根据标准,
cur向右移动来到节点7。

10.cur来到节点7,cur此时没有左子树,根据标准令cur向右指针方向移动,cur来到null 的位置。

11.cur为空,过程停止。

以上所有步骤都严格按照我们之前说的cur移动标准,cur依次到达的节点为:4、2、1、2、
3、4、6、5、6、7,我们将这个序列叫Morris序。

可以看出对于有左子树的节点都可以到达两次(4、2、6),没有左子树的节点都只会到达一次。对于任何一个只能到达一次的节点X,接下来cur要么跑到X的右子树上,要么就返回上级。而对于任何一个能够到达两次的节点Y,在第一次达到Y之后
cur都会先去Y的左子树转一圈,然后会第二次来到Y,接下来cur要么跑到Y的右子树上,要么就返回上级。同时,对于任何一个能够到达两次的节点Y,是如何知道此时的cur是第一次来到Y还是第二次来到Y呢?如果Y的左子树上的最右节点的指针( mostRight.right))是指向null的,那么此时cur就是第一次到达Y;如果 mostRight.right是指向Y的,那么此时cur就是第二
次到达Y。这就是 Morrisi遍历和 Morris序的实质。

下面给出Morris的模板代码。

 /**
     * morris遍历模板
     * @param head
     */
    public void morris(Node head) {
        if (head == null) {
            return;
        }
        Node cur = head;
        Node mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            // 如果当前cur有左子树
            if (mostRight != null) {
                // 找到cur左子树最右的节点
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                // 从上面的while里出来后,mostRight就是cur左子树上最右的节点
                if (mostRight.right == null) {   // 如果mostRight.right指向null
                    mostRight.right = cur;    //让其指向cur
                    cur = cur.left;     //cur向左移动
                    continue;       //回到最外层的while,继续判断cur的情况
                } else {     //如果mostRight.right是指向cur的
                    mostRight.right = null;   //让其指向null
                }
            }
            // cur如果没有左子树,cur向右移动
            // 或者cur左子树上最右节点的右指针是指向cur的,cur向右移动
            cur = cur.right;
        }
    }

Morris的先序和中序遍历只要在遍历的特定位置打印即可。

直接附上代码。

Morris前序遍历

/**
     * morris的先序遍历
     *
     * @param head
     */
    public void morrisPre(Node head) {
        if (head == null) {
            return;
        }
        Node cur = head;
        Node mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            // 如果当前cur有左子树
            if (mostRight != null) {
                // 找到cur左子树最右的节点
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                // 从上面的while里出来后,mostRight就是cur左子树上最右的节点
                if (mostRight.right == null) {   // 如果mostRight.right指向null
                    mostRight.right = cur;    //让其指向cur

                    System.out.println(cur.val + ""); //**打印行为**

                    cur = cur.left;     //cur向左移动
                    continue;       //回到最外层的while,继续判断cur的情况
                } else {     //如果mostRight.right是指向cur的
                    mostRight.right = null;   //让其指向null
                }
            } else {
                System.out.println(cur.val + ""); //**打印行为**
            }
            // cur如果没有左子树,cur向右移动
            // 或者cur左子树上最右节点的右指针是指向cur的,cur向右移动
            cur = cur.right;
        }
        System.out.println();
    }

Morris中序遍历

 /**
     * morris的中序遍历
     *
     * @param head
     */
    public void morrisIn(Node head) {
        if (head == null) {
            return;
        }
        Node cur = head;
        Node mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            // 如果当前cur有左子树
            if (mostRight != null) {
                // 找到cur左子树最右的节点
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                // 从上面的while里出来后,mostRight就是cur左子树上最右的节点
                if (mostRight.right == null) {   // 如果mostRight.right指向null
                    mostRight.right = cur;    //让其指向cur
                    cur = cur.left;     //cur向左移动
                    continue;       //回到最外层的while,继续判断cur的情况
                } else {     //如果mostRight.right是指向cur的
                    mostRight.right = null;   //让其指向null
                }
            }
            // cur如果没有左子树,cur向右移动
            // 或者cur左子树上最右节点的右指针是指向cur的,cur向右移动
            System.out.println(cur.val + "");  //打印行为
            cur = cur.right;
        }
        System.out.println();
    }

至于Morris的后序遍历相对复杂一点。

根据 Morris遍历,加工出后序遍历。

1.对于cur只能到达一次的节点(无左子树的节点),直接跳过,没有打印行为。

2.对于cur可以到达两次的任何一个节点(有左子树的节点)X,cur第一次到达X时没有打印行为;当第二次到达X时,逆序打印X左子树的右边界。

3.cur遍历完成后,逆序打印整棵树的右边界。
以图1来举例说明后序遍历的打印过程,棵二叉树的 Morris序为:4、2、1、2、3、4、
6、5、6、7。

当第二次达到2时,逆序打印节点2左子树的右边界:1

当第二次达到4时,逆序打印节点4左子树的右边界:3、2

当第二次达到6时,逆序打印节点6左子树的右边界:5

cur遍历完成后,逆序打印整棵树的右边界:7、6、4

可以看到这个顺序就是后序遍历的顺序。但是我们应该如何实现逆序打印一棵树的右边界?因为整个过程的额外空间复杂度要求是O(1),所以逆序打印一棵树右边界的过程中,是不
能申请额外的数据结构的。为了更好地说明整个过程,下面举一个右边界比较长的例子,如图5。

在这里插入图片描述

假设cur第二次到达了A,并且要逆序打印节点A左子树的右边界,首先将E.R指向nul,
然后将右边界逆序调整成如图6所示的样,整个过程类似单链表的逆序操作。

在这里插入图片描述

附上Morris后序遍历的代码。

/**
     * morris的后序遍历
     *
     * @param head
     */
    public void morrisPos(Node head) {
        if (head == null) {
            return;
        }
        Node cur = head;
        Node mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            // 如果当前cur有左子树
            if (mostRight != null) {
                // 找到cur左子树最右的节点
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                // 从上面的while里出来后,mostRight就是cur左子树上最右的节点
                if (mostRight.right == null) {   // 如果mostRight.right指向null
                    mostRight.right = cur;    //让其指向cur
                    cur = cur.left;     //cur向左移动
                    continue;       //回到最外层的while,继续判断cur的情况
                } else {     //如果mostRight.right是指向cur的
                    mostRight.right = null;   //让其指向null
                    printEdge(cur.left);
                }
            }
            // cur如果没有左子树,cur向右移动
            // 或者cur左子树上最右节点的右指针是指向cur的,cur向右移动
            cur = cur.right;
        }
        printEdge(head);
        System.out.println();
    }

    /**
     * 打印树枝
     *
     * @param head
     */
    public static void printEdge(Node head) {
        Node tail = reverseEdge(head);
        Node cur = tail;
        while (cur != null) {
            System.out.println(cur.val + " ");
            cur = cur.right;
        }
        reverseEdge(tail);
    }

    /**
     * 反转树枝:实际就是反转树的树枝(链表)
     *
     * @param from
     * @return
     */
    public static Node reverseEdge(Node from) {
        Node pre = null;
        Node next = null;
        while (from != null) {
            next = from.right;
            from.right = pre;
            pre = from;
            from = next;
        }
        return pre;
    }

四、总结

二叉树的递归和非递归遍历还是比较容易的,相对于Morris遍历就是把节点的null指向利用上而已,不过在Morris的后序遍历还是复杂的,需要仔细研究。

Morris遍历已经是进阶的二叉树遍历了,可以实现时间复杂度为O(N),额外空间复杂度为O(1),(PS:如果对时间复杂度和空间复杂度不清楚可以看看其他大神的博客哦,滑稽 ,,,)

画图软件:drawio

参考:《程序员代码面试指南》第二版———左程云

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值