JS手写二叉搜索树

394 阅读9分钟

1.二叉搜索树特点:

  • 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 任意节点的左、右子树也分别为二叉查找树;
  • 没有键值相等的节点。

2.为什么要使用二叉搜索树:

选择二叉搜索树而不是那些基本的数据结构,是因为在二叉搜索树上进行查找非常快,为二叉搜索树添加或删除元素 也非常快。

3.(练习题)来判断以下树是否为二叉搜索树:

A:

image.png

B:

image.png

答案请写再评论区哦~

二叉树有哪些常见的操作呢:

  • insert(key):向树中插入一个新得键。
  • search(key):再树中找到一个值,如果存在返回true,不存在返回false.
  • inOrderTraverse:中序遍历所有节点。
  • preOrderTraverse:先序遍历所有节点。
  • postOrderTraverse:后序遍历所有节点。
  • min:返回树中最小值。
  • max:返回树中最大值。
  • remove(key):从树中移除某个键。难点!!!!

4.接下来,我们用代码来实现上列常见的操作:

4.1:insert(key)

分析:首先第一步我们是要判断这个树存不存在,不存在的话,这个值就为root了。如果存在呢,就要开始比较大小。首先会去和根比较,如果比根小,那么我们需要先判断根的左边是否有值,如果没有值,rootleft就可以直接指向left了。如果有值,那么我们继续比较,重复以上的操作,我们最终会找一个节点的left或者rightnull,那么指向这个新值就好。

简易流程图:

image.png

代码:

function binarySearchTree() {
   // 设置root为null
   this.root = null;
   // 创建节点的函数
   function vNode(key) {
       this.left = null;
       this.right = null;
       this.key = key;
   }
   // 插入
   binarySearchTree.prototype.insert = function (key) {
       // 1.创建节点
       let newNode = new vNode(key);
       // 判断根节点
       if (this.root === null) {
           this.root = newNode;
       } else {
           // 执行插入递归函数
           this.insertNode(this.root, newNode);
       }
   };
   // 插入的递归函数
   binarySearchTree.prototype.insertNode = function (node, newNode) {
       // 向左查找
       if (node.key > newNode.key) {
           // 判断节点是否为空
           if (!node.left) {
               node.left = newNode;
           } else {
               // 不为空继续递归比较
               this.insertNode(node.left, newNode);
           }
       } else {
           if (!node.right) {
               node.right = newNode;
           } else {
               this.insertNode(node.right, newNode);
           }
       }
   };
}

测试代码:

let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
console.log(BSTTest);

结果:

image.png 符合我们的预期。

4.2:search(key)

分析:其实这个思路和上面插入非常像,插入我们是要确定这个值的位置在哪,而search则是找到这个值。所以我们再比较两个值的时候,加一个相等判断的特殊逻辑就可以实现search。

流程图:

image.png 绿色部分就是相比较插入新增的地方,其实原理是一样的。这里我们用while来实现以下。

代码:

binarySearchTree.prototype.search = function (key) {
        // current 作为目标节点
        let current = this.root;
        if(!current){
            return false;
        }
        if (current.key == key) {
            return true;
        }
        // 当目标节点为null时,结束循环,查找结束,返回false.
        while (current) {
            // 目标节点和搜索节点相等时
            if (key === current.key) {
                return true;
            }
            if (key > current.key) {
                current = current.right;
            } else {
                current = current.left;
            }
        }
        return false;
    };

测试代码:

let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
console.log(BSTTest);
console.log(BSTTest.search(1)); // false;
console.log(BSTTest.search(8)); // true;
console.log(BSTTest.search(7)); // true;

结果:

image.png

4.3:preOrderTraverse

遍历过程:

  • 先访问根节点。
  • 遍历其左子节点,左节点为空时,遍历其右子节点。
  • 遍历其右子节点,右子节点为空时,遍历结束。

前序分析:

对于前序遍历而言,当我们访问到这个节点的值时,我们就要抛出。

示例图:红字为输出顺序

image.png

代码:

// 先序遍历
    binarySearchTree.prototype.preOrderTraversal = function (node) {
        this.preOrderTraversalNode(this.root);
    };
    // 遍历递归函数
    binarySearchTree.prototype.preOrderTraversalNode = function (node) {
        if (node !== null) {
            // 遍历输出
            console.log(node.key);
            this.preOrderTraversalNode(node.left);
            this.preOrderTraversalNode(node.right);
        }
    };

测试代码:

let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
console.log(BSTTest);
BSTTest.preOrderTraversal();

结果:

image.png

