【尚硅谷数据结构】第 10 章 树结构的基础部分

参考:https://blog.csdn.net/oneby1314/category_10231585.html

1、二叉树

1.1、为什么需要二叉树

1.1.1、数组存储方式的分析

  • 优点: 通过下标方式访问元素, 速度快。 对于有序数组, 还可使用二分查找提高检索速度
  • 缺点: 如果要检索具体某个值, 或者插入值(按一定顺序)会整体移动,效率较低

在这里插入图片描述

1.1.2、链式存储方式的分析

  • 优点: 在一定程度上对数组存储方式有优化(比如: 插入一个数值节点, 只需要将插入节点, 链接到链表中即可,删除效率也很好)。
  • 缺点: 在进行检索时, 效率仍然较低, 比如(检索某个值, 需要从头节点开始遍历)

在这里插入图片描述

1.1.3、树存储方式的分析

  • 能提高数据存储, 读取的效率, 比如利用 二叉排序树(Binary Sort Tree), 既可以保证数据的检索速度, 同时也可以保证数据的插入, 删除, 修改的速度。 【示意图,后面详讲】

在这里插入图片描述

1.2、树的常用术语

  • 节点
  • 根节点
  • 父节点
  • 子节点
  • 叶子节点 (没有子节点的节点)
  • 节点的权(节点值)
  • 路径(从 root 节点找到该节点的路线)
  • 子树
  • 树的高度(最大层数)
  • 森林 :多颗子树构成森林

在这里插入图片描述

1.3、二叉树的概念

  • 树有很多种, 每个节点最多只能有两个子节点的一种形式称为二叉树,二叉树的子节点分为左节点和右节点

在这里插入图片描述

  • 如果该二叉树的所有叶子节点都在最后一层, 并且结点总数= 2^n -1, n 为层数, 则我们称为满二叉树

在这里插入图片描述

  • 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层, 而且最后一层的叶子节点在左边连续, 倒数第二层的叶子节点在右边连续, 我们称为完全二叉树

在这里插入图片描述

1.4、二叉树的遍历

1.4.1、代码思路

  • 前序遍历: 先输出父节点, 再遍历左子树和右子树

  • 中序遍历: 先遍历左子树, 再输出父节点, 再遍历右子树

  • 后序遍历: 先遍历左子树, 再遍历右子树, 最后输出父节点

在这里插入图片描述

1.4.2、前 中 后序遍历

  • 定义二叉树节点

    // 先创建HeroNode节点
    class HeroNode {
          
          
        private int no;
        private String name;
        private HeroNode left; // 默认为null
        private HeroNode right; // 默认为null
    
        public HeroNode(int no, String name) {
          
          
            this.no = no;
            this.name = name;
        }
    
        public int getNo() {
          
          
            return no;
        }
    
        public void setNo(int no) {
          
          
            this.no = no;
        }
    
        public String getName() {
          
          
            return name;
        }
    
        public void setName(String name) {
          
          
            this.name = name;
        }
    
        public HeroNode getLeft() {
          
          
            return left;
        }
    
        public void setLeft(HeroNode left) {
          
          
            this.left = left;
        }
    
        public HeroNode getRight() {
          
          
            return right;
        }
    
        public void setRight(HeroNode right) {
          
          
            this.right = right;
        }
    
        @Override
        public String toString() {
          
          
            return "HeroNode{" +
                    "no=" + no +
                    ", name='" + name + '\'' +
                    '}';
        }
    
        // 前序遍历
        public void preOrder() {
          
          
            System.out.println(this); // 先输出父节点
            // 递归的向左子树前序遍历
            if (this.left != null) {
          
          
                this.left.preOrder();
            }
            // 递归向右子树前序遍历
            if (this.right != null) {
          
          
                this.right.preOrder();
            }
        }
    
        // 中序遍历
        public void infixOrder() {
          
          
            // 递归向左子树中序遍历
            if (this.left != null) {
          
          
                this.left.infixOrder();
            }
            // 输出父节点
            System.out.println(this);
            // 递归向右子树中序遍历
            if (this.right != null) {
          
          
                this.right.infixOrder();
            }
        }
    
        // 后序遍历
        public void postOrder() {
          
          
            // 递归向左子树后序遍历
            if (this.left != null) {
          
          
                this.left.preOrder();
            }
            // 递归向右子树后序遍历
            if (this.right != null) {
          
          
                this.right.postOrder();
            }
            System.out.println(this);
        }
    }
    
  • 定义二叉树:二叉树需要一个根节点 root 作为整个树的入口

    // 定义BinaryTree_二叉树
    class BinaryTree {
          
          
        private HeroNode root;
    
        public void setRoot(HeroNode root) {
          
          
            this.root = root;
        }
    
        // 前序遍历
        public void preOrder() {
          
          
            if (this.root != null) {
          
          
                this.root.preOrder();
            }else {
          
          
                System.out.println("二叉树为空,无法遍历");
            }
        }
    
        // 中序遍历
        public void infixOrder() {
          
          
            if (this.root != null) {
          
          
                this.root.infixOrder();
            } else {
          
          
                System.out.println("二叉树为空,无法遍历");
            }
        }
    
        // 后序遍历
        public void postOrder() {
          
          
            if (this.root != null) {
          
          
                this.root.postOrder();
            } else {
          
          
                System.out.println("二叉树为空,无法遍历");
            }
        }
    }
    

