Java算法

二叉排序树

Posted by Chen Xingxu on April 26, 2020

Java 算法之二叉排序树

二叉排序树(BST)的定义

二叉排序树或者是空树,或者是满足以下性质的二叉树:

  1. 若它的左子树不空,则左子树上所有关键字的值均小于根关键字的值。
  2. 若它的右子树不空,则右子树上所有关键字的值均大于根关键字的值。
  3. 左右子树又各是一棵二叉排序树。

说明:由二叉排序树的定义可以知道,如果输出二叉排序树的中序遍历序列,则这个序列是递增有序的。

二叉排序树的存储结构

二叉排序树通常采用二叉链表进行存储,其结点类型定义与一般的二叉树类似。

二叉树结点的声明:

static final class Entry<T extends Comparable<T>>{
        //保存的数据
        private T item;
        //左子树
        private Entry<T> left;
        //右子树
        private Entry<T> right;
        //父节点
        private Entry<T> parent;
        Entry(T item,Entry<T> parent){
            this.item = item;
            this.parent = parent;
        }
    }

类属性:

 //根节点
 private Entry<T> root;
 //数据量
 private int size = 0;

二叉排序树的基本算法

  1. 查找关键字的算法

    显然,要找的关键字要么在左子树上,要么在右子树上,要么在根结点上。由二叉排序树的定义可以知道,根结点中的关键字将所有关键字分成了两部分,即大于根结点中关键字的部分和小于根结点中关键字的部分。可以将待查关键字先和根结点中的关键字比较,如果相等则查找成功;如果小于则到左子树中去查找,无须考虑右子树中的关键字;如果大于则到右子树中去查找,无须考虑左子树中的关键字。如果来到当前树的子树根,则重复上述过程;如果来到了结点的空指针域,则说明查找失败。可以看出这个过程和折半查找法十分相似,实际上折半查找法的判定树就是一棵二叉排序树。

    /**
         * 判断是否存在该元素
         * @param item 查找元素
         * @return 结果
         */
        public boolean contains(T item){
            return getEntry(item) != null;
        }
        /**
         * 获取节点节点
         * @param item 获取节点
         * @return 获取到的节点,可能为null
         */
        private Entry<T> getEntry(T item){
            Entry<T> t = root;
            int ret;
            //从根节点开始遍历
            for (;t != null;){
                ret = item.compareTo(t.item);
                if (ret < 0)
                    t = t.left;
                else if (ret > 0)
                    t = t.right;
                else
                    return t;
            }
            return null;
        }
    
  2. 插入关键字的算法

    二叉排序树是一个查找表,插入一个关键字首先要找到插入位置。对于一个不存在于二叉排序树中的关键字,其查找不成功的位置即为该关键字的插入位置。

    /**
         * 添加元素
         * @param item 待添加元素
         * @return 已添加元素
         */
        public T put(T item){
            //每次添加数据的时候都是从根节点向下遍历
            Entry<T> t = root;
      if (t == null){
                //当前的叉树树的为空,将新节点设置为根节点,父节点为null
                root = new Entry<>(item,null);
           size++; 
                return root.item;
            }
            //自然排序结果,如果传入的数据小于当前节点返回-1,大于当前节点返回1,否则返回0
            int ret = 0;
            //记录父节点
            Entry<T> p = t;
            while (t != null){
                //与当前节点比较
                ret = item.compareTo(t.item);
                p = t;
                //插入节点比当前节点小,把当前节点设置为左子节点,然后与左子比较,以此类推找到合适的位置
                if (ret < 0)
                    t = t.left;
                //大于当前节点
                else if (ret > 0)
                    t = t.right;
                else {
                    //相等就把旧值覆盖掉
                    t.item = item;
                    return t.item;
                }
            }
            //创建新节点
            Entry<T> e = new Entry<>(item,p);
            //根据比较结果将新节点放入合适的位置
            if (ret < 0)
                p.left = e;
            else
                p.right = e;
         size++;
            return e.item;
        }
    
  3. 删除关键字的操作

    挡在二叉排序树中删除一个关键字时,不能把以该关键字所在的结点为根的子树都删除,而是只删除这一个结点,并保持二叉排序树的特性。

    假设在二叉排序树上被删除的结点为 p,f 为其双亲结点,则删除结点 p 的过程分为以下 3 种情况。

    1) p 结点为叶子结点。由于删除叶子结点后不会破坏二叉排序树的特性,因此直接删除即可。

    2) p 结点只有右子树而无左子树,或者只有左子树而无右子树。将删除结点的父结点的左结点(右结点)指向删除结点的子结点,将左结点(右结点)指向删除结点的父节点。

    3) p 结点既有左子树又有右子树。找到删除节点的前驱节点或后继节点,然后将前驱或后继节点的值赋给删除节点,然后将前驱或后继节点删除。本质是删除前驱或后继节点。

    前驱节点的特点:

    ​ 1)删除的左子节点没有右子节点,那么左子节点即为前驱节点;

    ​ 2)删除节点的左子节点有右子节点,那么最右边的最后一个节点即为前驱节点。

    后继节点的特点:

    ​ 与前驱节点刚好相反,总是右子节点,或则右子节点的最左子节点;例如:删除节点为 c ,那么前驱节点为 m,后继节点为 n。