结合示例图与代码分析:(精华部分,看不懂的再学学递归函数)

回到上面的示例图来看,当我们节点2打印完成后,此时是不在执行递归函数了。你以为递归已经结束了吗,其实没有,我们会回到上面一个递归中,也就是节点为5的时候,这个时候,this.preOrderTraversalNode(node.left)这个函数我们已经执行完毕了,我们要开始执行下面这个递归函数了,也就是this.preOrderTraversalNode(node.right)。当我们打印3完成之后呢,5的这个节点两个递归函数都已经执行完毕了。这个时候我们又回到上层递归中节点为10的时候,这个时候,this.preOrderTraversalNode(node.left)这个函数我们已经执行完毕了,我们要开始执行下面这个递归函数了,也就是this.preOrderTraversalNode(node.right),然后继续遍历。

4.4inOrderTraverse(中序遍历所有节点)

遍历过程:

  • 遍历其左子节点,左节点为空时,遍历其右子节点。
  • 先访问根节点。
  • 遍历其右子节点,右子节点为空时,遍历结束。

流程图:

image.png

中序分析:

当左子节点遍历完成后,再准备遍历右子节点之前,我们将其值抛出。对比前序遍历代码,我们只需要将打印的值放在两个递归函数之间就好了。

中序代码:

// 中序遍历
    binarySearchTree.prototype.preOrderTraversal = function (node) {
        this.preOrderTraversalNode(this.root);
    };
    // 遍历递归函数
    binarySearchTree.prototype.preOrderTraversalNode = function (node) {
        if (node !== null) {
            this.preOrderTraversalNode(node.left);
            // 遍历输出
            console.log(node.key);
            this.preOrderTraversalNode(node.right);
        }
    };

测试代码:

let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
console.log(BSTTest);
BSTTest.preOrderTraversal();

测试结果:

image.png

4.5:postOrderTraverse(后序遍历)

这个后续遍历就用一句话来总结吧:当一个节点左节点和右节点都遍历完成后,将其抛出。自然而然,我们只需要将打印的值放在两个递归函数之后就好了,那就不细说了。

4.6:min

分析:返回最小的值,其实我们一样都能看出只用找到最左边的值就好了。

binarySearchTree.prototype.min = function () {
        let current = this.root;
        while (current.left) {
            current = current.left;
        }
        return current.key;
    };

4.7max

分析:找到最右边的值就是最大值了。

binarySearchTree.prototype.max = function () {
        let current = this.root;
        while (current.right) {
            current = current.right;
        }
        return current.key;
    };

4.8:remove(key)

第一种情况:删除的节点没有子节点。

image.png 比如这些2,3,11,20.这些节点。

分析:

首先我们要找到这个节点,找到后判断其没有子节点。然后将其父节点的这个节点指向null.

流程图:

image.png