1.4.3、代码测试

/**
 * @version V1.0
 * @ClassName:BinaryTreeDemo
 * @Description: TODO
 * @author:Daniel
 * @date:2020/12/5 上午10:56
 */
public class BinaryTreeDemo {
    
    
    public static void main(String[] args) {
    
    
        // 先需要创建一颗二叉树
        BinaryTree binaryTree = new BinaryTree();
        // 创建需要的节点
        HeroNode root = new HeroNode(1, "宋江");
        HeroNode node2 = new HeroNode(2, "吴用");
        HeroNode node3 = new HeroNode(3, "卢俊义");
        HeroNode node4 = new HeroNode(4, "林冲");
        HeroNode node5 = new HeroNode(5, "关胜");

        // 说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
        root.setLeft(node2);
        root.setRight(node3);
        node3.setRight(node4);
        node3.setLeft(node5);
        binaryTree.setRoot(root);

        // 测试
        System.out.println("前序遍历");
        binaryTree.preOrder();

        System.out.println("中序遍历");
        binaryTree.infixOrder();

        System.out.println("后序遍历");
        binaryTree.postOrder();
    }
}
  • 运行结果

在这里插入图片描述

前序遍历
HeroNode{no=1, name='宋江'}
HeroNode{no=2, name='吴用'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=5, name='关胜'}
HeroNode{no=4, name='林冲'}
中序遍历
HeroNode{no=2, name='吴用'}
HeroNode{no=1, name='宋江'}
HeroNode{no=5, name='关胜'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=4, name='林冲'}
后序遍历
HeroNode{no=2, name='吴用'}
HeroNode{no=5, name='关胜'}
HeroNode{no=4, name='林冲'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=1, name='宋江'}

1.4.4、小结

  • 看==父节点输出的顺序==就可以确定是前序,中序还是后序
    • 父节点先输出就是前序
    • 父节点第二输出就是后续
    • 父节点最后输出就是后续

1.5、二叉树的查找

1.5.1、代码思路

  • 将二叉树的前、中、后序遍历改为查找即可

  • 编码思路:

    • 如果查找到目标节点,直接返回,结束递归
    • 如果找不到,继续递归执行前(中、后)序查找

在这里插入图片描述

