算法设计与分析 二叉树的Morris遍历

简介: 算法设计与分析 二叉树的Morris遍历

Morris遍历

概述

  • Morris遍历:一种遍历二叉树的方式,并且时间复杂度O(N),空间复杂度O(1)
  • 常规的遍历无论递归还是非递归,空间复杂度都达不到常数级别(二叉树的高度 O(logN))。Morris遍历,通过利用原树中的大量空闲指针的方式,达到节省空间的目地
  • morris遍历的实现原则
    记作当前节点为cur。
    1 如果cur无左孩子,cur向右移动(cur=cur.right)
    2 如果cur有左孩子,找到cur左子树上最右的节点,记为mostright
    (1)如果mostright的right指针指向空,让其指向cur,cur向左移动(cur=cur.left)
    (2)如果mostright的right指针指向cur,让其指向空,cur向右移动(cur=cur.right)
    3 cur为空时遍历结束
  • Morris的应用:因为Morris遍历解决了最基本的遍历问题,所以很多二叉树的问题都可以使用Morris进行优化

实现思路

image.png

  1. cur:1,mostright:5;将5的右指针指向1,cur移动到2
    image.png
  2. cur:2,mostright:4;将4的右指针指向2,cur移动到4
    image.png
  3. cur:4,cur目前没有左孩子(不设置mostright),将cur移动到2
    image.png
  4. cur:2,mostright:4;将4的右指针指向null,cur移动到5
    image.png
  5. cur:5,cur无左孩子,将cur移动到1

image.png

  1. cur:1,mostright:5;将5的右指针指向null,cur移动到3
    image.png
  2. cur:3,mostright:6,将6的右指针指向3,cur移动6
    image.png
  3. cur:6,cur无左孩子,将cur移动到3
    image.png
  4. cur:3,mostright:6;将6的右指针指向null,cur移动到7
    image.png
  5. cur:7,cur无左孩子,将cur移动到null,遍历结束
    image.png

Morris(cur)序列:1,2,4,2,5,1,3,6,3,7

可以发现:若节点有左孩子,会在Morris序列中出现2次,若无,就是一次

  • 解析:
    (1)常规的递归遍历方法,通过栈的数据结构记录头节点,实现树的遍历。每一个树的节点在整个遍历的过程中,都会经过三次
    (2)Morris利用的是底层线索化,若存在左子树就是通过左子树最右的节点的右指针与头节点建立连系,即通过左子树的最右节点回到头节点。所以可以做到树的遍历,含左子树的节点经过两次,不含左子树的只经过一次
    (3)如何判断节点是第几次访问:若访问该节点时mostRight.right = null,说明第一次访问,若mostRight.right = 当前节点,第二次访问

代码实现

    public static class Node{
        int value;
        Node left;
        Node right;
        public Node(int date){
            this.value = date;
        }
    }
    public static void morris(Node head){
        if (head == null){
            return;
        }
        Node cur = head; //当前节点
        Node mostRight = null; //左孩子的最右端
        while (cur != null){
            mostRight = cur.left;
            if (mostRight == null){
                //无左子树
                //只访问一次
                cur = cur.right;
            }
            else {
                //cur存在左孩子
                while (mostRight.right != null && mostRight.right != cur){
                    //找到最右端
                    //因为:mostRight.right可能以及进行了修改指向了头结点,要进行判断
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null){
                    //将最右端的右指针与头节点相连
                    //第一次访问
                    mostRight.right = cur;
                    //cur左移
                    cur = cur.left;
                } else {
                    //mostRight.right == cur
                    //第二次访问
                    mostRight.right = null;
                    cur = cur.right;
                }
            }
        }
    }

