二叉搜索树的删除操作可以交换吗_JavaScript数据结构 — 二叉搜索树(BST)ES6实现...

9b724df40d9d03f8bd9b47f8e93c9114.png

1. 概述

最基本的数据结构是向量和链表,为了将二者的优势结合起来,我们引入了二叉树,可以认为二叉树是列表在维度上的拓展。而今天要介绍的二叉搜索树(BST)则是在形式上借鉴了二叉树,同时也巧妙借鉴了有序向量的优势和特点。


2. 关键概念

2.1. 寻关键码访问(call-by-key)

关键码:

大小可比较

相等可比对

2.2. 词条(entry)

形式:<key , value>

节点-词条-关键码

每一个节点对应一个词条,每个词条对应一个关键码


3. 特点

微观上处处满足顺序性,宏观上整体满足单调性

顺序性:左子树中所有节点必须不大于根节点,右子树中所有节点必须不小于根节点,

单调性:BST的中序遍历(垂直投影)序列必然单调非降

bd5bbe58d73b22001f8b2236f7dc5523.png

4. 实现

继承自二叉树(binTree),具体实现可看我的上篇文章:

田浩:JavaScript数据结构 —— 二叉树ES6实现​zhuanlan.zhihu.com
56d6b445e3cca6a4aafa350836352f69.png

接口:

  • 查找search
  • 插入insert
  • 删除remove

实现:

class BST extends BinTree {
    constructor(rootNode) {
        super(rootNode);
        // 记忆热点,总是指向命中节点(命中节点可能是null,即假想的哨兵节点)的父亲
        this._hot = null;
    }

    search(xNdode, e) {
    }

}

4.1 查找

思路:减而治之,逐步比较,缩小查找范围,直至最终达到平凡的情况。可以看做在效仿有序向量二分查找。

实现:递归,这尾递归实际上很容易借助栈结构变为迭代形式。由递归改为迭代,时间复杂度相同,但是递归调用会执行包含通用的逻辑,每一帧的复杂度较高,所以迭代形式在实际中更高效。这里有必要对其进行改写。内部设置hot变量,为了记录查找到的节点的父亲节点,如果查找失败,我们可以假象存在这样一个节点,_hot依然指向假象节点的父亲节点,这一点很重要,将会为后边的插入算法提供极大地便利

代码:

    // 以xNode为根的子树中,查找特定的关键码e
    search(e, xNode = this._root) {
        // 将hot置为null,以防上次查找结果的干扰
        this._hot = null;
        return this._searchIn(e, xNode);
    }

    // 以xNode为根的子树中,查找特定的关键码e
    // 时间复杂度为O(h),h为树的高度
    _searchIn(e, xNode) {
        // 递归基
        if (!xNode || e === xNode.data) {
            return xNode;
        }
        // 记忆当前节点
        this._hot = xNode;
        // 尾递归,很容易改成迭代版本
        return this._searchIn(e, (e < xNode.data ? xNode.lChild : xNode.rChild));
    }

复杂度:时间复杂度为O(h),h为子树高度,因为没递归一次,查找高度都会下降1层。

4.2 插入

思路:调用search接口,记录_hot的位置,将待插入数字封装成新的节点插入。

实现:

    insert(e) {
        // 查找e,记录this._hot的的位置 ,O(h)
        let xNode = this.search(e, this._root);
        let eNode = null;
        // 禁止雷同元素,所以在查找失败时,才进行插入操作
        if (!xNode) {
            // 创建eNode,其父亲为this._hot
            eNode = new BinNode(e, this._hot);
            // 将eNode作为this._hot的左/右孩子
            e < this._hot.data ? this._hot.lChild = eNode : this._hot.rChild = eNode;
            this._size++;
            // 更新数高度,O(h)
            this._updateHeightAbove(eNode);
        }
        return eNode;
    }

复杂度:整体复杂度来自search和_updateHeightAbove,前文讲过,他们复杂度都不会超过O(h)。

4.3 删除

相较与插入接口的实现,删除接口要稍微复杂(复杂来源于节点的删除过程)些。他们同样都需要先调用search接口记录_hot的位置。

实现:

主算法:

    remove(x) {
        // 查找e,记录this._hot的的位置 ,O(h)
        let xNode = this.search(x, this._root);
        // 如果不存在返回false
        if (!xNode) {
            return false;
        }
        this._removeAt(xNode);
        this._size--;
        // 更新数高度,O(h)
        this._updateHeightAbove(this._root);

        return true;
    }

_removeAt实现,要区分几种情况:

  • 第一种:待删除节点只有一个分支,简单的将其删除,并将用他唯一而孩子取而代之。整棵树依然是BST。实际上这种方法里隐含了无分支情况。
       if (!xNode.rChild) {
            newNode = xNode.lChild;
        }
        else if (!xNode.lChild) {
            newNode = xNode.rChild;
        }
  • 第二种情况,双分支:使用法宝——化繁为简,将此种情况转化为单分支情况。找到待删除元素的直接后继(即中序遍历下紧邻的下一个元素),然后将待删除元素和直接后继交换,他的直接后继一定是一个单分支(参考中序遍历规则以及直接后继实现),此时此刻问题已被转化为第一种情况。如下图:

306801b33741dd7a5d44f7738553a5bf.png
            // 直接后继 复杂度O(h)
            oldNode = oldNode.succ();
            // 交换值
            let tempData = xNode.data;
            xNode.data = oldNode.data;
            oldNode.data = tempData;
            // xnode直接后继现在是xNode的值,他只能有右孩子。
            // f接下来将oldNode删除,并让他的右孩子代替其位置
            newNode = oldNode.rChild;

分支之后是统一的删除待删除节点和拼接新的节点的逻辑:

        // 记录_hot
        this._hot = oldNode.parent;
        if (newNode) {
            newNode.parent = this._hot;
            if (newNode.data < this._hot.data) {
                this._hot.lChild = newNode;
            }
            else {
                this._hot.rChild = newNode;
            }
        }
        // 处理无子节点情况
        if (!newNode) {
            oldNode.parent.isRChild(oldNode) ? oldNode.parent.rChild = null : oldNode.parent.lChild = null;
        }

复杂度:整体复杂度来自succ和_updateHeightAbove,他们复杂度都不会超过O(h)。

至此,已经实现了二叉搜索树的全部接口。具体实现代码以及配套测试代码可见:

https://github.com/tianhao351/javascript-data-structures/blob/master/src/bst/BST.js​github.com

5. 总结

目前实现的二叉搜索树不管是静态操作(查找)还是动态操作(插入和删除)都很高效,即时间复杂度均为O(h)。

然而就最坏意义而言,bst可能退化成一维链表(每个节点均只有一个子节点),h正比于n。

就平均意义而言,我们将待插入节点的所有随机插入bst,h正比于根号n。

至此,我们还无法对bst的高度h做有效的控制,所得到的性能也无法令我们满意。如果我们能够将h有效的控制,使bst能够适度平衡,高度将渐进的不超过h(log n)。具体请看下集:

声明:

本文完全原创,转载请联系我本人并标明出处。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值