/**
     * 删除元素
     * @param item 删除元素
     * @return 删除结果
     */
    public boolean remove(T item){
        //获取删除节点
        Entry<T> delEntry = getEntry(item);
        if (delEntry == null) return false;
        //删除节点的父节点
        Entry<T> p = delEntry.parent;
        size--;
        //情况1:没有子节点
        if (delEntry.left == null && delEntry.right == null){
            //删除节点为根节点
            if (delEntry == root){
                root = null;
            }else {//非根节点
                //删除的是父节点的左节点
                if (delEntry == p.left){
                    p.left = null;
                }else {//删除右节点
                    p.right = null;
                }
            }
            //情况2:删除的节点只有左节点
        }else if (delEntry.right == null){
            Entry<T> lc = delEntry.left;
            //删除的节点为根节点,将删除节点的左节点设置成根节点
            if (p == null) {
                lc.parent = null;
                root = lc;
            } else {//非根节点
                if (delEntry == p.left){//删除左节点
                    p.left = lc;
                }else {//删除右节点
                    p.right = lc;
                }
                lc.parent = p;
            }
            //情况3:删除节点只有右节点
        }else if (delEntry.left == null){
            Entry<T> rc = delEntry.right;
            //删除根节点
            if (p == null) {
                rc.parent = null;
                root = rc;
            }else {//删除非根节点
                if (delEntry == p.left)
                    p.left = rc;
                else
                    p.right = rc;
                rc.parent = p;
            }
            //情况4:删除节点有两个子节点
        }else {//有两个节点,找到后继节点,将值赋给删除节点,然后将后继节点删除掉即可
            Entry<T> successor = successor(delEntry);//获取到后继节点
            delEntry.item = successor.item;
            //后继节点为右子节点
            if (delEntry.right == successor){
                //右子节点有右子节点
                if (successor.right != null) {
                    delEntry.right = successor.right;
                    successor.right.parent = delEntry;
                }else {//右子节点没有子节点
                    delEntry.right = null;
                }
            }else {//后继节点必定是左节点
                successor.parent.left = null;
            }
            return true;
        }
        //让gc回收
        delEntry.parent = null;
        delEntry.left = null;
        delEntry.right = null;
        return true;
    }

/**
     * 获取节点节点
     * @param item 获取节点
     * @return 获取到的节点,可能为null
     */
    private Entry<T> getEntry(T item){
        Entry<T> t = root;
        int ret;
        //从根节点开始遍历
        for (;t != null;){
            ret = item.compareTo(t.item);
            if (ret < 0)
                t = t.left;
            else if (ret > 0)
                t = t.right;
            else
                return t;
        }
        return null;
    }
    /**
     * 查找后继节点
     * @param delEntry 删除节点
     * @return 后继节点
     */
    private Entry<T> successor(Entry<T> delEntry) {
        Entry<T> r = delEntry.right;//r节点必定不为空
        while (r.left != null){
            r = r.left;
        }
        return r;
    }