Java二叉搜索树

二叉搜索树(BST) = 二叉 + 搜索 + 树

二叉树模型

下面是一个简易的树杈子模型
在这里插入图片描述

可以看到,每个树杈最多有俩分支,还有的树杈没有分支,即每个树杈最多有不超过两个分支

把这样的树杈子结构倒过来,去掉树根,就变成了一种数据结构——二叉树,like下面这样↓
在这里插入图片描述

根据这种结构我们引出下面这些概念:

节点类:

  1. 分支与分支的交点称为树节点
  2. 没有分支相交的节点称为叶子节点
  3. 最顶上的节点称为根节点
  4. 上下相邻的节点称为父子节点(上面的是爹,下面的是儿子)
  5. 父节点的父节点称为祖先节点
  6. 有同一个爹的称为兄弟节点

子树类:

  1. 一个爹的左儿子,称为该爹的左子树
  2. 一个爹的右儿子,称为该爹的右子树

下图把各节点用圆圈表示出来,并注明了它在不同参考对象下,与其他节点的关系
在这里插入图片描述

二叉搜索树

二叉搜索树就是在二叉树的基础上,添加了可以搜索的功能

对于任意一个父节点 father,和它的左子节点 leftSon,右子节点 rightSon,左子树 leftTree,右子树 rightTree
如果满足 leftSon < father < rightSonmax( leftTree ) < father < min( rightTree ) 这俩条件
就称这颗二叉树为二叉搜索树

在这里插入图片描述
上面这棵树就是一颗二叉搜索树

扫描二维码关注公众号,回复: 13616470 查看本文章

他满足第一条:任意节点的 左节点值都小于它,右节点值都大于它
也满足第二条:任意节点的 左子树里的最大节点值小于它,右子树里的最小节点值大于它

其实二叉搜索树还有一个直观的特点,将每个节点向下投影,你会发现他们从左至右是升序的
在这里插入图片描述

二叉搜索树的用途

这种有序性造就了二叉搜索树的用途,如果每次搜索都从根节点出发,拿目标值跟当前节点比较
如果比当前节点小,往左边搜索
如果比当前节点大,往右边搜索
如果和当前节点相等,找到了!

这种搜索方式,不管往哪边走,最好的情况都是能每比较一次,就可以筛掉一半不符合条件的元素,相比线性搜索快很多

有利就有弊,二叉搜索树提高了搜索的速度,但是需要维护增加的元素,它不能像线性结构一样每次追加到头尾就行,他必须要保证新来的元素,要跟旧元素 “合群”。因此,它新增元素时需要花费更久的时间

二叉搜索树的实现

以下示例按照 K-V 键值对的方式构建二叉搜索树

1. 声明节点类

根据图示,我们的节点类应该有三部分,数据 + 左子节点 + 右子节点,和双向链表很像

public class BinarySearchTree<K extends Comparable<K>, V> {
    
    

    private static class Node<K extends Comparable<K>, V>{
    
    
        K key;
        V value;

        Node<K, V> left;
        Node<K, V> right;

        Node(K key, V value) {
    
    
            this.key = key;
            this.value = value;
        }
    }
}

这里要求 key 必须实现Comparable接口,因为要保证节点是可比较的(有序性)

2. 搭建BST结构

一颗二叉搜索树,它的入口节点就是根节点,每次搜索都要从根节点开始,所以我们将其作为属性保存到外部类中

public class BinarySearchTree<K extends Comparable<K>, V> {
    
    

    private static class Node<K extends Comparable<K>, V>{
    
    
        K key;
        V value;

        Node<K, V> left;
        Node<K, V> right;

        Node(K key, V value) {
    
    
            this.key = key;
            this.value = value;
        }
    }
    
    private Node<K, V> root;
    
}

3. 增加一个节点

增加节点时同样需要从根节点入手,通过上面所述的两条要求,逐步向下找到合适的位置,将新节点安放过去

他满足第一条:任意节点的 左节点值都小于它,右节点值都大于它
也满足第二条:任意节点的 左子树里的最大节点值小于它,右子树里的最小节点值大于它

    /**
     * @return 如果 BST 中已存在给定的参数 key,则添加失败,返回 false
     */
    public boolean put(K key, V value){
    
    
        if(key == null){
    
    
            throw new NullPointerException("传入的 key 不能为 null");
        }
        if(root == null){
    
    
            root = new Node<>(key, value);
            return true;
        }
        //临时变量记录父节点,为后续连接新叶子节点做备份跟进
        Node<K, V> parent = null;
        //用于迭代的当前节点
        Node<K, V> current = root;
        while (current != null) {
    
    
            if(current.key.compareTo(key) > 0){
    
    
                //如果当前节点的key大于给定的key,往左走
                parent = current;
                current = current.left;
            }else if(current.key.compareTo(key) < 0){
    
    
                //往右走
                parent = current;
                current = current.right;
            }else{
    
    
                //BST已有该key
                return false;
            }
        }
        //能正常退出循环,说明current已经为null,新节点作为叶子节点追加到BST的末端
        //但是要追加到parent的left,还是right,还需要进一步判断
        if(parent.key.compareTo(key) > 0){
    
    
            //parent大于新节点,新节点添加到左边
            parent.left = new Node<>(key, value);
        }else{
    
    
            parent.right = new Node<>(key, value);
        }
        return true;
    }