1.5.2、前 中 后序查找

  • 定义二叉树节点

    // 先创建HeroNode节点
    class HeroNode {
          
          
        private int no;
        private String name;
        private HeroNode left; // 默认为null
        private HeroNode right; // 默认为null
    
        public HeroNode(int no, String name) {
          
          
            this.no = no;
            this.name = name;
        }
    
        public int getNo() {
          
          
            return no;
        }
    
        public void setNo(int no) {
          
          
            this.no = no;
        }
    
        public String getName() {
          
          
            return name;
        }
    
        public void setName(String name) {
          
          
            this.name = name;
        }
    
        public HeroNode getLeft() {
          
          
            return left;
        }
    
        public void setLeft(HeroNode left) {
          
          
            this.left = left;
        }
    
        public HeroNode getRight() {
          
          
            return right;
        }
    
        public void setRight(HeroNode right) {
          
          
            this.right = right;
        }
    
        @Override
        public String toString() {
          
          
            return "HeroNode{" +
                    "no=" + no +
                    ", name='" + name + '\'' +
                    '}';
        }
    
        // 前序遍历
        public void preOrder() {
          
          
            System.out.println(this); // 先输出父节点
            // 递归的向左子树前序遍历
            if (this.left != null) {
          
          
                this.left.preOrder();
            }
            // 递归向右子树前序遍历
            if (this.right != null) {
          
          
                this.right.preOrder();
            }
        }
    
        // 中序遍历
        public void infixOrder() {
          
          
            // 递归向左子树中序遍历
            if (this.left != null) {
          
          
                this.left.infixOrder();
            }
            // 输出父节点
            System.out.println(this);
            // 递归向右子树中序遍历
            if (this.right != null) {
          
          
                this.right.infixOrder();
            }
        }
    
        // 后序遍历
        public void postOrder() {
          
          
            // 递归向左子树后序遍历
            if (this.left != null) {
          
          
                this.left.preOrder();
            }
            // 递归向右子树后序遍历
            if (this.right != null) {
          
          
                this.right.postOrder();
            }
            System.out.println(this);
        }
    
    
        /*
         * @Description: 前序查找
         * @Param: [no:要查找的no]
         * @Return: com.atguigu.tree.HeroNode:如果找到就返回该Node,如果没有就返回null
         * @Author: Daniel
         * @Date: 2020/12/5
         */
        public HeroNode preOrderSearch(int no) {
          
          
            System.out.println("进入前序遍历!");
            // 比较当前节点是不是
            if (this.no == no) {
          
          
                return this;
            }
            // 1. 判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
            // 2. 如果左递归前序查找,找到节点,则返回
            HeroNode resNode = null;
            if (this.left != null) {
          
          
                resNode = this.left.preOrderSearch(no);
            }
            if (resNode != null) {
          
          
                return resNode;
            }
            if (this.right != null) {
          
          
                resNode = this.right.preOrderSearch(no);
            }
            return resNode;
        }
    
        // 中序遍历查找
        public HeroNode infixOrderSearch(int no) {
          
          
            // 判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
            HeroNode resNode = null;
            if (this.left != null) {
          
          
                resNode = this.left.infixOrderSearch(no);
            }
            if (resNode != null) {
          
          
                return resNode;
            }
            // 如果没有找到,就和当前节点比较,如果是则返回当前节点
            if (this.no == no) {
          
          
                return this;
            }
            if (this.right != null) {
          
          
                resNode = this.right.infixOrderSearch(no);
            }
            return resNode;
        }
    
        // 后续遍历查找
        public HeroNode postOrderSearch(int no) {
          
          
            // 判断当前节点的左子节点是否为空,如果不为空,则递归后序查找
            HeroNode resNode = null;
            if (this.left != null) {
          
          
                resNode = this.left.postOrderSearch(no);
            }
            if (resNode != null) {
          
           // 说明在左子树找到
                return resNode;
            }
            // 如果左子树没有找到,则向右子树进行后序遍历查找
            if (this.right != null) {
          
          
                resNode = this.right.postOrderSearch(no);
            }
            if (resNode != null) {
          
          
                return resNode;
            }
            // 如果左右子树都没有找到,就笔记当前节点是不是
            if (this.no == no) {
          
          
                return this;
            }
            return resNode;
        }
    }
    
  • 定义二叉树:二叉树需要一个根节点 root 作为整个树的入口

    // 定义BinaryTree_二叉树
    class BinaryTree {
          
          
        private HeroNode root;
    
        public void setRoot(HeroNode root) {
          
          
            this.root = root;
        }
    
        // 前序遍历
        public void preOrder() {
          
          
            if (this.root != null) {
          
          
                this.root.preOrder();
            }else {
          
          
                System.out.println("二叉树为空,无法遍历");
            }
        }
    
        // 中序遍历
        public void infixOrder() {
          
          
            if (this.root != null) {
          
          
                this.root.infixOrder();
            } else {
          
          
                System.out.println("二叉树为空,无法遍历");
            }
        }
    
        // 后序遍历
        public void postOrder() {
          
          
            if (this.root != null) {
          
          
                this.root.postOrder();
            } else {
          
          
                System.out.println("二叉树为空,无法遍历");
            }
        }
    
        // 前序遍历查找
        public HeroNode preOrderSearch(int no) {
          
          
            if (root != null) {
          
          
                return root.preOrderSearch(no);
            } else {
          
          
                return null;
            }
        }
    
        // 中序遍历查找
        public HeroNode infixOrderSearch(int no) {
          
          
            if (root != null) {
          
          
                return root.infixOrderSearch(no);
            } else {
          
          
                return null;
            }
        }
    
        // 后序遍历查找
        public HeroNode postOrderSearch(int no) {
          
          
            if (root != null) {
          
          
                return this.root.postOrderSearch(no);
            } else {
          
          
                return null;
            }
        }
    }
    

1.5.3、测试代码

  • 测试代码:测试前序、中序、后序查找

    public class BinaryTreeDemo {
          
          
        public static void main(String[] args) {
          
          
            // 先需要创建一颗二叉树
            BinaryTree binaryTree = new BinaryTree();
            // 创建需要的节点
            HeroNode root = new HeroNode(1, "宋江");
            HeroNode node2 = new HeroNode(2, "吴用");
            HeroNode node3 = new HeroNode(3, "卢俊义");
            HeroNode node4 = new HeroNode(4, "林冲");
            HeroNode node5 = new HeroNode(5, "关胜");
    
            // 说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
            root.setLeft(node2);
            root.setRight(node3);
            node3.setRight(node4);
            node3.setLeft(node5);
            binaryTree.setRoot(root);
    
            // 测试
            System.out.println("前序遍历");
            binaryTree.preOrder();
    
            System.out.println("中序遍历");
            binaryTree.infixOrder();
    
            System.out.println("后序遍历");
            binaryTree.postOrder();
    
            // 前序遍历查找
            System.out.println("前序遍历方式~~~");
            HeroNode resNode = binaryTree.preOrderSearch(5);
            if (resNode != null) {
          
          
                System.out.printf("找到了,信息为 no = %d name = %s", resNode.getNo(),resNode.getName());
            } else {
          
          
                System.out.printf("没有找到 no = %d 的英雄", 5);
            }
        }
    }
    

