[ 数据结构 ] 二叉树详解--------前序、中序、后序、存储、线索化

0 前言

  1. why?为什么需要树结构?

    数组虽然查找快(通过下标访问),但无法动态扩容(拷贝到新的数组),而链表相反,树结构刚好结合两者优点

  2. 浅谈树?

    树的存储和读取效率都很高,比如二叉排序树,理解树的抽象模型有助于理解递归的原理,树的模型接近于单向链表,父节点指向左右子树,而链表相比二叉树可以看成单叉

1 初识二叉树

  1. 遍历:分为前、中、后序,区分前中后简单说就是对当前节点的处理操作(打印)是在左右子节点的递归调用的前面、中间、还是后面,比如前序:先打印当前节点,再分别左右子节点递归调用
  2. 查找:同样有前、中、后序,取决于判断操作是在两个递归调用的前面、中间、后面,当然,如果到方法的最后(搜索到叶子节点)都没能找到目标,需要返回null(结果需要在判断之后立即返回,不可以先收集再在末尾返回)
  3. 删除:先找再删,这里的找不同于2中的查找,判断操作是在当前节点判断子节点是否要删除,毕竟只能通过父节点的指针删除,而不能自己删除自己

image-20230108112820521.png

//二叉树的前序、中序、后序遍历,前中后序查找,删除三类方法
//主类+树类+节点类
public class Tree01_BinaryTreeDemo {
    public static void main(String[] args) {

        //手动创建二叉树
        BinaryTree tree = new BinaryTree();
        Hero root  = new Hero(1, "宋江");
        Hero node2 = new Hero(2, "吴用");
        Hero node3 = new Hero(3, "卢俊义");
        Hero node4 = new Hero(4, "林冲");
        Hero node5 = new Hero(5, "关胜");
        Hero node8 = new Hero(8, "晁盖");
        Hero node7 = new Hero(7, "武松");

        root.setLeft(node2);
        root.setRight(node3);
        node3.setRight(node4);
        node3.setLeft(node5);
        node2.setLeft(node8);
        node2.setRight(node7);
        tree.setRoot(root);

        //遍历
        tree.preOrder();//12354
//        tree.infixOrder();//21534
//        tree.postOrder();//25431

        //查找
        tree.postOrderSearch(4);

        //删除
//        tree.delNode(3);
//        System.out.println("删除后:");
//        tree.preOrder();
    }
}

//二叉树
class BinaryTree {
    private Hero root;

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

    //三种方式遍历树,从root起
    public void preOrder() {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("二叉树为空");
        }
    }
    public void infixOrder() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("二叉树为空");
        }
    }
    public void postOrder() {
        if (root != null) {
            root.postOrder();
        } else {
            System.out.println("二叉树为空");
        }
    }

    //三种方式查找树,从root起
    public void preOrderSearch(int no) {
        if (root != null) {
            if (root.preOrderSearch(no) != null) {
                System.out.println("查找结果为" + root.preOrderSearch(no));
            } else {
                System.out.println("没找到!!!");
            }
        } else {
            System.out.println("二叉树为空");
        }
    }
    public void infixOrderSearch(int no) {
        if (root != null) {
            if (root.infixOrderSearch(no) != null) {
                System.out.println("查找结果为" + root.infixOrderSearch(no));
            } else {
                System.out.println("没找到!!!");
            }
        } else {
            System.out.println("二叉树为空");
        }
    }
    public void postOrderSearch(int no) {
        if (root != null) {
            Hero result= root.postOrderSearch(no);
            if (result != null) {
                System.out.println("查找结果为" + result);
            } else {
                System.out.println("没找到!!!");
            }
        } else {
            System.out.println("二叉树为空");
        }
    }

    //删除节点
    public void delNode(int no) {
        if (root == null) {
            System.out.println("二叉树空,无法删除");
        } else {
            //删除的节点就是root,直接置空,否则遍历删除
            if (root.getNo() == no) {
                root = null;
            } else {
                root.delNode(no);
            }
        }
    }
}

//树节点
//编写三种遍历方式
class Hero {
    private int no;
    private String name;
    private Hero left;
    private Hero right;

