【算法&数据结构体系篇class30】:Morris遍历

文章详细介绍了Morris遍历这种二叉树遍历方法,其特点是时间复杂度为O(N),空间复杂度为O(1)。通过对二叉树节点的处理,实现了在遍历过程中不使用额外数据结构。此外,文章提供了Morris遍历的先序、中序、后序遍历实现,并展示了如何用Morris遍历求解二叉树的最小深度问题。
摘要由CSDN通过智能技术生成

一、Morris遍历

一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1)

通过利用原树中大量空闲指针的方式,达到节省空间的目的

二、Morris遍历细节

假设来到当前节点cur,开始时cur来到头节点位置

1)如果cur没有左孩子,cur向右移动(cur = cur.right)

2)如果cur有左孩子,找到左子树上最右的节点mostRight:

  a.如果mostRight的右指针指向空,让其指向cur,

  然后cur向左移动(cur = cur.left)

  b.如果mostRight的右指针指向cur,让其指向null,

  然后cur向右移动(cur = cur.right)

3)cur为空时遍历停止

三、Morris遍历实质

建立一种机制

对于没有左子树的节点只到达一次,

对于有左子树的节点会到达两次

morris遍历时间复杂度依然是O(N)

四、代码演示

package class30;


/**Morris遍历
 *
 * 一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1)
 *  
 * 通过利用原树中大量空闲指针的方式,达到节省空间的目的
 *
 * 假设来到当前节点cur,开始时cur来到头节点位置
 * 1)如果cur没有左孩子,cur向右移动(cur = cur.right)
 * 2)如果cur有左孩子,找到左子树上最右的节点mostRight:
 * 	a.如果mostRight的右指针指向空,让其指向cur,
 * 	然后cur向左移动(cur = cur.left)
 * 	b.如果mostRight的右指针指向cur,让其指向null,
 * 	然后cur向右移动(cur = cur.right)
 * 3)cur为空时遍历停止
 *
 *
 * Morris遍历实质
 * 建立一种机制:
 *
 * 对于没有左子树的节点只到达一次,
 *
 * 对于有左子树的节点会到达两次
 *  
 * morris遍历时间复杂度依然是O(N)
 */
public class MorrisTraversal {

