数据结构之二分搜索树(BST)

树是一种十分重要的数据结构,树的设计来源生活,生活中就处处可见(如下图),由于树的天然组织结构,在处理数据上可以达到高效,所以我们有必要学习一下树,本文主要介绍二分搜索树(binary search tree)。

在这里插入图片描述

要点

在这里插入图片描述

树科普

在这里插入图片描述

如上图:这是我们见得典型的树结构 --------二叉树。
二叉树的性质:
1 根节点 唯一
2 每个节点最多有两个孩子
3 每个节点最多只有一个父亲节点
叶子节点: 一个孩子也没有 (节点的左右孩子都为空)
二叉树也就有天然的递归结构:
每个节点的左孩子可以看成一个二叉树的根节点 成为左子树
每个节点的右孩子可以看成一个二叉树的根节点 成为右子树
满二叉树:对于每个节点来说,除了叶子节点外,都有两个孩子,所以二叉树不一定是满的。
ps:一个节点也可以看做二叉树 左右节点都为空,满足节点的定义就行。

二分搜索树

在这里插入图片描述

不同叫法:二叉排序树 二叉查找树,二叉搜索树
特点: 二分搜索树是一个二叉树,具备二叉树的所有性质。
二分搜索树的独特性质:
  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值。
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
二分搜索树优点:

大大加快了查询速度(给了数值就知道沿着那个方向查)
参考生活实例,图书馆寻找特定书。去那个方向类的阅览室找xxx就行了

二分搜索树缺点:

存储的元素要有可比较性,可以看作二分搜索树的局限性。

类的设计

1 、首先设计一个我们自己的二分搜索树类----BST,这个类中先进行节点以及重要成员、方法的设计。
public class BST<E extends Comparable<E>> {
    /**
     * 节点设计
     */
    private class Node {
        public E e;
        public Node left;
        public Node right;

        public Node(E e) {
            this.e = e;
            left = null;
            right = null;
        }
    }
     // 根节点的设计  用于标记根节点
    private Node root;// 根节点
    private int size;// 元素数目

    public BST() {
        root = null;
        size = 0;
    }

    public int getSize() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }
 }

设计缘由:
1 可以存储任意类型设计为 泛型
2 存储的对象要可比较性 故对泛型约束满足Comparable接口(参见二分搜索树的缺点)
使用参考:
1 Comparable 的使用
可以参考 Integer 类的compareTo比较源码
可以参考:文章1
2 super extends的使用区别
可以参考:文章2

2 插入(递归实现)

插入思路图
在这里插入图片描述

如上图我们插入元素时,从根节点开始逐个比较,比根节点小则向左子树比较,比根节点大则向右子树比较,一直往下比较直到目标节点的下一节点为空,便插入空的位置。

具体实现:
此处我们先搞个测试(后面上优化后的代码)

  /**
     * 向以node为根的二分搜索树中添加元素 e
     *
     * @param e    要插入的元素
     * @param node 根节点
     *             此方法我们自己使用 ,本方法使用递归实现。
     *             思路:采用递归思想,则树的根是不断变化的。
     *             <p>
     *             addTest 为测试方法  add 为addTest的优化版
     */
    private void addTest(Node node, E e) {
        //元素重复 直接返回
        if (e.equals(node.e)) {
            return;
        } else if (e.compareTo(node.e) < 0 && node.left == null) {
            node.left = new Node(e);
            size++;
            return;
        } else if (e.compareTo(node.e) > 0 && node.right == null) {
            node.right = new Node(e);
            size++;
            return;
        }
        // 开始递归调用  往下面遍历
        if (e.compareTo(node.e) < 0) {
            addTest(node.left, e);
        } else if (e.compareTo(node.e) > 0) {
            addTest(node.right, e);
        }
    }

我们设想,反正插入时,都是插入空的位置(目标节点的左孩子,或者右孩子为空)
所以:
1 树为空时 创建节点,吧此节点当做根节点
2 树不为空时 插入目标元素
3 递归实现