    public Hero(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 Hero getLeft() {
        return left;
    }

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

    public Hero getRight() {
        return right;
    }

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

    @Override
    public String toString() {
        return "HeroNode [no=" + no + ", name=" + name + "]";
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (left != null) {
            left.preOrder();
        }
        if (right != null) {
            right.preOrder();
        }
    }
    //中序遍历
    public void infixOrder() {
        if (left != null) {
            left.infixOrder();
        }
        System.out.println(this);
        if (right != null) {
            right.infixOrder();
        }
    }
    //后序遍历
    public void postOrder() {
        if (left != null) {
            left.postOrder();
        }
        if (right != null) {
            right.postOrder();
        }
        System.out.println(this);
    }

    //前序查找
    //这里当前节点的查找结果毫无疑问若正确可以直接返回
    //子节点的查找结果必须先收集再判断,若正确直接返回,为空则在方法末尾返回
    //  =>如果不立即判断返回则当查找到第一个叶子节点直接必出结果,即路径上遇到结果就返回,到叶子都没遇到就返回null了
    //整体判断逻辑:当前节点能否返回(能否返回:找到正确结果才返回)-->左递归能否返回-->右递归直接返回-->补上方法返回值null
    //总结:以前序为例,无论是遍历还是查找,对于当前节点的操作其实只是左递归前的附带操作,
    //      因此从执行结果来看,整体是先从上往下执行,(中+左)=>(中+左)=>(中+左)=>叶子,然后开始从下层处理到上层
    public Hero preOrderSearch(int no) {

        if (this.no == no) {
            return this;
        }

        if (left != null) {
            if (left.preOrderSearch(no) != null) {
                return left.preOrderSearch(no);
            }
        }

        if (right != null) {
            return right.preOrderSearch(no);
        }

        return null;
    }
    //中序查找
    public Hero infixOrderSearch(int no) {

        if (left != null) {
            if (left.infixOrderSearch(no) != null) {
                return left.infixOrderSearch(no);
            }
        }
        if (this.no == no) {
            return this;
        }
        if (right != null) {
            if (right.infixOrderSearch(no) != null) {
                return right.infixOrderSearch(no);
            }
        }
        return null;
    }
    //后序查找
    public Hero postOrderSearch(int no) {
        Hero result = null;

        if (left != null) {
            result = left.postOrderSearch(no);
            if (result != null) {
                return result;
            }
        }
        if (right != null) {
            result = right.postOrderSearch(no);
            if (result != null) {
                return result;
            }
        }

        //统计比较多少次,以后序查找为例,
        //注意为了得到正确的比较次数,需要将递归结果先收集再做判断或返回,否则次数会翻倍
        System.out.println("一次");
        if (this.no == no) {
            return this;
        }
        return result;
    }

    //删除节点
    //核心:在当前节点判断是否删除子节点(因为单向二叉树只能操作自己的左右指针),注意根节点的判断在树类中
    //若删除了子节点,则直接返回结束方法(递归),否则调用递归删除
    public void delNode(int no) {
        if (this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        if (this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        if (this.left != null) {
            this.left.delNode(no);
        }
        if (this.right != null) {
            this.right.delNode(no);
        }
    }
}

2 顺序存储二叉树

说明:

  1. 顺序存储二叉树其实就是将数组转换成树,也可以说是将树节点存放在数组中
  2. 根节点表示下标为0的数组元素,那么下标为n的节点,它的左子节点下标(2 * n + 1),右子节点(2 * n - 1),父节点(n-1) / 2.
  3. 按要求,顺序二叉树需满足完全二叉树(叶子节点在最下面两层,最后一层左侧连续)

image-20230108111944477.png

//顺序存储二叉树(数组和树可相互转换,这里用数组存放二叉树节点)
//数组存储元素顺序是按树层序遍历顺序......
public class Tree02_ArrBinaryTreeDemo {
    public static void main(String[] args) {
        int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
        arrBinaryTree.preOrder();

    }
}

//树
class ArrBinaryTree {
    private int[] arr;

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

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

    //前序遍历
    //核心:父子节点间的索引计算等式,左子节点2n+1,右子节点2n+2,父节点(n-1)/2
    //递归终止条件为索引越界arr.length,当然给递归调用加if判断也可以终止调用
    public void preOrder(int index) {
        if (arr == null || arr.length == 0) {
            System.out.println("数为空,无法遍历!");
            return;
        }
        if (index >= 0 && index < arr.length) {
            System.out.println(arr[index]);
        } else {
            return;
        }

        preOrder(2 * index + 1);
        preOrder(2 * index + 2);

    }
}

3 线索化二叉树

image-20230108105115964.png

  1. 问题:先看上图,数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树,节点数n=6,每个节点两个指针计算,满打满算2n个指针,但是除去根节点只有n-1个指针指向已有节点,算下来就有2n-(n-1)=n+1个空指针,怎么用这些个空指针呢?
  2. 以中序遍历为例,如果遍历时利用这些空指针按规则指向中序遍历结果的相邻节点(前驱节点/后继节点),那么就会将树中序线索化,也就有了后面新的遍历方式(线型遍历)
  3. 线型遍历:利用线索二叉树叶子节点连成的线索,以非递归的方式遍历树(有点像链表)
  4. 线索化代码核心:定义全局变量pre用来存放中序遍历的前驱节点(毕竟是单向的二叉树,无法在当前节点让前驱节点指向自己),通过pre节点和当前节点(两节点形成前驱-后继关系)来改变原来空指针的指向

image-20230108111712367.png

//二叉树的中序线索化及其线型遍历
public class Tree03_ThreadedBinaryTreeDemo {
    public static void main(String[] args) {

        //手动创建二叉树
        Ho root = new Ho(1, "tom");
        Ho node2 = new Ho(3, "jack");
        Ho node3 = new Ho(6, "smith");
        Ho node4 = new Ho(8, "mary");
        Ho node5 = new Ho(10, "king");
        Ho node6 = new Ho(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.threadedNode();
//        System.out.println(node5.getLeft().toString());
//        System.out.println(node5.getRight().toString());

        threadedBinaryTree.threadedList();


    }
}

//二叉树
class ThreadedBinaryTree {
    private Ho root;
    private Ho pre = null;//前驱节点

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

    //三种方式遍历树,从root起
    public void preOrder() {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("二叉树为空");
        }
    }
    public void infixOrder() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("二叉树为空");
        }
    }
    public void postOrder() {
        if (root != null) {
            root.postOrder();
        } else {
            System.out.println("二叉树为空");
        }
    }

    //三种方式查找树,从root起
    public void preOrderSearch(int no) {
        if (root != null) {
            if (root.preOrderSearch(no) != null) {
                System.out.println("查找结果为" + root.preOrderSearch(no));
            } else {
                System.out.println("没找到!!!");
            }
        } else {
            System.out.println("二叉树为空");
        }
    }
    public void infixOrderSearch(int no) {
        if (root != null) {
            if (root.infixOrderSearch(no) != null) {
                System.out.println("查找结果为" + root.infixOrderSearch(no));
            } else {
                System.out.println("没找到!!!");
            }
        } else {
            System.out.println("二叉树为空");
        }
    }
    public void postOrderSearch(int no) {
        if (root != null) {
            Ho result= root.postOrderSearch(no);
            if (result != null) {
                System.out.println("查找结果为" + result);
            } else {
                System.out.println("没找到!!!");
            }
        } else {
            System.out.println("二叉树为空");
        }
    }

    //删除节点
    public void delNode(int no) {
        if (root == null) {
            System.out.println("二叉树空,无法删除");
        } else {
            //删除的节点就是root,直接置空,否则遍历删除
            if (root.getNo() == no) {
                root = null;
            } else {
                root.delNode(no);
            }
        }
    }

    //中序线索化
    //切入点:线索化无非就是改变左右指向,从null改为指向前驱和后继节点,但是遍历到当前节点时只经过了前驱节点,不可能拿到后继节点,所以...
    //中序遍历的基本思想,处理当前节点时:若需要线索化则改变当前节点的左指向和类型(完成前驱线索化),
    //同时通过改变前驱节点的右指向和类型(完成后继线索化),2步处理完更新前驱节点
    //线索化后再遍历会死龟!!!
    public void threadedNode(Ho node) {
        if (node == null) {
            return;
        }

        threadedNode(node.getLeft());
        if (pre !=  null&&pre.getRight()==null) {
            pre.setRight(node);
            pre.setRightType(1);
        }
        if (node.getLeft() == null) {
            node.setLeft(pre);
            node.setLeftType(1);
        }
        pre = node;

        threadedNode(node.getRight());
    }
    //中序线索化方法重载
    public void threadedNode() {
        this.threadedNode(root);
    }

    //线型方式遍历(非递归),实现线索化二叉树的中序遍历
    //逻辑:左子树上寻找线索化节点并打印,持续输出后继节点,没有后继节点节点就取右子树后找线索化节点-->循环
    //宏观上:就是寻找线索化节点和它的后继节点
    public void threadedList() {
        Ho node = root;
        while (node!=null) {
            //找到线索化节点并打印8,打印8
            //找到线索化节点并打印10,打印10
            //找到线索化节点并打印14,打印14
            while (node.getLeftType()==0) {
                node = node.getLeft();
            }
            System.out.println(node);
            //后继节点打印3
            //后继节点打印1
            //后继节点打印6
            while ( node.getRightType() == 1) {
                node = node.getRight();
                System.out.println(node);
            }
            //右子节点10
            //右子节点6
            //右子节点null,退出
            node = node.getRight();
        }
    }
}

//树节点
//编写三种遍历方式
class Ho {
    private int no;
    private String name;
    private Ho left;
    private Ho right;
    //新增指针类型,0表示子树,1表示前驱/后继节点,问:有啥用?答:遍历防止死龟
    private int leftType;
    private int rightType;