代码:

    binarySearchTree.prototype.delete = function (key) {
        let current = this.root;
        let parent = null;
        let isLeft = false;
        if (key === current.key) {
            this.root = null;
            return;
        }
        while (current) {
            // 找到这个节点
            if (key === current.key) {
                let isDeleteNode = current;
                //   1.如果没有子节点
                if (!current.left && !current.right) {
                    // 判断此节点是左边还是右边
                    if (isLeft) {
                        parent.left = null;
                        return true;
                    } else {
                        parent.right = null;
                        return true;
                    }
                }
            
            if (key > current.key) {
                isLeft = false;
                parent = current;
                current = current.right;
            } else {
                isLeft = true;
                parent = current;
                current = current.left;
            }
        }
        return false;
    };

测试代码:

let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
BSTTest.delete(4);
console.log(BSTTest);

结果:

image.png 节点值为4已经成功删除了。

第二种情况:删除的节点只有一个根节点。

image.png 我们要删除节点5和15.

分析:

当我们要删除的节点有一个子节点时,只需要将这个删除节点的子节点替换到自己的位置即可。

image.png

代码:

binarySearchTree.prototype.delete = function (key) {
        let current = this.root;
        let parent = null;
        let isLeft = false;
        if (key === current.key) {
            this.root = null;
            return;
        }
        while (current) {
            // 找到这个节点
            if (key === current.key) {
                let isDeleteNode = current;
                //   1.如果没有子节点
                if (!current.left && !current.right) {
                    // 判断此节点是左边还是右边
                    if (isLeft) {
                        parent.left = null;
                        return true;
                    } else {
                        parent.right = null;
                        return true;
                    }
                }
                // 新增 ===================================================
                // 有一个子节点
                else if (!current.left || !current.right) {
                    // 本左 子左 只写了一种情况还有三种先省略
                    if (current.left && isLeft) {
                        parent.left = current.left;
                        return true;
                    }
                }
            
            if (key > current.key) {
                isLeft = false;
                parent = current;
                current = current.right;
            } else {
                isLeft = true;
                parent = current;
                current = current.left;
            }
        }
        return false;
    };

测试代码:

let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(6);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
BSTTest.delete(7);
console.log(BSTTest);

测试结果:

image.png

第三种情况:删除的节点有两个子节点。

image.png 我们要删除类似6,9,15这些节点.

分析:

当我们要删除这些节点的时候,其实是很麻烦的。所以我们要找到一些规律。我们要从这个节点的下面,找到一个节点来代替他的位置。那么这个节点就有一个特点,要么比删除的节点大一点,要么小一点,要无线接近与这个节点,我们就可以来替代被删除节点的位置了。这两个节点有个特别的名字。

  • 前驱:比该节点小一点点的节点。
  • 后继:比该节点大一点点的节点。 比如说我们要删除节点6,我们要找它的后继节点,看示例图一眼就能发现是节点7,此时我们就可以将节点7放到节点6的位置上去。这时又多了一种情况,当后继节点有右节点的时候,我们还需要把这个节点放到之前节点7的位置。

删除节点6后的示例图:

image.png

代码:

binarySearchTree.prototype.delete = function (key) {
        let current = this.root;
        let parent = null;
        let isLeft = false;
        if (key === current.key) {
            this.root = null;
            return;
        }
        while (current) {
            // 找到这个节点
            if (key === current.key) {
                let isDeleteNode = current;
                //   1.如果没有子节点
                if (!current.left && !current.right) {
                    // 判断此节点是左边还是右边
                    if (isLeft) {
                        parent.left = null;
                        return true;
                    } else {
                        parent.right = null;
                        return true;
                    }
                }
                // 新增
                // 有一个子节点
                else if (!current.left || !current.right) {
                    // 本左 子左 只写了一种情况还有三种先省略
                    if (current.left && isLeft) {
                        parent.left = current.left;
                        return true;
                    }
                }
                // 新增的 ====================================================================
                // 有两个子节点
                // 找到前驱后继节点 替换删除的
                else if (current.left && current.right) {
                    // 找后继
                    let houji = current;
                    // 后继父节点 
                    houjiParent = current;
                    // 寻找后继节点
                    houji = houji.right;
                    while (houji.left) {
                        houjiParent = houji;
                        houji = houji.left;
                    }
                    //判断后继节点是否为要删除节点的right
                    // 如果是的话 就不需要找houjiParent这个节点了 houjiParent就等于删除的节点
                    if (isDeleteNode.right !== houji) {
                        houjiParent.left = houji.right;
                        houji.right = isDeleteNode.right;
                    }

                    // 判断后继节点有没有子节点
                    if (!houji.right) {
                        // 查找的是左节点
                        if (isLeft) {
                            parent.left = houji;
                        } else {
                            parent.right = houji;
                        }
                    }
                    // 有子节点
                    else {
                        // 查找的是左节点
                        if (isLeft) {
                            parent.left = houji;
                        } else {
                            parent.right = houji;
                        }
                    }
                    // 替换完成之后 后继节点的左节点要指向删除节点的左节点 最终完成替换
                    houji.left = current.left;
                }
                // ============================================================
            
            if (key > current.key) {
                isLeft = false;
                parent = current;
                current = current.right;
            } else {
                isLeft = true;
                parent = current;
                current = current.left;
            }
        }
        return false;
    };

测试代码:

let BSTTest = new binarySearchTree();
BSTTest.insert(11);
BSTTest.insert(5);
BSTTest.insert(12);
BSTTest.insert(4);
BSTTest.insert(9);
BSTTest.insert(10);
BSTTest.insert(7);
BSTTest.insert(8);
BSTTest.delete(5);
console.log(BSTTest);

执行结果:

image.png 以上就是整个删除的操作了,其实就分三种情况而已。最难的一种找到后继节点也挺简单的。实在不理解,找个本子画一画就能懂了。还不懂的留言哈,帮你解答。

感言

其实在学这个地方的时候,最惊艳我的地方就是遍历那一块。仅仅改变打印位置就是不一样的顺序。递归的魅力还是值得我去好好学学的。

彩蛋

最为前端er,其实二叉搜索树再开发时是基本用不上的。但是树的这个结构我们并不模式,尤其是dom树。作为两大框架VueReact来讲,diff算法是一道大坎。期待我下一章把,手写diff算法

喜欢的记得点赞关注哦~