// 1 树为空时 创建节点,吧此节点当做根节点
 /* if (root == null) {
            root = new Node(e);//创建根节点,把元素插入到根节点
            size++;
        } else {
      //2  树不为空时 插入目标元素 
            add(root, e);
        }*/

        // 优化
        root = add(root, e);

  /**
     * 插入的递归优化 (参考addTest )
     */
    private Node add(Node node, E e) {
        // 为空时创建元素  这个空的就是要插入的位置
        if (node == null) {
            size++;
            return new Node(e);
        }
        // 3 递归调用 返回插入位置节点
        if (e.compareTo(node.e) < 0) {
            node.left = add(node.left, e);
        } else if (e.compareTo(node.e) > 0) {
            node.right = add(node.right, e);
        }
        return node;
    }
3 查找(递归实现)

查找树种是否包含此元素
思路:
1 树是空的不包含
2 从根节点开始元素比较,相等时就是此节点。
3 从根节点开始元素比较,小于时递归左找。
4 从根节点开始元素比较,大于时递归有找。

具体操作如下:


  /**
     * 是否包含元素
     *
     * @param e 目标元素
     */
    public boolean contain(E e) {
        return contain(root, e);
    }


    /**
     * @param node 以node为根节点的节点
     * @param e    目标元素
     */
    private boolean contain(Node node, E e) {
        // 树空
        if (node == null) {
            return false;
        }
        //此节点含有元素时
        if (e.compareTo(node.e) == 0) {
            return true;
        } else if (e.compareTo(node.e) < 0) {
            // 比此元素小 递归左面找
            return contain(node.left, e);
        } else {
            //(e.compareTo(node.e) > 0)
            // 比此元素大 递归右面找
            return contain(node.right, e);
        }
    
4 遍历

在这里插入图片描述

深度优先遍历的基本思想:
对每一个可能的分支路径深入到不能再深入为止,而且每个结点只能访问一次。深度优先遍历的非递归的通用做法是采用栈。要特别注意的是,二分搜索树的深度优先遍历比较特殊,可以细分为前序遍历、中序遍历、后序遍历。
前序遍历:先访问当前节点,再依次递归访问左右子树 ,访问到前面节点才继续
中序遍历:先递归访问左子树,再访问自身,再递归访问右子树,访问到中间节点才继续
后序遍历:先递归访问左右子树,再访问自身节点,访问到后面节点才继续
广度优先遍历:
深度优先遍历的基本思想:从上往下对每一层依次访问,在每一层中,从左往右(也可以从右往左)访问结点,访问完一层就进入下一层,直到没有结点可以访问为止。广度优先遍历的非递归的通用做法是采用队列。

前序遍历(递归实现)
 /**
     * 二分搜索树 --前序遍历
     */
    public void preOrder() {
        System.out.println("树中的元素:");
        preOrder(root);
    }

    /**
     * 前序遍历以node为根的二分搜索树
     **/
    private void preOrder(Node node) {
      /* if (node == null){
           return;
       }*/
        // 熟练递归后写法 不拘谨与定义
        if (node != null) {
            System.out.print(node.e + "  ");
            // 递归遍历
            preOrder(node.left);
            preOrder(node.right);
        }

        /*递归总结(通常):
         * 先写递归终止条件,在写递归组成逻辑。
         * */
    }
前序遍历(非递归实现)

思路图:
在这里插入图片描述

具体实现:

 /**
     * 二分搜索树非递归前序遍历
     * <p>
     * 前序:  根节点->左子树-->右子树
     */
    public void preOrderNR() {
        Stack<Node> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            Node currentElement = stack.pop();
            System.out.println(currentElement.e);
            if (currentElement.right != null) {
                stack.push(currentElement.right);
            }
            if (currentElement.left != null) {
                stack.push(currentElement.left);
            }
        }
    }
中序遍历

知道了前中后序遍历的区别就好写了,只是访问顺序不同

 /**
     * 中序遍历 递归实现
     */
    public void inOrder() {
        inOrder(root);
    }

    /**
     * 中序遍历
     *
     * @param node 以node为根节点的节点
     */
    private void inOrder(Node node) {
        if (node == null) {
            return;
        }
        inOrder(node.left);
        System.out.println(node.e);
        inOrder(node.right);
    }
后序遍历

知道了前中后序遍历的区别就好写了,只是访问顺序不同

public void postOrder() {
        postOrder(root);
    }

    private void postOrder(Node node) {
        if (node == null) {
            return;
        }
        postOrder(node.left);
        postOrder(node.right);
        System.out.println(node.e);
    }
广度优先遍历:

思路图:
在这里插入图片描述