使用Morris遍历得到前,中,后序序列

  • 先序序列
    1 只到达一次的节点直接打印
    2 到达两次的只打印第一次
    public static class Node{
        int value;
        Node left;
        Node right;
        public Node(int date){
            this.value = date;
        }
    }
    public static void morrisPre(Node head){
        if (head == null){
            return;
        }
        Node cur = head; //当前节点
        Node mostRight = null; //左孩子的最右端
        while (cur != null){
            mostRight = cur.left;
            if (mostRight == null){
                //无左子树
                //只访问一次的节点直接打印
                System.out.println(cur.value);
                cur = cur.right;
            }
            else {
                //cur存在左孩子
                while (mostRight.right != null && mostRight.right != cur){
                    //找到最右端
                    //因为:mostRight.right可能以及进行了修改指向了头结点,要进行判断
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null){
                    //将最右端的右指针与头节点相连
                    mostRight.right = cur;
                    //cur第一次访问
                    System.out.println(cur.value);
                    //cur左移
                    cur = cur.left;
                } else {
                    //mostRight.right == cur;
                    mostRight.right = null;
                    //cur第二次访问,不进行操作
                    cur = cur.right;
                }
            }
        }
    }
  • 中序遍历
    1 只到达一次的节点直接打印
    2 到达两次的只打印第二次
    public static class Node{
        int value;
        Node left;
        Node right;
        public Node(int date){
            this.value = date;
        }
    }
    public static void morrisIn(Node head){
        if (head == null){
            return;
        }
        Node cur = head; //当前节点
        Node mostRight = null; //左孩子的最右端
        while (cur != null){
            mostRight = cur.left;
            if (mostRight == null){
                //无左子树
                //只访问一次的节点直接打印
                System.out.println(cur.value);
                cur = cur.right;
            }
            else {
                //cur存在左孩子
                while (mostRight.right != null && mostRight.right != cur){
                    //找到最右端
                    //因为:mostRight.right可能以及进行了修改指向了头结点,要进行判断
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null){
                    //将最右端的右指针与头节点相连
                    mostRight.right = cur;
                    //cur第一次访问,不操作
                    //cur左移
                    cur = cur.left;
                } else {
                    //mostRight.right == cur;
                    mostRight.right = null;
                    //cur第二次访问打印
                    System.out.println(cur.value);
                    cur = cur.right;
                }
            }
        }
    }
  • 后序遍历
    1 第一次访问的不做任何操作
    2 第二次访问时要求逆序打印左树的右边界
    3 整个遍历结束后,逆序打印整个树的右边界
    4 问题解决:任何逆序打印右边界,空间复杂度为O(1),将右边界看为:节点只含右指针的单列表。将整个列表逆序,访问,再逆序恢复
    public static class Node{
        int value;
        Node left;
        Node right;
        public Node(int date){
            this.value = date;
        }
    }
    public static void morrisPos(Node head){
        if (head == null){
            return;
        }
        Node cur = head; //当前节点
        Node mostRight = null; //左孩子的最右端
        while (cur != null){
            mostRight = cur.left;
            if (mostRight == null){
                //无左子树
                //只访问一次的节点不操作
                cur = cur.right;
            }
            else {
                //cur存在左孩子
                while (mostRight.right != null && mostRight.right != cur){
                    //找到最右端
                    //因为:mostRight.right可能以及进行了修改指向了头结点,要进行判断
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null){
                    //将最右端的右指针与头节点相连
                    mostRight.right = cur;
                    //cur第一次访问,不操作
                    //cur左移
                    cur = cur.left;
                } else {
                    //mostRight.right == cur;
                    mostRight.right = null;
                    //cur第二次访问逆序打印左子树的右边界
                    printEdge(cur.left);
                    cur = cur.right;
                }
            }
        }
        //整个遍历结束后,打印整个树的右边界
        printEdge(head);
    }
    public static void printEdge(Node x){
        //打印X的右边界
        Node temp = reverseEdge(x); //逆序后的头节点
        Node cur = temp;
        while (cur != null){
            //逆序打印
            System.out.println(cur.value);
            cur = cur.right;
        }
        reverseEdge(temp); //再次逆序
    }
    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应用判断是否为BST

  • 只需再中序遍历的打印位置进行修改即可,判断上一个值是否比当前值小
    public static class Node{
        int value;
        Node left;
        Node right;
        public Node(int date){
            this.value = date;
        }
    }
    public static Boolean isBST(Node head){
        if (head == null){
            return true;
        }
        Node cur = head; //当前节点
        Node mostRight = null; //左孩子的最右端
        int preValue = Integer.MIN_VALUE;
        while (cur != null){
            mostRight = cur.left;
            if (mostRight == null){
                //无左子树
                if (preValue < cur.value){
                    preValue = cur.value;
                }else {
                    return false;
                }
                cur = cur.right;
            }
            else {
                //cur存在左孩子
                while (mostRight.right != null && mostRight.right != cur){
                    //找到最右端
                    //因为:mostRight.right可能以及进行了修改指向了头结点,要进行判断
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null){
                    //将最右端的右指针与头节点相连
                    mostRight.right = cur;
                    //cur第一次访问,不操作
                    //cur左移
                    cur = cur.left;
                } else {
                    //mostRight.right == cur;
                    mostRight.right = null;
                    if (preValue < cur.value){
                        preValue = cur.value;
                    }else {
                        return false;
                    }
                    cur = cur.right;
                }
            }
        }
        return true;
    }


