十-1, Java实现简单二叉树(遍历查找和删除)

10.1 树存在的必要性

  1. [数组存储方式的优缺点]
  • 优点: 通过下标直接访问目标数据, 速度快. 对于有序数组, 还可使用二分查找提高检索速度
  • 缺点: 如果要检索具体某个值, 或者插入值(按一定顺序)会整体移动, 效率较低.
  1. [链表存储方式的优缺点]
  • 优点:在一定程度上对数组存储方式有所优化。比如插入一个数值节点,只需要将插入节点链接到链表中即可,其删除效率也较高
  • 缺点:在进行检索时,效率仍然较低。比如检索某个值, 需要从头节点开始遍历。

从上面数组和链表的特点我们可以知道: 数组查找快, 删除插入慢; 链表查找慢,删除插入快, 在数组和链表中, 查找和增删不可兼得.

  1. [树存储的特点]
  • 而树这种数据结构, 在查找速度快的同时又兼顾了增删的效率,可以说是结合了数组和链表的优点
  • 树存储方式的核心特点为:能提高数据存储、读取的效率。比如利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入、删除、修改的速度。

10.2 树的常用术语

树这种数据结构有较多的常用术语,这些术语我们需要熟练记住。

  • 这些常用术语包括:
  1. 节点
  2. 根节点
  3. 父节点
  4. 子节点
  5. 叶子节点(没有子节点的节点)
  6. 节点的权(节点值)
  7. 路径(从 root 节点找到该节点的路线)
  8. 子树
  9. 树的高度(最大层数)
  10. 森林(多颗子树构成森林)

10.3 二叉树的定义

二叉树

  • 树有很多种, 每个节点最多只能有两棵子树, 而且子树有左右顺序之分, 这树叫二叉树.

满二叉树

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

完全二叉树

  • 二叉树满足以下特点
    1. 只有最下面两层结点度<2
    2. 并且最下一层结点连续集中在靠左的若干位置上
  • 这样的二叉树叫做完全二叉树.

每一层都是要按从左往右,依此填满,最后一层可以不满, 如下图.

注意: 满二叉树一定是完全二叉树, 完全二叉树不一定是满二叉树.


下面几节, 关于二叉树的操作,都是对如下图所示的二叉树进行的:

10.5 二叉树的基本操作(手动添加结点)

  • 程序的组成:
    [结点类]: 左右结点域, 数据, 构造方法, toString(), 左右节点的添加方法
    [二叉树操作类]: 构造方法(指定二叉树的根节点), 测试方法(手动添加每个父节点的左右结点)
  1. 结点内部类: 包含左结点,右结点指针域, 数据域, get和set方法, 构造方法(初始化结点数据域), toString()(自定义输出数据域)
  • 因为本栗子追求最简单的演示, 所有删去了toString(), 数据域的get,set方法等未用到的方法,在之后的复杂栗子中, 会做进一步的展开.
public static class BinaryTreeNode{
        //二叉树结点的内容: 数据域, 左右结点指针域
        private BinaryTreeNode leftNode;
        private BinaryTreeNode rightNode;
        private int id;
        private String name;
        
        //设置结点的左孩子节点
        public void setLeftNode(BinaryTreeNode leftNode) {
            this.leftNode = leftNode;
        }
        
        //设置结点的右孩子节点
        public void setRightNode(BinaryTreeNode rightNode) {
            this.rightNode = rightNode;
        }

        //构造方法, 初始化结点的数据域
        public BinaryTreeNode(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }
  1. 二叉树操作类和测试方法: 包含 二叉树的构造方法(指定二叉树的根节点), 测试方法
//对结点的操纵
    ///添加新节点到二叉树
    private BinaryTreeNode root;
    public BasicBinaryTree(BinaryTreeNode node){
        this.root = node; //指定根节点
    }

    public static void main(String[] args) {
        //创建若干个结点
        BinaryTreeNode node1 = new BinaryTreeNode(1, "宋江");
        BinaryTreeNode node2 = new BinaryTreeNode(2, "吴用");
        BinaryTreeNode node3 = new BinaryTreeNode(3, "卢俊义");
        BinaryTreeNode node4 = new BinaryTreeNode(4, "公孙胜");
        BinaryTreeNode node5 = new BinaryTreeNode(5, "关胜");

        //创建二叉树, 并指定根节点
        BasicBinaryTree tree = new BasicBinaryTree(node1);

        //手动添加结点
        node1.setLeftNode(node2);
        node1.setRightNode(node3);
        node3.setLeftNode(node4);
        node3.setRightNode(node5);
    }

运行结果(通过DEBUG展示)

10.4 二叉树的遍历(前序, 中序, 后序)

【案例需求】

使用前序、中序、后序分别遍历二叉树。

【二叉树三种遍历的定义】

  • 前序遍历
    • 先遍历根节点, 再遍历左子树, 最后遍历右子树;
  • 中序遍历
    • 先遍历左子树, 再遍历根节点, 最后遍历右子树;
  • 后序遍历
    • 先遍历左子树, 再遍历右子树, 最后遍历根节点;

根据这三种遍历顺序的定义,我们可以得知两个结论:

  1. 左子树总是在右子树的前面遍历;
  2. 前、中、后的遍历顺序指的是遍历根节点的顺序。

【实现思路】

上面说过,二叉树的遍历包括:前序遍历、中序遍历、后序遍历。

其中前序遍历的实现思路如下:

  1. 首先输出当前节点;
  2. 如果左子节点不为空,就对左子节点递归前序遍历;
  3. 如果右子节点不为空,就对右子节点递归前序遍历。

中序遍历的实现思路如下:

  1. 首先判断左子节点是否为空,如果不为空,就对左子节点递归中序遍历;
  2. 然后输出当前节点;
  3. 最后判断右子节点是否为空,如果不为空,就对右子节点递归中序遍历。

后序遍历的实现思路如下:

  1. 首先判断左子节点是否为空,如果不为空,就对左子节点递归后序遍历;
  2. 然后判断右子节点是否为空,如果不为空,就对右子节点递归后序遍历;
  3. 最后输出当前节点。

【代码实现】

  • 程序结构:
    • 结点类(TreeNode):
      1. 左右指针域
      2. 设置父结点的左右孩子结点的set方法
      3. 初始化结点数据的构造方法
      4. 输出结点信息的toString方法
      5. 具体实现对结点的前中后序遍历
    • 二叉树遍历类(BinaryTreeTraverse)
      1. 设置父节点root的构造方法
      2. 使用结点类的前中后序遍历方法
      3. 测试方法

结点类具体实现:

public class BinaryTreeNode {
    /**
     * 二叉树结点类
     * 1. 左右节点指针域
     * 2. 结点相关的数据域
     * 3. 设置结点的左右节点的set方法
     * 4. 初始化结点数据的构造方法
     * 5. 输出结点数据的toString()
     */
    private BinaryTreeNode leftNode;
    private BinaryTreeNode rightNode;
    private int id;
    private String name;
    //带参构造, 初始化结点的数据
    public BinaryTreeNode(int id, String name){
        this.id = id;
        this.name = name;
    }
    // 设置结点的左孩子结点
    public void setLeftNode(BinaryTreeNode node){
        this.leftNode = node;
    }
    //设置结点的右孩子节点
    public void setRightNode(BinaryTreeNode node){
        this.rightNode = node;
    }
    //重写输出结点数据信息的toString()
    public String toString(){
        return "id : "+id+", name: "+name;
    }
    /遍历二叉树, 我们在结点类中作具体实现
    //1. 前序遍历
    public void preOrder(){
        //输出父节点
        System.out.println(this);
        //遍历输出左子树
        if(this.leftNode != null){
            this.leftNode.preOrder();
        }
        //遍历输出右子树
        if(this.rightNode != null){
            this.rightNode.preOrder();
        }
    }
    //2. 中序遍历
    public void midOrder(){
        //遍历左子树
        if( this.leftNode != null){
            this.leftNode.midOrder();
        }
        //输出父节点
        System.out.println(this);
        //遍历右子树
        if(this.rightNode != null){
            this.rightNode.midOrder();
        }
    }
    //3. 后序遍历
    public void postOrder(){
        //遍历左子树
        if( this.leftNode != null){
            this.leftNode.postOrder();
        }
        //遍历右子树
        if( this.rightNode != null){
            this.rightNode.postOrder();
        }
        System.out.println(this);
    }
}

二叉树遍历类具体实现

public class BinaryTreeTraverse {
    /**
     * 二叉树的遍历
     * 1. 借助构造方法指定二叉树的根节点
     * 2. 使用结点类提供的遍历方法进行遍历
     */
    private BinaryTreeNode root;

    public BinaryTreeTraverse(BinaryTreeNode node) {
        this.root = node;
    }

    //通过判断父节点是否为空, 我们来决定二叉树遍历的开始和结束
    public void preOrderTraverse(){
        if(root != null){
            root.preOrder();
        }else{
            System.out.println("二叉树为空, 前序遍历失败!");
        }
    }
    public void midOrderTraverse(){
        if(root != null){
            root.midOrder();
        }else{
            System.out.println("二叉树为空, 中序遍历失败!");
        }
    }
    public void postOrderTraverse(){
        if(root != null){
            root.postOrder();
        }else{
            System.out.println("二叉树为空, 后序遍历失败");
        }
    }

    //测试方法(单独做成一个测试类也是可行的)
    public static void main(String[] args) {
        //建立二叉树结点
        BinaryTreeNode node1 = new BinaryTreeNode(1, "宋江");
        BinaryTreeNode node2 = new BinaryTreeNode(2, "吴用");
        BinaryTreeNode node3 = new BinaryTreeNode(3, "卢俊义");
        BinaryTreeNode node4 = new BinaryTreeNode(4, "公孙胜");
        BinaryTreeNode node5 = new BinaryTreeNode(5, "关俊");
        //新建一棵二叉树
        BinaryTreeTraverse tree = new BinaryTreeTraverse(node1);
        //结点添加到二叉树
        node1.setLeftNode(node2);
        node1.setRightNode(node3);
        node3.setLeftNode(node4);
        node3.setRightNode(node5);

        //二叉树的遍历
        System.out.println("前序遍历二叉树的结果为: ");
        tree.preOrderTraverse();
        System.out.println("=========================");

        System.out.println("中序遍历二叉树的结果为: ");
        tree.midOrderTraverse();
        System.out.println("=========================");

        System.out.println("后序遍历二叉树的结果为: ");
        tree.postOrderTraverse();
        System.out.println("=========================");
    }

10.5 二叉树的查找

【案例需求】

分别使用前序, 中序, 后序查找二叉树中指定编号节点.

【思路分析】

前序遍历查找的思路如下