    public Ho(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 Ho getLeft() {
        return left;
    }

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

    public Ho getRight() {
        return right;
    }

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

    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;
    }

    @Override
    public String toString() {
        return "HoNode [no=" + no + ", name=" + name + "]";
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (left != null) {
            left.preOrder();
        }
        if (right != null) {
            right.preOrder();
        }
    }
    //中序遍历,线索化后的
//    public void infixOrder() {
//        if (left != null&& leftType==0) {
//            left.infixOrder();
//        }
//        System.out.println(this);
//        if (right != null&& rightType==0) {
//            right.infixOrder();
//        }
//    }
    //中序遍历
    public void infixOrder() {
        if (left != null) {
            left.infixOrder();
        }
        System.out.println(this);
        if (right != null) {
            right.infixOrder();
        }
    }
    //后序遍历
    public void postOrder() {
        if (left != null) {
            left.postOrder();
        }
        if (right != null) {
            right.postOrder();
        }
        System.out.println(this);
    }

    //前序查找
    //这里当前节点的查找结果毫无疑问若正确可以直接返回
    //子节点的查找结果必须先收集再判断,若正确直接返回,为空则在方法末尾返回
    //  =>如果不立即判断返回则当查找到第一个叶子节点直接必出结果,即路径上遇到结果就返回,到叶子都没遇到就返回null了
    //整体判断逻辑:当前节点能否返回(能否返回:找到正确结果才返回)-->左递归能否返回-->右递归直接返回-->补上方法返回值null
    //总结:以前序为例,无论是遍历还是查找,对于当前节点的操作其实只是左递归前的附带操作,
    //      因此从执行结果来看,整体是先从上往下执行,(中+左)=>(中+左)=>(中+左)=>叶子,然后开始从下层处理到上层
    public Ho preOrderSearch(int no) {

        if (this.no == no) {
            return this;
        }

        if (left != null) {
            if (left.preOrderSearch(no) != null) {
                return left.preOrderSearch(no);
            }
        }

        if (right != null) {
            return right.preOrderSearch(no);
        }

        return null;
    }
    //中序查找
    public Ho infixOrderSearch(int no) {

        if (left != null) {
            if (left.infixOrderSearch(no) != null) {
                return left.infixOrderSearch(no);
            }
        }
        if (this.no == no) {
            return this;
        }
        if (right != null) {
            if (right.infixOrderSearch(no) != null) {
                return right.infixOrderSearch(no);
            }
        }
        return null;
    }
    //后序查找
    public Ho postOrderSearch(int no) {
        Ho result = null;

        if (left != null) {
            result = left.postOrderSearch(no);
            if (result != null) {
                return result;
            }
        }
        if (right != null) {
            result = right.postOrderSearch(no);
            if (result != null) {
                return result;
            }
        }

        //统计比较多少次,以后序查找为例,
        //注意为了得到正确的比较次数,需要将递归结果先收集再做判断或返回,否则次数会翻倍
        System.out.println("一次");
        if (this.no == no) {
            return this;
        }
        return result;
    }

    //删除节点
    //核心:在当前节点判断是否删除子节点(因为单向二叉树只能操作自己的左右指针),注意根节点的判断在树类中
    //若删除了子节点,则直接返回结束方法(递归),否则调用递归删除
    public void delNode(int no) {
        if (this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        if (this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        if (this.left != null) {
            this.left.delNode(no);
        }
        if (this.right != null) {
            this.right.delNode(no);
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值