目录
相关文章
|
5天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
24 2
|
11天前
|
存储 算法 安全
基于哈希表的文件共享平台 C++ 算法实现与分析
在数字化时代,文件共享平台不可或缺。本文探讨哈希表在文件共享中的应用,包括原理、优势及C++实现。哈希表通过键值对快速访问文件元数据(如文件名、大小、位置等),查找时间复杂度为O(1),显著提升查找速度和用户体验。代码示例展示了文件上传和搜索功能,实际应用中需解决哈希冲突、动态扩容和线程安全等问题,以优化性能。
|
20天前
|
存储 算法 Python
文件管理系统中基于 Python 语言的二叉树查找算法探秘
在数字化时代,文件管理系统至关重要。本文探讨了二叉树查找算法在文件管理中的应用,并通过Python代码展示了其实现过程。二叉树是一种非线性数据结构,每个节点最多有两个子节点。通过文件名的字典序构建和查找二叉树,能高效地管理和检索文件。相较于顺序查找,二叉树查找每次比较可排除一半子树,极大提升了查找效率,尤其适用于海量文件管理。Python代码示例包括定义节点类、插入和查找函数,展示了如何快速定位目标文件。二叉树查找算法为文件管理系统的优化提供了有效途径。
47 5
|
21天前
|
缓存 算法 搜索推荐
Java中的算法优化与复杂度分析
在Java开发中,理解和优化算法的时间复杂度和空间复杂度是提升程序性能的关键。通过合理选择数据结构、避免重复计算、应用分治法等策略,可以显著提高算法效率。在实际开发中,应该根据具体需求和场景,选择合适的优化方法,从而编写出高效、可靠的代码。
29 6
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
76 1
|
2月前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
2月前
|
存储 缓存 算法
如何提高二叉树遍历算法的效率?
选择合适的遍历算法,如按层次遍历树时使用广度优先搜索(BFS),中序遍历二叉搜索树以获得有序序列。优化数据结构,如使用线索二叉树减少空指针判断,自定义节点类增加辅助信息。利用递归与非递归的特点,避免栈溢出问题。多线程并行遍历提高速度,注意线程安全。缓存中间结果,避免重复计算。预先计算并存储信息,提高遍历效率。综合运用这些方法,提高二叉树遍历算法的效率。
71 5
|
2月前
|
算法
树的遍历算法有哪些?
不同的遍历算法适用于不同的应用场景。深度优先搜索常用于搜索、路径查找等问题;广度优先搜索则在图的最短路径、层次相关的问题中较为常用;而二叉搜索树的遍历在数据排序、查找等方面有重要应用。
45 2
|
2月前
|
机器学习/深度学习 JSON 算法
二叉树遍历算法的应用场景有哪些?
【10月更文挑战第29天】二叉树遍历算法作为一种基础而重要的算法,在许多领域都有着不可或缺的应用,它为解决各种复杂的问题提供了有效的手段和思路。随着计算机科学的不断发展,二叉树遍历算法也在不断地被优化和扩展,以适应新的应用场景和需求。
66 0
|
3月前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。