  1. 首先判断当前节点(this)的编号是否等于目标编号,如果相等, 则直接返回当前节点;
  2. 如果不相等, 再判断左子节点是否为空, 如果不为空, 则递归前序查找左子树;
  3. 如果2中左子树递归前序查找的结果不为空, 说明找到了, 返回即可;
  4. 如果没有找到, 则判断当前节点的右子树是否为空, 如果不为空, 就递归前序查找右子树, 把结果存放到res中;
  5. 返回查询结果res;

核心代码:

  1. 结点类中具体实现的查找方法(这里以中序查找为例)
public BinaryTreeNodeSe midOrderSearchById(int id) {
        //找到了, 直接返回
        if (this.id == id) return this;
        //左子树非空, 递归左子树查询
        BinaryTreeNodeSe res = null;
        if (this.leftNode != null) {
            res = this.leftNode.midOrderSearchById(id);
        }
        //左子树查到了(res不为空), 直接返回即可, 下面的递归右子树直接略过
        if (res != null) return res;

        if (this.rightNode != null) {
            res = this.rightNode.midOrderSearchById(id);
        }
        return res;
    }
  1. 二叉树类中使用结点类查找方法的方法(仍以中序查找为例)
// 2. 中序遍历查找
    public void midOrderSe(int id){
        //第一层判断, 二叉树是否为空
        if( root != null){
            BinaryTreeNodeSe res  = root.midOrderSearchById(id);
            // 第二层判断, 是否查到对应id 的结点
            if(res != null){
                System.out.println("找到了id= "+id+"的二叉树结点, 具体的信息如下: ");
                System.out.println(res);
            }else{
                System.out.println("二叉树上没有id="+id+"的结点");
            }
        }else{
            System.out.println("二叉树为空, 查找失败! ");
        }
    }

Java实现二叉树的前中后序查找完整代码实例

10.5 二叉树的删除

【案例需求】

根据节点编号删除指定节点:

如果节点为叶子节点,直接删除;
如果节点为父节点,则把此节点和节点的整个子树都删除。
【思路分析】

除了二叉树的根节点之外,其余的节点都是有父节点的。由于二叉树的每个节点关系都是单向的,即每个节点记录的都是自己的左、右子节点的信息,所以是无法通过目标节点自己来删除自己的,而是需要借助目标节点的父节点来删除目标节点。

因此二叉树的节点删除思路如下:

  1. 如果二叉树的根节点就是目标节点,那么直接将根节点置空即可;
  2. 否则,如果当前根节点的左子节点不为空且是目标节点,就将左子节点置空 this.left = null 并返回;
  3. 如果第 2 步没删除,若当前根节点的右子节点不为空且是目标节点,就将右子节点置空 this.right = null 并返回;
  4. 如果第 3 步没删除,就遍历左子树递归查找删除;
  5. 如果第 4 步也没删除,就遍历右子树递归查找删除;

【代码实现】

  1. 结点类中具体实现的结点删除方法
//二叉树结点的删除
    public void delNodeById(int id){
        //根节点的删除交给二叉树类完成
        //1. 如果左子树非空,并且找到了id相同的结点, 删除并返回
        if(this.leftNode != null && this.leftNode.id == id){
            this.leftNode = null;
            return;
        }
        //2. 如果1未得到执行, 那么弱右子树非空, 并且找到了id相同的结点, 删除并返回
        if(this.rightNode != null && this.rightNode.id == id) {
            this.rightNode = null;
            return;
        }
        //3. 如果1,2均为获得执行, 而左子树又非空的话, 递归遍历左子树
        if(this.leftNode != null)
            this.leftNode.delNodeById(id);

        //3. 如果1,2均为获得执行, 并且3也没有找到合适的结点
        // 若右子树又非空的话, 递归遍历右子树
        if(this.rightNode != null)
            this.rightNode.delNodeById(id);
    }
  1. 二叉树类中使用结点类中删除方法的方法
//结点删除的方法
    public void nodeDelById(int id){
        
        if( root != null){
            if(root.id == id){
                root = null;
                System.out.println("id = "+id+"的结点已删除");
            }else{
                root.delNodeById(id);
                System.out.println("id = "+id+"的结点已删除");

            }
           
        }else{
            System.out.println("二叉树已经为空, 查找失败");
        }
    }

Java实现二叉树结点删除的完整代码示例

posted @ 2022-05-26 20:31  青松城  阅读(152)  评论(0编辑  收藏  举报