4. 查找节点值

按key查找节点,本质就是对二叉搜索树有目标的遍历,还是按照相同的操作,让目标key(aimKey)和当前key(curKey)作比较

  1. 如果 curKey > aimKey,向左查找
  2. 如果 curKey < aimKey,向右查找
  3. 如果 curKey = aimKey,找到了
  4. 如果一直遍历到叶子节点都没能找到,返回null
	//从 root 入手
    public V get(K key) {
    
    
        return get(root, key);
    }
    private V get(Node<K, V> cur, K aimKey) {
    
    
        //遍历到尾,依然没找到 aimKey
        if(cur == null){
    
    
            return null;
        }
        if(cur.key.compareTo(aimKey) > 0){
    
    
            //curKey > aimKey,往左走
            return get(cur.left, aimKey);
        }else if(cur.key.compareTo(aimKey) < 0){
    
    
            //curKey < aimKey,往右走
            return get(cur.right, aimKey);
        }else{
    
    
            //curKey = aimKey,找到了
            return cur.value;
        }
    }

5. 删除节点

删除节点相对繁琐,如果是删除末端节点(叶子节点)还好,如果是非叶子节点,还要考虑被删节点之后的节点怎么办

在这里,我们还需要引出另外两个概念

  • 前驱节点:对于节点A,它左子树中最大的那个节点B,B是A的前驱(但是B依然小于A)
  • 后继节点:对于节点A,它右子树中最小的那个节点C,C是A的后继(但是C依然大于A)

在这里插入图片描述

无论前驱还是后继,他最多只有一个子节点

知道了前驱、后继有啥用

如果删除掉125,好像很轻松,直接令 150.left = null 就OK了,那是因为125没有后代节点(叶子节点)

如果删除150呢?我们需要保留150的后代节点,150只有一个直接的儿子,我们把150的儿子交给100保管就行了 100.right = 125

最复杂的来了,如果删除200呢?到底让100加冕成新root,还是报送300?可是二者都已经有了原来组建好的家庭,强拆会带来混乱

这时前驱、后继就是双方家庭选出的代表,他们是旧 root 的“左膀右臂”,他们的值最挨着 root 的值!这也就意味着,将前驱或后继提升为新root,对整个BST影响最小

妙哉~~
在这里插入图片描述

	public V remove(K key) {
    
    
        if(root == null || key == null){
    
    
            return null;
        }
        //扫描目标
        Node<K, V> parent = null;
        Node<K, V> current = root;
        while(current != null){
    
    
            if(current.key.compareTo(key) > 0){
    
    
                parent = current;
                current = current.left;
            }else if(current.key.compareTo(key) < 0) {
    
    
                parent = current;
                current = current.right;
            }else{
    
    
                //找到了!current就指向待删除的节点
                break;
            }
        }
        //如果不是break出来的
        if(current == null) return null;

        //current有两个直接儿子,把前驱或后继的值赋给当前节点,再删除前驱或后继
        if(current.left != null && current.right != null){
    
    
            //备份目标节点
            Node<K, V> aim = current;
            //找前驱(从左子树开始,一直往右走),记得时时更新parent
            parent = current;
            current = current.left;
            while(current.right != null){
    
    
                parent = current;
                current = current.right;
            }
            //current已经指向了前驱
            aim.key = current.key;
            aim.value = current.value;
        }
        //走到这,current所指向的节点,最多只有一个子节点,把current删除
        if(current == root){
    
    
            root = root.left != null ? root.left : root.right;
        }
        //判断current是parent的左儿子还是右儿子
        else if(current == parent.left){
    
    
            //如果current有左儿子,就让parent.left指向左儿子,否则指向右儿子
            parent.left = current.left != null ? current.left : current.right;
        }else{
    
    
            parent.right = current.left != null ? current.left : current.right;
        }
        return current.value;
    }

猜你喜欢

转载自blog.csdn.net/qq_44676331/article/details/113680511
今日推荐