    //节点类
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int v) {
            value = v;
        }
    }

    public static void process(Node root) {
        if (root == null) {
            return;
        }
        // 1
        process(root.left);
        // 2
        process(root.right);
        // 3
    }

    //morris 遍历 空间复杂度O(1) 不需要像递归遍历需要栈空间  有左子树的节点 会遍历2回
    public static void morris(Node head) {
        if (head == null) return;
        Node cur = head;             //定义辅助变量cur指向头节点
        Node mostRight = null;       //表示当前cur节点的左子树的最右节点 初始值null

        //开始遍历 直到cur 来到null时跳出
        while (cur != null) {
            mostRight = cur.left;    //先让mostRight 指向当前节点左子节点

            //如果存在左子树 那么就接着遍历出左子树 最右节点
            if (mostRight != null) {
                //继续往该左子树最右节点下沉 注意判断右节点 非空 并且 不指向当前节点cur 则继续下沉右节点
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                //跳出循环 那么就是两种情况 1.右节点指向为空 那么就需要指向cur  然后cur 下沉到左子树 然后继续while遍历
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    //否则 就是右指向为当前cur 说明此时是第二次来到cur  令指向为null cur向右子树走,向右走可以
                    mostRight.right = null;
                }
            }
            //由于当 最右节点的右指向为 cur   或者 没有mostRight cur下的左树  两者情况都是 cur来到右指向
            //所以一句直接在 这个位置写 对其都生效 合并一句
            cur = cur.right;
        }
    }


    //morris 改先序遍历  基于morris遍历修改
    //没有左子树的节点 直接打印   有左子树的节点,在第一次遍历的时候打印    两处打印位置
    public static void morrisPre(Node head){
        if(head == null) return;
        Node cur = head;        //辅助变量 当前节点
        Node mostRight = null;  //当前节点的左子树最右边界
        while(cur != null){     //开始遍历 直到cur为空跳出 遍历完成
            mostRight = cur.left;  //赋值当前节点的左树
            if(mostRight != null){  //左树不为空
                //左树的右节点不为空 且不为cur 那么就进行下沉 至最右节点
                while(mostRight.right != null && mostRight.right != cur){
                    mostRight = mostRight.right;
                }
                //来到最右节点后 判断如果右节点为空 那么就指向cur  cur来到左树  然后跳过重新遍历
                if(mostRight.right == null){
                    //注意: 这里就是 存在左树节点,且第一次遍历的位置 在这里进行打印
                    System.out.print(cur.value + " ");
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else{
                    //右节点指向cur  那么指向就赋值null,相当于恢复原树结构 cur来到右树(该动作在104行进行 与其他条件合并使用同动作)  此时是存在左树节点 第二个遍历的位置
                    mostRight.right = null;
                }
            }else {  //节点 没有左树 那么该节点直接打印
                System.out.print(cur.value + " ");
            }
            //当节点 没有左树 或者在 有左树的情况下 其最右节点指向cur  两种情况下 我们需要将cur 指向其右树 继续遍历
            cur = cur.right;
        }
        System.out.println();
    }

    //morris 改中序遍历  基于morris遍历修改
    //没有左子树的节点 直接打印   有左子树的节点,在第二个次遍历的时候打印    两处打印位置  实际上在一个位置合并使用打印语句 一句即可
    public static void morrisIn(Node head){
        if(head == null) return;
        Node cur = head;        //辅助变量 当前节点
        Node mostRight = null;  //当前节点的左子树最右边界
        while(cur != null){     //开始遍历 直到cur为空跳出 遍历完成
            mostRight = cur.left;  //赋值当前节点的左树
            if(mostRight != null){  //左树不为空
                //左树的右节点不为空 且不为cur 那么就进行下沉 至最右节点
                while(mostRight.right != null && mostRight.right != cur){
                    mostRight = mostRight.right;
                }
                //来到最右节点后 判断如果右节点为空 那么就指向cur  cur来到左树 然后跳过重新遍历
                if(mostRight.right == null){
                    //注意: 这里就是 存在左树节点,且第一次遍历的位置
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else{
                    //右节点指向cur  那么指向就赋值null,相当于恢复原树结构 cur来到右树(该动作在104行进行 与其他条件合并使用同动作)  此时是存在左树节点 第二次遍历的位置
                    mostRight.right = null;
                }
            }
            //当节点 没有左树 或者在 有左树的情况下 其最右节点指向cur  两种情况下 我们需要将cur 指向其右树 继续遍历
            //同时这个位置 没有左树 就直接打印   ; 有左树 且第二个来到的下行位置 第二个来到是在前面else判断部分,放在这个位置 等他前面判断完也是会经过的
            System.out.print(cur.value + " ");
            cur = cur.right;
        }
        System.out.println();
    }

    //morris 改后序遍历  基于morris遍历修改
    //有左子树的节点 在第二次遍历时 进行 逆序打印其左子树全部的右节点  最后在逆序打印整个树的全部右节点
    public static void morrisPos(Node head){
        if(head == null) return;
        Node cur = head;        //辅助变量 当前节点
        Node mostRight = null;  //当前节点的左子树最右边界
        while(cur != null){     //开始遍历 直到cur为空跳出 遍历完成
            mostRight = cur.left;  //赋值当前节点的左树
            if(mostRight != null){  //左树不为空
                //左树的右节点不为空 且不为cur 那么就进行下沉 至最右节点
                while(mostRight.right != null && mostRight.right != cur){
                    mostRight = mostRight.right;
                }
                //来到最右节点后 判断如果右节点为空 那么就指向cur  cur来到左树 然后跳过重新遍历
                if(mostRight.right == null){
                    //注意: 这里就是 存在左树节点,且第一次遍历的位置
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else{
                    //右节点指向cur  那么指向就赋值null,相当于恢复原树结构 cur来到右树(该动作在104行进行 与其他条件合并使用同动作)  此时是存在左树节点 第二次遍历的位置
                    mostRight.right = null;

                    //逆序打印当前节点 左树的全部右节点
                    printEdge(cur.left);
                }
            }
            cur = cur.right;
        }
        //最后需要再逆序打印整个树的全部右节点
        printEdge(head);
        System.out.println();
    }


    //逆序打印 右节点
    public static void printEdge(Node head){
        //逆序打印 需要先把节点指向逆序 返回然后逆序后的节点 也就是最右下的右节点
        Node tail = reverse(head);
        //将当前逆序后的节点保存 进行遍历
        Node cur = tail;
        while (cur !=null ){
            System.out.print(cur.value + " ");
            cur = cur.right;
        }
        //最后需要再恢复回原树结构
        reverse(tail);
    }

    //逆序树结构 返回逆序后的头节点
    public static Node reverse(Node head){
        Node pre = null;
        Node next = null;
        while(head != null){
            next = head.right;
            head.right = pre;
            pre = head;
            head = next;
        }
        return pre;
    }


    // for test -- print tree
    public static void printTree(Node head) {
        System.out.println("Binary Tree:");
        printInOrder(head, 0, "H", 17);
        System.out.println();
    }

    public static void printInOrder(Node head, int height, String to, int len) {
        if (head == null) {
            return;
        }
        printInOrder(head.right, height + 1, "v", len);
        String val = to + head.value + to;
        int lenM = val.length();
        int lenL = (len - lenM) / 2;
        int lenR = len - lenM - lenL;
        val = getSpace(lenL) + val + getSpace(lenR);
        System.out.println(getSpace(height * len) + val);
        printInOrder(head.left, height + 1, "^", len);
    }

    public static String getSpace(int num) {
        String space = " ";
        StringBuffer buf = new StringBuffer("");
        for (int i = 0; i < num; i++) {
            buf.append(space);
        }
        return buf.toString();
    }


    /**
     * 判断是否是二叉搜索树  morris遍历 判断中序遍历是否节点都是升序排序的 是就是二叉搜索树
     * @param head
     * @return
     */
    public static boolean isBST(Node head) {
        if (head == null) {
            return true;
        }
        Node cur = head;
        Node mostRight = null;
        //注意 这里定义一个 前一个节点的的值 用来与后面cur当前节点做对比
        Integer pre = null;
        boolean ans = true;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    mostRight.right = null;
                }
            }

            //当前节点左树为空  节点就是往右节点接着遍历 这里我们就进行判断
            //中序遍历  也就是在这个位置打印节点值
            // 如果pre前节点非空 且 大于等于 当前节点的话 说明 节点不是升序 则不是二叉搜索树 返回false
            if (pre != null && pre >= cur.value) {
                //注意需要把morris遍历完 再去返回 不能在这里遇到不符合就直接return 因为整个树结构的指向要恢复原结构
                ans = false;
            }
            //如果符合 那么pre 就来到当前cur位置  cur接着向右节点
            pre = cur.value;
            cur = cur.right;
        }
        //若没有不符合的条件  遍历完之后 直接返回ans true
        return ans;
    }

    public static void main(String[] args) {
        Node head = new Node(4);
        head.left = new Node(2);
        head.right = new Node(6);
        head.left.left = new Node(1);
        head.left.right = new Node(3);
        head.right.left = new Node(5);
        head.right.right = new Node(7);
        printTree(head);
        morrisIn(head);
        morrisPre(head);
        morrisPos(head);
        printTree(head);

    }

}

 输出:


Binary Tree:
                                         v7v       
                        v6v       
                                         ^5^       
       H4H       
                                         v3v       
                        ^2^       
                                         ^1^       

1 2 3 4 5 6 7 
4 2 1 3 6 5 7 
1 3 2 5 7 6 4 
Binary Tree:
                                         v7v       
                        v6v       
                                         ^5^       
       H4H       
                                         v3v       
                        ^2^       
                                         ^1^       


Process finished with exit code 0

五、给定一棵二叉树的头节点head求以head为头的树中,最小深度是多少?

 常规方法: 二叉树的递归解法

优化空间O(1): Morris遍历法

package class30;

/**
 * 给定一棵二叉树的头节点head
 *
 * 求以head为头的树中,最小深度是多少?   头节点到叶子节点
 *
 * https://leetcode.cn/problems/minimum-depth-of-binary-tree/description/
 */
public class MinDepth {

    public static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int x) {
            val = x;
        }
    }

    // 下面的方法是一般解    递归返回最小深度是多少
    public static int minDepth1(TreeNode head) {
        //空树 高度0  一个根节点 高度1
        if(head == null) return 0;
        if(head.left == null && head.right == null) return 1;

        //定义深度变量 依次比较 左右子树递归的值  刷新高度的最小值
        int depth = Integer.MAX_VALUE;
        if(head.left != null){
            depth = Math.min(depth,minDepth1(head.left));
        }
        if(head.right != null){
            depth = Math.min(depth,minDepth1(head.right));
        }
        return 1 + depth;  //最后返回根节点1 加上子树的最小深度
    }

    // 下面的方法是morris遍历的解
    public static int minDepth2(TreeNode head) {
        if(head == null) return 0;
        TreeNode cur = head;
        TreeNode mostRight = null;
        int curLevel = 0;    //定义一个层级变量 初始为0
        int minHeight = Integer.MAX_VALUE;
        while (cur != null){
            mostRight = cur.left;
            if(mostRight != null){
                int rightBoardSize = 1; //左子树非空 那么就定义一个长度 表示该左子树 依次往右的节点数 当前左子节点已经遍历 初始值1
                //刷新左子树最右节点值  同时右节点数++
                while (mostRight.right != null && mostRight.right!= cur){
                    rightBoardSize++;
                    mostRight = mostRight.right;
                }
                if(mostRight.right == null){
                    //第一次到达 然后刷新 指向cur  cur来到左节点 当前层级要++
                    curLevel++;
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
               }else{//第二次到达
                    //如果是指向了cur  那么就需要判断 当前节点的左子树是否空 因为可能存在左树 如果不存在 那么就表示该节点是叶子节点
                    //可以进行高度的比较
                    if(mostRight.left == null){
                        //刷新当前层级与当前最小高度
                        minHeight = Math.min(curLevel,minHeight);
                    }
                    //此时因为是第二次到达 肯定是从小往上  那么层级就要减去 rightBoardSize 其右节点个数
                    curLevel -= rightBoardSize;
                    mostRight.right = null;   //指向再恢复为空
                }
            }else{
                // 左树为空 同时先把层级++ 最后再下面刷新cur指向右节点
                curLevel++;
            }
            //节点没有左子树 获取有左子树 但是其最右侧节点指回了cur节点的时候 需要刷新cur 来到右节点继续遍历
            cur = cur.right;
        }
        //最后退出时,需要再判断下整个树是否有右节点 有的话 就进行判断其符合叶子节点 的高度
        int finalRight = 1;
        cur = head;  //cur节点重新指向头节点来遍历
        while (cur.right != null){  //遍历到最后一个右节点 cur保存
            finalRight++;
            cur = cur.right;
        }
        //遍历到最右的右节点 再判断 该节点是否存在左子树 只有不存在的时候 该节点才能作为叶子节点 与当前高度进行较小高度比较
        if(cur.left == null){
            minHeight = Math.min(finalRight,minHeight);
        }
        return minHeight;
    }
}
可以使用 Morris 遍历算法。该算法利用了线索二叉树的思想,将每个节点的右子树指向其后继节点,从而实现不需要使用栈的遍历。 具体实现步骤如下: 1. 将当前节点设为根节点。 2. 如果当前节点的左子树为空,则输出当前节点的值,并将其右子节点作为当前节点。 3. 如果当前节点的左子树不为空,在当前节点的左子树中找到当前节点在中序遍历中的前驱节点。 a. 如果前驱节点的右子节点为空,将它的右子节点设置为当前节点,当前节点更新为其左子节点。 b. 如果前驱节点的右子节点为当前节点,将它的右子节点重新设为空(恢复树的形状),输出当前节点的值,当前节点更新为其右子节点。 4. 重复步骤 2 和步骤 3,直到遍历完整棵树。 实现代码如下: ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def morris_traversal(root: TreeNode) -> list: res = [] curr = root while curr: if curr.left is None: res.append(curr.val) curr = curr.right else: # 找到当前节点的前驱节点 pre = curr.left while pre.right and pre.right != curr: pre = pre.right if pre.right is None: # 将前驱节点的右子节点指向当前节点,以便后续恢复树的形状 pre.right = curr curr = curr.left else: # 恢复树的形状,输出当前节点的值,当前节点更新为其右子节点 pre.right = None res.append(curr.val) curr = curr.right return res ``` 注意:这种算法会改变树的形状,因此不适用于需要多次遍历的情况。另外,由于需要修改树的结构,所以该算法不是完全不使用栈,而是用了隐式的栈(即树的右子节点)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值