树结构基础

157 阅读17分钟

1. 二叉树的基本概念

1.1 为什么需要树这种数据结构

  1. 数组存储方式的分析:

    优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。

    缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低。

  2. 链式存储方式的分析:

    优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。

    缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)。

  3. 树存储方式分析:

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

1.2 树的常用术语

  1. 结点
  2. 根结点
  3. 父结点
  4. 子结点
  5. 叶子结点(没有子结点的结点)
  6. 结点的权(结点值)
  7. 路径(从root结点找到该结点的路线)
  8. 子树
  9. 树的高度(最大层数)
  10. 森林(多棵子树构成森林)

1.3 二叉树的概念

  1. 每个结点最多只能有两个子节点的树称为二叉树,二叉树的子结点分为左结点和右结点。

  1. 满二叉树:该二叉树的所有子结点都在最后一层,并且结点总数为 2^n-1 ,n为层数。

  1. 完全二叉树:该二叉树的所有叶子节点都在最后一层或者倒数第二层,且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续。

2. 二叉树的遍历

  • 前序遍历:先输出父结点,再遍历左子树和右子树。
  • 中序遍历:先遍历左子树,再输出父结点,再遍历右子树。
  • 后序遍历:先遍历左子树,再遍历右子树,再输出父结点

总结:输出父结点的顺序确定是前序、中序还是后序。

2.1 实例:

  1. 对此二叉树进行前序、中序、后序遍历。
  2. 分别用前序查找、中序查找、后序查找三种方式,查找5号结点,看分别查找了多少次。
  3. 删除二叉树的结点:叶子结点直接删除该结点,非叶子结点删除该子树。
package com.atguigu.Tree;

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("前序遍历:");//1 2 3 5 4
        binaryTree.preOrder();

        System.out.println("中序遍历:");//2 1 5 3 4
        binaryTree.infixOrder();

        System.out.println("后序遍历:");//2 5 4 3 1
        binaryTree.postOrder();


        //前序遍历查找
        //前序遍历的次数:4
        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);
        }

        //中序遍历查找
        //中序遍历的次数:3
        //System.out.println("中序遍历方式~~~");
        //HeroNode resNode = binaryTree.infixOrderSearch(5);
        //if (resNode != null) {
        //    System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
        //} else {
        //    System.out.printf("没有找到 no = %d 的英雄", 5);
        //}

        //后序遍历查找
        //System.out.println("后序遍历方式~~~");
        //HeroNode resNode = binaryTree.postOrderSearch(5);
        //if (resNode != null) {
        //    System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
        //} else {
        //    System.out.printf("没有找到 no = %d 的英雄", 5);
        //}

        //测试删除结点
        System.out.println("删除前,前序遍历");
        binaryTree.preOrder();//1 2 3 5 4
        binaryTree.delNode(5);
        System.out.println("删除后,前序遍历");
        binaryTree.preOrder();//1 2 3 4
    }
}

//定义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("空树,不能删除~");
        }
    }

    //前序遍历
    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;
        }
    }
}

//先创建 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.那么就需要向左子树进行递归删除
        if (this.left != null) {
            this.left.delNode(no);
        }
        //5.则应当向右子树进行递归删除
        if (this.right != null) {
            this.right.delNode(no);
        }
    }

    //编写前序遍历的方法
    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.postOrder();
        }
        //递归向右子树后序遍历
        if (this.right != null) {
            this.right.postOrder();
        }
        //输出父结点
        System.out.println(this);
    }

    //前序遍历查找

    /**
     * @param no 查找no
     * @return 如果找到就返回该Node,如果没有找到就返回null
     */
    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;
        }

        //1.左递归前序查找,找到结点,则返回,否则继续判断
        //2.判断当前结点的右子结点是否为空,如果不为空,则继续向右递归前序查找
        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;
        }

        System.out.println("进入中序查找");
        //如果没有找到,就和当前结点比较,如果是则返回当前结点
        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;
        }

        System.out.println("进入后序查找");
        //如果左右子树都没有找到,就比较当前结点是不是
        if (this.no == no) {
            return this;
        }
        return resNode;
    }
}

output:

前序遍历:
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='宋江'}

前序遍历方式~~~
进入前序遍历
进入前序遍历
进入前序遍历
进入前序遍历
找到了,信息为:no=5 name=关胜

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

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

3. 顺序存储二叉树

3.1 基本概念

从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组。

如图中二叉树的结点以数组的方式进行存放 arr[1, 2, 3, 4, 5, 6, 7]。

3.2 特点

  1. 顺序存储二叉树通常指考虑完全二叉树
  2. 第n个元素的左子结点为 2*n+1
  3. 第n个元素的右子结点为 2*n+2
  4. 第n个元素的父结点为 (n-1)/2

n表示二叉树中的din个元素(n从0开始)。

3.3 代码实现

将数组 [1,2,3,4,5,6,7] 以二叉树前序遍历的方式进行遍历,遍历的结果应为1, 2, 4, 5, 3, 6, 7。


public class ArrBinaryTreeDemo {

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7};
        //创建一个 ArrBinaTree
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
        arrBinaryTree.preOrder();//1 2 4 5 3 6 7
    }
}

//编写一个ArrayBinaryTree,实现顺序存储二叉树遍历
class ArrBinaryTree {
    private int[] arr;//存储数据结点二叉树遍历