1.6、二叉树的删除

1.6.1、二叉树删除的要求

  • 如果删除的节点是叶子节点, 则删除该节点
  • 如果删除的节点是非叶子节点, 则删除该子树

1.6.2、代码思路

  • 由于树的本质还是单向链表:

    • 单向链表无法实现自删除,我们需要定位至待删除节点的前一个节点,才能执行删除
    • 同样,如果我们直接定位至二叉树中待删除的节点,那么其父节点信息便会丢失,所以我们一定要定位至待删除节点的父节点
  • 编码思路:

    • 先判断根节点 root 是不是待删除的节点,如果是,则删除根节点,否则开始执行递归
    • 判断当前节点(this)的左节点是否为待删除的节点,如果是,删除 this.left ,然后返回,结束递归
    • 判断当前节点(this)的左节点是否为待删除的节点,如果是,删除 this.right,然后返回,结束递归
    • 否则继续执行左递归,左递归执行完后,执行右递归

在这里插入图片描述

1.6.3、二叉树的递归删除

  • 定义二叉树节点

    // 定义BinaryTree_二叉树
    class BinaryTree {
          
          
        private HeroNode root;
    
        public void setRoot(HeroNode root) {
          
          
            this.root = root;
        }
    
        // 删除节点
        public void delNode(int no) {
          
          
            if (root != null){
          
          
                // 如果只有一个root节点,这里理解判断root是不是要删除的节点
                if (root.getNo() == no) {
          
          
                    root = null;
                } else {
          
          
                    root.delNode(no);
                }
            }else {
          
          
                System.out.println("空树,不能删除!");
            }
        }
    
  • 定义二叉树:二叉树需要一个根节点 root 作为整个树的入口

    // 先创建HeroNode节点
    class HeroNode {
          
          
        private int no;
        private String name;
        private HeroNode left; // 左子树,默认为null
        private HeroNode right; // 右子树,默认为null
    
        public HeroNode(int no, String name) {
          
          
            this.no = no;
            this.name = name;
        }
    
        public int getNo() {
          
          
            return no;
        }
    
        public void setNo(int no) {
          
          
            this.no = no;
        }
    
        public String getName() {
          
          
            return name;
        }
    
        public void setName(String name) {
          
          
            this.name = name;
        }
    
        public HeroNode getLeft() {
          
          
            return left;
        }
    
        public void setLeft(HeroNode left) {
          
          
            this.left = left;
        }
    
        public HeroNode getRight() {
          
          
            return right;
        }
    
        public void setRight(HeroNode right) {
          
          
            this.right = right;
        }
    
        @Override
        public String toString() {
          
          
            return "HeroNode{" +
                    "no=" + no +
                    ", name='" + name + '\'' +
                    '}';
        }
    
        // 递归删除节点
        // 1. 如果删除的节点是叶子节点,则删除该节点
        // 2. 如果删除的节点是非叶子节点,则删除该子树
        public void delNode(int no) {
          
          
            // 思路:
            /*
             * 1. 因为我们的二叉树是单向的,所以我们是判断当前节点的子节点是否是需要删除节点,而不能取判断当前这个节点是不是需要删除节点
             * 2. 如果当前节点的左子节点不为空,并且左子节点就是需要删除节点,就将this.left = null;并且就返回(结束递归删除)
             * 3. 如果当前节点的右子节点不为空,并且右子节点就是需要删除节点,就将this.right = null; 并且返回(结束递归删除)
             * 4. 如果第2步和第3步没有删除节点,那么我们就需要向左子树进行递归删除
             * 5. 如果第4步也没有删除节点,则应当向右子树进行递归删除
             */
    
            // 2. 如果当前节点的左子节点不为空,并且左子节点就是需要删除节点,就将this.left = null;并且就返回(结束递归删除)
            if (this.left != null && this.left.no == no) {
          
          
                this.left = null;
                return;
            }
            // 3. 如果当前节点的右子节点不为空,并且右子节点就是需要删除节点,就将this.right = null; 并且返回(结束递归删除)
            if (this.right != null && this.right.no == no) {
          
          
                this.right = null;
                return;
            }
            // 4.如果第2步和第3步没有删除节点,那么我们就需要向左子树进行递归删除
            if (this.left != null) {
          
          
                this.left.delNode(no);
            }
            // 5.如果第4步也没有删除节点,则应当向右子树进行递归删除
            if (this.right != null) {
          
          
                this.right.delNode(no);
            }
        }
    
    

1.6.4、测试代码

  • 代码

    public class BinaryTreeDemo {
          
          
        public static void main(String[] args) {
          
          
            // 先需要创建一颗二叉树
            BinaryTree binaryTree = new BinaryTree();
            // 创建需要的节点
            HeroNode root = new HeroNode(1, "宋江");
            HeroNode node2 = new HeroNode(2, "吴用");
            HeroNode node3 = new HeroNode(3, "卢俊义");
            HeroNode node4 = new HeroNode(4, "林冲");
            HeroNode node5 = new HeroNode(5, "关胜");
    
            // 说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
            root.setLeft(node2);
            root.setRight(node3);
            node3.setRight(node4);
            node3.setLeft(node5);
            binaryTree.setRoot(root);
    
            System.out.println("删除前,前序遍历");
            binaryTree.preOrder();
            binaryTree.delNode(5);
            System.out.println("删除后,前序遍历");
            binaryTree.preOrder();
        }
    }
    

2、顺序存储二叉树

2.1、顺序存储二叉树的概念

2.1.1、顺序存储二叉树与数组的转换

  • 基本说明:从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树可以转换成数组。

  • 要求:

    • 右图的二叉树的结点,要求以数组的方式来存放 arr : [1, 2, 3, 4, 5, 6, 7]

    • 要求在遍历数组 arr时,仍然可以用前序遍历,中序遍历和后序遍历的方式完成结点的遍历

在这里插入图片描述

2.1.2、顺序存储二叉树的特点

  • 顺序存储二叉树特点

    • 顺序二叉树通常只考虑完全二叉树
    • 顺序存储二叉树中第 n 个元素的左子节点对应的数组下标为 2 * n + 1
    • 顺序存储二叉树中第 n 个元素的右子节点对应的数组下标为 2 * n + 2
    • 顺序存储二叉树中第 n 个元素的父节点对应的数组下标为 (n-1) / 2
    • n 的含义: 表示二叉树中的第几个元素(按0开始编号,如图所示)
  • 下标怎么来的?

    • 顺序存储二叉树通常只考虑完全二叉树,完全二叉树的节点个数规律为:1, 2, 4, 8, 12 , … 即 2 的 n 次方

    • 每个父节点都会延伸出两个子节点,假设当前节点编号为 n (索引从 0 开始),设当前节点在第 x 层(索引从 1 开始),可得如下数据:

      • 1~(x-1) 层所拥有的的节点个数:2n-1 -1

      • 1~x 层所拥有的节点个数:2n - 1

      • 在第 x 层,节点 n 后面有多少个节点:

        back = 2n - 1 - n - 1 = 2n -n - 2

      • 在第 x 层,节点 n 前面有多少个节点:

        front = n - (2n-1 -1) = n - 2n-1 + 1

      • 在第 x+1 层,节点 n 左节点前面有多少个节点:

        frontDouble = 2 * (n - 2n-1 + 1) = 2n - 2n + 2

      • 所以节点 n 左节点的索引为:

        index = n + back + frontDouble + 1 = n + (2n -n - 2) + (2n - 2n + 2) + 1 = 2*n + 1

在这里插入图片描述

2.2.2、前 中 序遍历

  • 编写顺序存储二叉树的前序、中序、后序遍历

    // 编写一个ArrBinaryTree,实现顺序存储二叉树遍历
    class ArrBinaryTree {
          
          
        private int[] arr; // 存储数据节点的数组
    
        public ArrBinaryTree(int[] arr) {
          
          
            this.arr = arr;
        }
    
        // 重载preOrder
        public void preOrder() {
          
          
            this.preOrder(0);
        }
    
        public void infixOrder() {
          
          
            this.infixOrder(0);
        }
    
        // 编写一个方法,完成顺序存储二叉树的前序遍历
        /*
         * @Description:
         * @Param: [index:数组的下标]
         * @Return: void
         * @Author: Daniel
         * @Date: 2020/12/6
         */
        private void preOrder(int index) {
          
          
            // 如果数组为空,或者arr.length = 0
            if (arr == null || arr.length == 0) {
          
          
                System.out.println("数组为空,不能按照二叉树的前序遍历");
            }
            // 输出这个元素
            System.out.print(arr[index] + " ");
            // 向左递归遍历
            if ((index * 2 + 1) < arr.length) {
          
          
                preOrder(2 * index + 1);
            }
            // 向右递归遍历
            if ((index * 2 + 2) < arr.length) {
          
          
                preOrder(2 * index + 2);
            }
        }
    
        // 中序遍历
        private void infixOrder(int index) {
          
          
            // 如果数组为空,或者arr.length = 0
            if (arr == null && arr.length == 0) {
          
          
                System.out.println("数组为空,不能按照二叉树的中序遍历");
            }
            // 向左递归遍历
            if ((index * 2 + 1) < arr.length) {
          
          
                infixOrder(index * 2 + 1);
            }
            // 输出这个元素
            System.out.print(arr[index] + " ");
            // 向右递归遍历
            if ((index * 2 + 2) < arr.length) {
          
          
                infixOrder(index * 2 + 2);
            }
        }
    }
    

2.2.3、测试代码

public class ArrBinaryTreeDemo {
    
    

    public static void main(String[] args) {
    
    
        int[] arr = {
    
    1, 2, 3, 4, 5, 6, 7};
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
        System.out.println("前序遍历:");
        arrBinaryTree.preOrder();
        System.out.println();
        System.out.println("中序遍历:");
        arrBinaryTree.infixOrder();
    }

}
  • 程序运行结果

    前序遍历:
    1 2 4 5 3 6 7 
    中序遍历:
    4 2 5 1 6 3 7 
    

2.3、顺序存储二叉树应用实例

  • 八大排序算法中的堆排序,就会使用到顺序存储二叉树, 关于堆排序,我们放在【树结构实际应用】章节讲解。

3、线索化二叉树

3.1、引出问题

  • 将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树,当我们对这颗二叉树进行中序遍历时, 输出数列为 {8, 3, 10, 1, 6, 14 }

  • 但是 6, 8, 10, 14 这几个节点的左右指针,并没有完全的利用上,如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?

  • 解决方案:线索二叉树

在这里插入图片描述

3.2、线索二叉树基本介绍

  • n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。 利用二叉链表中的空指针域, 存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")

  • 这种加上了线索的二叉链表称为线索链表, 相应的二叉树称为线索二叉树(Threaded BinaryTree)。

  • 根据线索性质的不同, 线索二叉树可分为前序线索二叉树、 中序线索二叉树和后序线索二叉树三种

  • 前驱结点和后继节点:

    • 一个结点的前一个结点, 称为前驱结点
    • 一个结点的后一个结点, 称为后继结点
  • 当我们对二叉树进行中序遍历时, 得到的数列为 {8, 3, 10, 1, 6, 14 }

    • 那么 8 节点的前驱结点为 null ,8 和后驱节点为 3

    • 那么 3 节点的前驱结点为 8 ,3 和后驱节点为 10

    • 以此类推…

在这里插入图片描述

3.3、线索二叉树的生成

3.3.1、代码思路

  • 如何实现线索二叉树?以中序线索化为例,按照上面的步骤操作即可

  • 怎么知道当前节点的前驱结点和后继节点?

    • 想想单链表的删除是怎么做的?我们记录当前节点的前一个节点,在程序中使用 pre 指针记录当前正在遍历的节点 curNode ,以及其前驱结点 preNode
    • 如果条件满足,preNode 就是 curNode 的前驱结点:curNode.left = pre;
    • 如果条件满足,curNode 节点就是 preNode 的后继节点:pre.right = curNode;
  • 何时递归完毕?当前递归至最深层,curNode 为空时开始回溯:curNode == null

  • 举例说明:

    • 我们进行中序遍历时,先执行左递归至最深层

    • 对于图例来说,就是递归到节点 8 左节点,满足 node.left == null ,开始执行回溯

    • 于是我们回到节点8 ,该节点并没有前驱结点,即前驱结点为 null ,恰好此时 pre 也为 null

    • 设置节点 8 的前驱结点:node.left = pre;

    • 操作完成后,pre 和 node 指针向后移动一步

在这里插入图片描述

  • 回溯到节点 3 ,节点 3 的 pre 节点是节点 8

  • 设置节点 8 的后继节点:pre.right = node;

  • 操作完成后,pre 和 node 指针向后移动一步

在这里插入图片描述

  • 继续执行右递归,节点 10 的前驱结点为 3

  • 设置节点 10 的前驱结点:node.left = pre;

  • 操作完成后,pre 和 node 指针向后移动一步

在这里插入图片描述

  • 以此类推,递归至最后,所有线索都已设置完毕

  • 操作完成后,pre 和 node 指针向后移动一步,此时 node == null ,递归完毕,节点 6 的 right 指针的类型为真正的子节点,并非

在这里插入图片描述

大致流程:

  • 递归结束条件:node == null

  • 执行左递归

  • 设置前驱结点、后继节点,并后移 pre 和 node 指针(pre 指针需手动移动,node 指针其实是通过递归来移动的)

  • 执行右递归

在这里插入图片描述

  • 关于线索化之后的二叉树:只有一个右节点的 right 域为 null ,这个右节点是整棵树最深层的右节点,遍历到该右节点时,表示整棵树遍历完成

3.3.2、代码实现

  • 由于将原有的指针域(left 和 right 指针)指向了前驱节点和后继节点,所以代码里面需要表示当前指针到底是指向了真正的子节点还是前驱节点或后继节点
  • leftType 、rightType:0 表示指向真正的子节点,1 表示指向前驱节点或后继节点
// 先创建HeroNode节点
class HeroNode {
    
    
    private int no;
    private String name;
    private HeroNode left; // 左子树,默认为null
    private HeroNode right; // 右子树,默认为null

    // 说明:
    // 1. 如果leftType == 0 表示指向的是左子树,如果是1则表示指向前驱节点
    // 2. 如果rightType == 0 表示指向的是右子树,如果是1则表示指向后继节点
    private int leftType;
    private int rightType;

    public int getLeftType() {
    
    
        return leftType;
    }

    public void setLeftType(int leftType) {
    
    
        this.leftType = leftType;
    }

    public int getRightType() {
    
    
        return rightType;
    }

    public void setRightType(int rightType) {
    
    
        this.rightType = rightType;
    }

    public HeroNode(int no, String name) {
    
    
        this.no = no;
        this.name = name;
    }

    public int getNo() {
    
    
        return no;
    }

    public void setNo(int no) {
    
    
        this.no = no;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public HeroNode getLeft() {
    
    
        return left;
    }

    public void setLeft(HeroNode left) {
    
    
        this.left = left;
    }

    public HeroNode getRight() {
    
    
        return right;
    }

    public void setRight(HeroNode right) {
    
    
        this.right = right;
    }

    @Override
    public String toString() {
    
    
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    } 
}
  • 线索二叉树的定义:

    // 定义ThreadedBinaryTree 实现了线索化功能的二叉树
    class ThreadedBinaryTree {
          
          
        private HeroNode root;
    
        // 为了实现线索化,需要创建要给指向当前节点的前驱节点的指针
        // 在递归进行线索化时,pre重视保留前一个节点
        private HeroNode pre = null;
    
        public void setRoot(HeroNode root) {
          
          
            this.root = root;
        }
    
        // 重载threadedNodes方法
        public void threadedNodes() {
          
          
            this.threadedNodes(root);
        }
    
        // 编写对二叉树进行中序线索化的方法
        public void threadedNodes(HeroNode node) {
          
          
            // 如果node == null, 不能线索化
            if (node == null) {
          
          
                return;
            }
    
            // (一)线索化左子树
            threadedNodes(node.getLeft());
    
            // (二)线索化当前节点
            // 处理当前节点的前驱节点
            // 以8节点来理解
            // 8节点的.left = null , 8节点的.leftType = 1
            if (node.getLeft() == null) {
          
          
                // 让当前节点的左指针指向前驱节点
                node.setLeft(pre);
                // 修改前驱节点的右指针类型
                node.setLeftType(1);
            }
    
            // 处理后继节点
            if (pre != null && pre.getRight() == null) {
          
          
                // 让前驱节点的右指针指向当前节点
                pre.setRight(node);
                // 修改前驱节点的右指针类型
                pre.setRightType(1);
            }
    
            // ! ! ! 每处理一个节点后,让当前节点是下一个节点的前驱节点
            pre = node;
    
            // (三)线索化右子树
            threadedNodes(node.getRight());
        }
    
    
        // 后序线索二叉树
        public void threadedNodes_post(HeroNode node) {
          
          
            if (node == null) {
          
          
                return;
            }
            // 1. 线索化左子树
            threadedNodes_post(node.getLeft());
            // 2. 线索化右子树
            threadedNodes_post(node.getRight());
    
            // 3. 线索化当前节点
            // 处理前驱节点
            if(node.getLeft() == null) {
          
          
                node.setLeft(pre);
                node.setLeftType(1);
            }
            // 处理后继节点
            if (pre != null && pre.getRight() == null) {
          
          
                pre.setRight(node);
                pre.setRightType(1);
            }
            pre = node;
        }
    }
    

3.3.3、测试代码

  • 代码

    public class ThreadedBinaryTreeDemo {
          
          
    
        public static void main(String[] args) {
          
          
    
            HeroNode root = new HeroNode(1, "tom");
            HeroNode node2 = new HeroNode(3, "jack");
            HeroNode node3 = new HeroNode(6, "smith");
            HeroNode node4 = new HeroNode(8, "mary");
            HeroNode node5 = new HeroNode(10, "king");
            HeroNode node6 = new HeroNode(14, "dim");
    
            // 二叉树,后面要递归创建,现在简单处理使用手动创建
            root.setLeft(node2);
            root.setRight(node3);
            node2.setLeft(node4);
            node2.setRight(node5);
            node3.setLeft(node6);
    
            // 测试线索化
            ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
            threadedBinaryTree.setRoot(root);
            threadedBinaryTree.threadedNodes();
    
            // 测试:以10号节点测试
            HeroNode leftNode = node5.getLeft();
            HeroNode rightNode = node5.getRight();
            System.out.println("10号节点的中序前驱节点是" + leftNode);
            System.out.println("10号节点的中序后继节点是" + rightNode);
        }
    }
    
  • 运行结果

    10号节点的前驱节点是HeroNode{no=3, name='jack'}
    10号节点的后继节点是HeroNode{no=1, name='tom'}
    

3.4、线索二叉树的遍历

3.4.1、代码思路

  • 为什么我们要做线索化?

    • 因为线索化后, 各个结点指向有变化, 因此原来的遍历方式不能使用, 这时需要使用新的方式遍历线索化二叉树, 各个节点可以通过线性方式遍历, 因此无需使用递归方式, 这样也提高了遍历的效率。 遍历的次序应当和中序遍历保持一致。
    • 以中序遍历为例,线索化可以让每个具有前驱结点的左节点right 指针都能指向其下一个要遍历的节点(后继节点)
    • 我们先找到第一个具有前驱结点的左节点(整棵树最深层的左叶子节点),我们沿着该左叶子节点的后继节点遍历,可以中序遍历的结果
    • 如果遍历到的节点没有后继节点,那么说明该节点 left 和 right 都有节点,直接输出其右节点,并以该右节点根节点,继续执行上一步操作,往最深处寻找第一个具有前驱结点的左节点(当前步骤最深层的左叶子节点),我们继续沿着左叶子节点的后继节点遍历
    • 如此往复,直到最后得到的右节点为 null,上面有讲过,线索二叉树有且只有一个为 null 的 right 指针,该值为 null 时代表整棵树的结束
  • 举例说明:

    • 因为执行的中序遍历:左 --> 中 --> 后,先执行深度搜索,找到第一个有前驱结点的节点,第一次搜索找到节点 8 ,其前驱结点为 null ,从节点 8 的后继节点为节点 3

在这里插入图片描述

  • 节点 3 并没有后继节点,只有右节点 10 ,我们拿到节点 10

在这里插入图片描述

  • 寻找节点 10 下第一个具有前驱结点的节点,恰好就是它本身,然后寻找其后继节点

在这里插入图片描述

  • 以此类推…

  • 大致思路:

    • 寻找最深层的具有前驱结点的左节点

    • 沿着左节点遍历其后继节点

    • 如果没有后继节点,则直接输出其右节点,并以该右节点为根节点,继续寻找最深层的具有前驱结点的左节点

    • 如此往复,直到最后得到的右节点值为 null

在这里插入图片描述

3.4.2、代码实现

  • HeroNode 节点(没有变化)

    // 先创建HeroNode节点
    class HeroNode {
          
          
        private int no;
        private String name;
        private HeroNode left; // 左子树,默认为null
        private HeroNode right; // 右子树,默认为null
    
        public HeroNode(int no, String name) {
          
          
            this.no = no;
            this.name = name;
        }
    
        public int getNo() {
          
          
            return no;
        }
    
        public void setNo(int no) {
          
          
            this.no = no;
        }
    
        public String getName() {
          
          
            return name;
        }
    
        public void setName(String name) {
          
          
            this.name = name;
        }
    
        public HeroNode getLeft() {
          
          
            return left;
        }
    
        public void setLeft(HeroNode left) {
          
          
            this.left = left;
        }
    
        public HeroNode getRight() {
          
          
            return right;
        }
    
        public void setRight(HeroNode right) {
          
          
            this.right = right;
        }
    
        @Override
        public String toString() {
          
          
            return "HeroNode{" +
                    "no=" + no +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
  • 在 ThreadedBinaryTree 类中添加遍历方法

    // 定义ThreadedBinaryTree 实现了线索化功能的二叉树
    class ThreadedBinaryTree {
          
          
        private HeroNode root;
    
        // 为了实现线索化,需要创建要给指向当前节点的前驱节点的指针
        // 在递归进行线索化时,pre重视保留前一个节点
        private HeroNode pre = null;
    
        public void setRoot(HeroNode root) {
          
          
            this.root = root;
        }
    
        // 重载threadedNodes方法
        public void threadedNodes() {
          
          
            this.threadedNodes(root);
        }
    
        // 遍历线索化二叉树的方法
        public void threadedList() {
          
          
            // 定义一个变量,存储当前遍历的节点,从root开始
            HeroNode node = root;
            while (node != null) {
          
          
                // 循环的找到leftType = 1 的节点,第一个找到的就是8节点
                // 后面随着遍历而变化,因为当leftType==1时,说明该节点是按照线索化处理后的有效节点
                while (node.getLeftType() == 0) {
          
          
                    node = node.getLeft();
                }
                // 打印这个节点
                System.out.println(node);
                // 如果当前节点的右指针指向的是后继节点,就一直输出
                while (node.getRightType() == 1) {
          
          
                    // 获取到当前节点的后继节点
                    node = node.getRight();
                    System.out.println(node);
                }
                // 替换这个遍历的节点
                node = node.getRight();
            }
        }
    }
    

猜你喜欢

转载自blog.csdn.net/DDDDeng_/article/details/111246778