具体实现:

   /**
     * 二分搜索树的广度优先遍历(使用队列实现)
     */
    public void levelOrder() {
        Queue<Node> queue = new LinkedList<Node>();
        queue.add(root);
        while (!queue.isEmpty()) {
            Node cur = queue.remove();
            System.out.println(cur.e);
            if (cur.left != null) {
                queue.add(cur.left);
            }
            if (cur.right != null) {
                queue.add(cur.right);
            }
        }

    }
5 元素删除

在这里插入图片描述

最值的查找删除:
    /**
     * 二分搜索树的最小元素  递归实现
     */
    public E minimum() {
        if (size == 0) {
            throw new IllegalArgumentException("binary search tree is empty");
        }
        return minimum(root).e;
    }

    private Node minimum(Node node) {
        if (node.left == null) {
            return node;
        }
        return minimum(node.left);
    }

    /**
     * 二分搜索树的最大元素
     */
    public E maxmum() {
        if (size == 0) {
            throw new IllegalArgumentException("binary search tree is empty");
        }
        return maxmum(root).e;
    }

    private Node maxmum(Node node) {
        if (node.right == null) {
            return node;
        }
        return maxmum(node.right);
    }


    /**
     * 删除二分搜索树的最小值
     */
    public E removeMin() {
        E rec = minimum();
        // 删除操作
        root = removeMin(root);
        return rec;
    }

    /**
     * 删除以node为根节点的二分搜索树的最小节点
     * 返回删除节点后新的二分搜索树的节点
     */
    private Node removeMin(Node node) {
        if (node.left == null) {
            Node rightNode = node.right;
            node.right = null;
            size--;
            return rightNode;
        }
        node.left = removeMin(node.left);
        return node;
    }

    /**
     * 删除二分搜索树的最大值
     */
    public E removeMax() {
        E rec = minimum();
        // 删除操作
        root = removeMax(root);
        return rec;
    }

    /**
     * 删除以node为根节点的二分搜索树的最大节点
     * 返回删除节点后新的二分搜索树的节点
     */
    private Node removeMax(Node node) {
        if (node.right == null) {
            Node leftNode = node.left;
            node.left = null;
            size--;
            return leftNode;
        }
        node.right = removeMin(node.right);
        return node;
    }
删除任意元素

在这里插入图片描述
思路
找到了删除节点后:
1 待删除的节点左子树为空
2 待删除节点的右子树为空
3 待删除节点左右子树都不为空

情况1,2时:为最值的删除,使用最值处理即可(参考最值的删除)
情况3时:找到以待删除节点为根节点的最小节点, 用这个节点顶替待删除节点。

具体实现:

 /**
     * 删除以e 为节点的元素
     */
    public void remove(E e) {
        root = remove(root, e);
    }

    /**
     * 删除以node为根的二分搜索树中 值为e的节点  递归算法
     * <p>
     * 返回删除节点后,新的二分搜索树的根
     */
    private Node remove(Node node, E e) {
        if (node == null) {
            return null;
        }
        // 递归寻找
        if (e.compareTo(node.e) < 0) {
            node.left = remove(node.left, e);
            return node;
        } else if (e.compareTo(node.e) > 0) {
            node.right = remove(node.right, e);
            return node;
        } else {
             /*e.compareTo(node.e)==0
              找到了删除节点
              三种情况:
                  1 待删除的节点左子树为空
                  2  待删除节点的右子树为空
                  3 待删除节点左右子树都不为空
              */
             if (node.left==null){
                 Node rightNode= node.right;
                 node.right = null;
                 size--;
                 return rightNode;
             }
            if (node.right==null){
                Node leftNode= node.left;
                node.left = null;
                size--;
                return leftNode;
            }
            /*
            * 待删除节点的左右孩子都不为空时思路:
            * 1 找到以待删除节点为根节点的最小节点
            * 2 用这个节点顶替待删除节点
            * */
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;

            node.left=node.right=null;//call gc
            return successor;
            /*
            本栗子:找的后继:以删除节点右子树为根节点找最小值
                     前驱:以删除节点左子树为根节点找最大值
            *
            * */
        }

    }

小结

至此有关二分搜索树的相关操作就简单的搞了一遍,简单的看了看,自己写了400多行的BST代码,蛮有成就感哈。本文的图片、截图有的来源于自己所画,有的来源于慕课网的学习视屏(我直接省事截的图哈哈)
源码下载

The end

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值