    public ArrBinaryTree(int[] arr) {
        this.arr = arr;
    }

    //重载preOrder
    public void preOrder() {
        this.preOrder(0);
    }

    //编写一个方法,完成顺序存储二叉树的前序遍历
    /**
     *
     * @param index 数组的下标
     */
    public void preOrder(int index) {
        if (arr == null || arr.length == 0) {
            System.out.println("数组为空,不能按照二叉树的前序遍历");
        }
        //输出当前这个元素
        System.out.println(arr[index]);
        //向左递归遍历
        if ((index * 2 + 1) < arr.length) {
            preOrder(2 * index + 1);
        }
        //向右递归遍历
        if ((index * 2 + 2) < arr.length) {
            preOrder(2 * index + 2);
        }
    }
}
output:
1
2
4
5
3
6
7

4. 线索化二叉树

4.1 基本概念

  1. n个结点的二叉链表中含有 n+1 [公式:2n-(n-1)=n+1] 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")。
  2. 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
  3. 一个结点的前一个结点,称为前驱结点。
  4. 一个结点的后一个结点,称为后继结点。

4.2 案例

将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}

说明: 当线索化二叉树后,Node节点的 属性 left 和 right ,有如下情况:

  1. left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点。
  2. right指向的是右子树,也可能是指向后继节点,比如 ① 节点right 指向的是右子树,而⑩ 节点的right 指向的是后继节点。

public class ThreadBinaryTreeDemo {

    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);//3
        System.out.println("10号结点的后继结点是:" + rightNode);//1

        //当线索化二叉树后,不能再使用原来的遍历方法
        //threadedBinaryTree.infixOrder();
        System.out.println("使用线索化的方式遍历 线索化二叉树");
        threadedBinaryTree.threadedList();//8 3 10 1 14 6
    }
}

//定义 ThreadedBinaryTree 实现了线索化功能的二叉树
class ThreadedBinaryTree {
    private HeroNode root;

    //为了实现线索化,需要创建要给指向当前结点的前驱结点的指针
    //在递归进行线索化时,pre 总是保留前一个结点
    private HeroNode pre = null;

    public void setRoot(HeroNode root) {
        this.root = root;
    }

    //重载 threadNodes 方法
    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();
        }
    }

    //编写对二叉树进行中序线索化的方法
    /**
     *
     * @param node 就是当前需要线索化的结点
     */
    public void threadedNodes(HeroNode node) {

        //如果当前结点为空,就不能线索化
        if (node == null) {
            return;
        }

        //(一)先线索化左子树
        threadedNodes(node.getLeft());

        //(二)线索化当前结点

        //处理当前结点的前驱结点
        //以8结点来理解
        //8结点.left = null,8结点.leftType = 1
        if (node.getLeft() == null) {
            //让当前结点的左指针指向前驱结点
            node.setLeft(pre);
            //修改当前结点的左指针的类型,指向前驱结点
            node.setLeftType(1);
        }

        //处理pre的后继节点
        if (pre != null && pre.getRight() == null) {
            //让前驱结点的右指针指向当前结点
            pre.setRight(node);
            //修改前驱结点的右指针类型
            pre.setRightType(1);
        }

        //!!! 每处理一个结点后,让当前结点变为下一个结点的前驱结点
        pre = node;

        //(三)在线索化右子树
        threadedNodes(node.getRight());
    }

    //删除结点
    public void delNode(int no) {
        if (root != null) {
            //如果只有一个root结点,判断root是否就是要删除的结点
            if (root.getNo() == no) {
                root = null;
            } else {
                //递归删除
                root.delNode(no);
            }
        } else {
            System.out.println("空树,不能删除~");
        }
    }

    //前序遍历
    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;
        }
    }
}

//创建 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 + '\'' +
                '}';
    }

    //递归删除结点
    //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.那么就需要向左子树进行递归删除
        if (this.left != null) {
            this.left.delNode(no);
        }
        //5.则应当向右子树进行递归删除
        if (this.right != null) {
            this.right.delNode(no);
        }
    }

    //编写前序遍历的方法
    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.postOrder();
        }
        //递归向右子树后序遍历
        if (this.right != null) {
            this.right.postOrder();
        }
        //输出父结点
        System.out.println(this);
    }

    //前序遍历查找

    /**
     * @param no 查找no
     * @return 如果找到就返回该Node,如果没有找到就返回null
     */
    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;
        }

        //1.左递归前序查找,找到结点,则返回,否则继续判断
        //2.判断当前结点的右子结点是否为空,如果不为空,则继续向右递归前序查找
        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;
        }

        System.out.println("进入中序查找");
        //如果没有找到,就和当前结点比较,如果是则返回当前结点
        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;
        }

        System.out.println("进入后序查找");
        //如果左右子树都没有找到,就比较当前结点是不是
        if (this.no == no) {
            return this;
        }
        return resNode;
    }
}
output:
10号结点的前驱结点是:HeroNode{no=3, name='jack'}
10号结点的后继结点是:HeroNode{no=1, name='tom'}

使用线索化的方式遍历 线索化二叉树
HeroNode{no=8, name='mary'}
HeroNode{no=3, name='jack'}
HeroNode{no=10, name='king'}
HeroNode{no=1, name='tom'}
HeroNode{no=14, name='dim'}
HeroNode{no=6, name='smith'}