js数据结构,封装一个二叉搜索树

100 阅读5分钟

二叉树

当树形结构中,每个节点最多只存在一左一右两个子节点的时候,称之为二叉树

二叉搜索树(BST)

二叉搜索树是二叉树的一种,它的左侧子节点的值比父节点要小,右侧子节点的值比父节点要大

image.png

节点的封装

根据二叉树的定义,每个节点都有值和两个子节点的指针三个属性

class BSTNode {
    constructor(key) {
        this.key = key,
        this.left = null,
        this.right = null
    }
}

树的封装

属性

树结构的话,有个根节点就可以遍历整个结构了

class BST {
    constructor() {
        this.root = null
    }
}

方法

插入 insert()

首先,在插入时有两种情况,即树是否为空,如果为空,则直接添加根节点,如果不为空,则需要找到需要插入的位置,常用的方法有循环和递归,这里使用递归实现

        insert(key) {
            if(this.root === null) {
                this.root = new BSTNode(key)
            } else {
                this.insertNode(this.root, key)
            }
        }

        insertNode(node, key) {
            if(key < node.key) {
                // 当变量的值为基本数据类型时,无法return该变量,再给其赋值,因为return出去的是值本身
                if(node.left === null) return node.left = new BSTNode(key)
                this.insertNode(node.left, key)
            } else if (key > node.key) {
                if(node.right === null) return node.right = new BSTNode(key)
                this.insertNode(node.right, key)
            } else {}
        }
中序遍历 inOrderMap()

中序遍历是指,在二叉搜索树中,按照左 根 右的顺序遍历每一组节点,它的特点是,在二叉搜索树中是按照节点值的大小,从小到大遍历,这里的代码使用递归,出乎意料的简单,并且非常优雅

        inOrderMap(callback) {
            if(this.root === null) return console.error('当前树的根节点为空');
            this.inOrderMapNode(this.root, callback)
        }
        
        inOrderMapNode(node, callback) {
            if(node === null) return
            this.inOrderMapNode(node.left, callback)
            callback(node)
            this.inOrderMapNode(node.right, callback)
        }

当然,你也可以使用循环,就比较复杂了,但这里可以锻炼一下咱对数据结构的熟悉程度,所以这里也写下来,当然,只写这一次,因为遍历的方法有了,无非就是执行callback语句位置的问题

因为在二叉搜索树上,最小的节点一定在最左边,也就是说,我们必须到了最后一个子节点才开始执行回调,他的执行循序是先进后出,这让我们可以联想到另一个数据结构,栈(Stack),想到刚刚递归能很简单的实现中序遍历,并且递归的执行也是先进后出,这里也就不难理解了

        inOrderMapNode(tree,callback) {
            // 用于扁平化树状结构
            // const output = [];
            const stack = new Stack();
            // 将指针指向根节点
            let node = tree;
            // 当栈不为空 并且node 为空时 跳出循环
            // 当栈为空时,要么在开始,要么在结尾,而node为空则只会在node指针在最底层时触发,
            // 因此该条件只会在遍历完成时满足
            while (!stack.isEmpty || node) {
                // 当node不为空,将当前node存储到栈内,并将指针向left移动
                if (node) {
                stack.push(node);
                node = node.left;
                } else {
                // 当node为空时,即node指针已经到了最下层
                // 因为栈先进后出,所以这里是从下往上,将node从栈中弹出
                const pop = stack.pop();
                // 将弹出的节点存入数组
                // output.push(pop.data);
                callback(pop.key)
                // 如果弹出的node的right指针不为空,则node指针向right移动
                if (pop.right) {
                    node = pop.right;
                }
                // 接下来,会以目前的node指针指向的node为根节点,重复上面的操作,
                // 可以理解为,从下往上,查询栈中有right的元素,有则在他身上新压一个栈
                }
            }
        }
先序遍历 及 后序遍历

先序遍历是指,在二叉搜索树中,按照根 左 右的顺序遍历每一组节点
后序遍历是指,在二叉搜索树中,按照左 右 根的顺序遍历每一组节点

        preOrderMap(callback) {
            preOrderMapNode(this.root, callback)
        }

        preOrderMapNode(node, callback) {
            callback(node)
            this.preOrderMapNode(node.left, callback)
            this.preOrderMapNode(node.right, callback)
        }

        postOrderMap(callback) {
            postOrderMapNode(this.root, callback)
        }

        postOrderMapNode(node, callback) {
            this.preOrderMapNode(node.left, callback)
            this.preOrderMapNode(node.right, callback)
            callback(node)
        }
查询
  • max()    查询最大节点
  • min()    查询最小节点
  • search()    查询是否存在该节点
        min() {
            return this.minNode(this.root)
        }

        minNode(node) {
            let currentNode = node
            while(currentNode != null && currentNode.left != null) {
                currentNode = currentNode.left
            }
            return currentNode
        }

        max() {
            return this.maxNode(this.root)
        }

        maxNode(node) {
            let currentNode = node
            while(currentNode != null && currentNode.right != null) {
                currentNode = currentNode.right
            }
            return currentNode
        }

        // search(key) {
        //     let currentNode = this.root
        //     while(currentNode != null) {
        //         if(key < currentNode.key) currentNode = currentNode.left
        //         else if(key > currentNode.key) currentNode = currentNode.right
        //         else {
        //             return true
        //         }
        //     }
        //     return false
        // }

        search(key) {
            this.searchNode(this.root, key)
        }

        searchNode(node, key) {
            if(node === null) return false
            if(key < node.key) return searchNode(node.left, key)
            else if(key > node.key) return searchNode(node.right, key)
            else return true
        }
删除

删除是一个比较复杂的操作,分多种情况:

1. 当需要删除的节点没有子节点时

这是最简单的情况,只需要将需要删除的节点的父节点引用设为空即可

2. 当需要删除的节点有且只有一个节点时

这是比较简单的情况,直接将上下两个节点连接即可

3. 当需要删除的节点有多个子节点时

这是最复杂的情况,在这种情况下,需要将删除的目标节点的右侧节点下最小的节点放到目标节点的位置,作为连接,并且还需要注意这个最小节点可能还有右节点,可以直接递归自己来处理这个问题

当然,你用左侧最大的也行,据说这样是有问题的,但我没看出来哪里有问题,有时间去写个demo测一下,有知道的朋友也可以直接在评论里说明一下,你可以获得我真诚的一句多谢大佬

        remove(key) {
            this.root = this.removeNode(this.root, key)
        }

        removeNode(node,key) {
            if(!node) return false
            if(key < node.key) {
                node.left = this.removeNode(node.left, key)
                return node
            }
            else if(key > node.key)  {
                node.right = this.removeNode(node.right, key)
                return node
            }
            else {
                if(!node.left && !node.right) {
                    return null
                } 
                if(node.left != null && !node.right) {
                    return node.left
                }
                else if(node.right != null && !node.left) {
                    return node.right
                } else {
                    const n = this.minNode(node.right)
                    node.key = n.key
                    // 注2
                    // 这里很容易犯的错误就是直接写 n = null,
                    // 这是将变量n的地址值清空,并不会改变堆中的对象,
                    // 如果其他地方仍在引用这个对象,并不会改变值,
                    // 这也是为什么这里都要将处理的node返回并赋值
                    node.right = this.removeNode(node.right, n.key)
                    return node
                }
            }
        }
        
        // 注2演示
        let obj = {a:1}
        let b = obj
        obj = null // 清除的是地址值
        console.log(b); // {a:1}