从零开始:二叉搜索树插入、删除与遍历全攻略

151 阅读8分钟

简单介绍

二叉查找树(Binary Search Tree),又叫做二叉排序树或二叉搜索树,它有以下几个性质:

  • 每一个节点上最多有两个子节点。
  • 任意节点左子树上的值都小于当前节点。
  • 任意节点右子树上的值都大于当前节点。

image.png

也就是说,对二叉搜索树中序遍历,就能得到一个有序的数列

代码实现

构建二叉搜索树

前置思考

构建二叉树,要考虑往树中插入节点的几种情况:

  • 当这是一颗空树时,那只需要构建一个节点放入插入的值,并且这个节点就是树的根节点
  • 如果这不是一颗空树,那需要从根节点一步步开始对比,找到这个节点应该插入的位置
    • 【当前节点】的值比【对比节点】大,那【当前节点】在【对比节点】的右子树
      • 【对比节点】的右节点为空,则【对比节点】的右节点 === 【当前节点】
      • 【对比节点】的右节点不为空,则【对比节点】的右节点作为下一个对比节点,继续往下对比
    • 【当前节点】的值比【对比节点】小,那【当前节点】在【对比节点】的左子树
      • 【对比节点】的左节点为空,则【对比节点】的左节点 === 【当前节点】
      • 【对比节点】的左节点不为空,则【对比节点】的左节点作为下一个对比节点,继续往下对比

将上诉情况转换成流程图如下:

graph TD
    A[开始插入节点] --> B{是否为空树?}
    B -->|是| C[创建新节点<br>作为根节点]
    C --> D[插入完成]
    B -->|否| E[当前对比节点 = 根节点]
    E --> F{新节点值 <<br>对比节点值?}
    F -->|是| G{对比节点的<br>左子节点为空?}
    G -->|是| H[新节点 = 对比节点的<br>左子节点]
    H --> D
    G -->|否| I[对比节点 = 左子节点]
    I --> F

    F -->|否| J{对比节点的<br>右子节点为空?}
    J -->|是| K[新节点 = 对比节点的<br>右子节点]
    K --> D
    J -->|否| L[对比节点 = 右子节点]
    L --> F

模拟构建

模拟一下用【98,12,9,2,13,134,100,156】这个数组构建一颗二叉搜索树的过程:

第一步:插入98:此时是空树,创建节点为根节点

image.png

第二步:插入12:12<98, 且98的左节点为空,于是12成为98的左节点

image.png

第三步:插入9:9<98, 98的左节点不为空

继续拿12对比,9<12,且12的左节点为空,9成为12的左节点

image.png

第四步:插入2:2<98, 98的左节点不为空

继续拿12对比,2<12,且12的左节点不为空

继续拿9对比,2<9,且9的左节点为空,2成为9的左节点

image.png

第五步:插入13:13<98, 98的左节点不为空

继续拿12对比,13>12,且12的右节点为空,13成为12的右节点

image.png

第六步:插入134:134>98,且98的右节点为空,134成为98的右节点

image.png

第七步:插入100:100>98,98的右节点不为空

继续拿134对比,100<134,且134的左节点为空,100成为134的左节点

image.png

第八步:插入156:156>98,98的右节点不为空

继续拿134对比,156>134,且134的的右节点为空,156成为134的右节点

image.png

代码实现

function TreeNode(val){
	 this.val = val
   this.left = this.right = null
}

function insertBST(root, val) {
    if (root === null) {
        return new TreeNode(val);
    }

    // 查找一个可以插入的空位置
    if (val < root.value) {
        // 如果左子树为空,则插入新节点
        if (root.left === null) {
            root.left = new TreeNode(val);
        } else {
            // 否则继续在左子树中查找
            root.left = insertBST(root.left, val);
        }
    } else {
        // 如果右子树为空,则插入新节点
        if (root.right === null) {
            root.right = new TreeNode(val);
        } else {
            // 否则继续在右子树中查找
            root.right = insertBST(root.right, val);
        }
    }

    // 返回更新后的根节点
    return root;
}

从二叉搜索树中查找某个值

前置思考

查找比较简单,从根节点开始一步步往下找:

  • 第一步判断目前的【查找节点】的值是否跟要找的值一致,是的话返回该节点
  • 否则,进入第二步:判断【查找值】是大于还是小于【查找节点的值】
    • 【查找值】>【查找节点的值】,递归,往【查找节点】的右节点去找
    • 【查找值】<【查找节点的值】,递归,往【查找节点】的左节点去找

代码实现

// BST查找数据
function findBST(root,val){
    if(root == null) return null;

    if(val === root.val) return root
    if(val < root.val){
        return findBST(root.left,val)
    }else{
        return findBST(root.right,val)
    }

}

从二叉搜索树中删除某个值

前置思考

针对待删除结点的子节点个数的不同,我们将它分为三种情况加以处理:

  • 删除的节点没有子节点,直接删除,只需要将父节点中指向该节点的链接设置为 null 就可以了。
  • 删除的节点只有一个子节点(只有左子节点或只有右子节点),只需要更新父节点指向待删除结点的子节点即可
  • 删除的节点有两个子节点, 那删除之后,怎样才能维持住这颗二叉树是二叉搜索树才是关键。
    • 关键是找前驱节点或者后继节点。(后面会介绍为啥这才是关键)
    • 前驱节点指的是小于该键的最大键,后继节点指的是大于该键的最小键
    • 通过中序遍历,在得到的序列中位于该点左侧的就是前驱节点,右侧的就是后驱节点。

模拟删除

删除的节点没有子节点:节点2

image.png

删除的节点只有一个子节点:节点9

image.png

删除的节点有两个子节点:节点12

image.png

到这里你是不是以为只需要找【被删除节点】的其中一个子节点顶替【被删除的节点】就可以了?

那结合下面的案例进行分析,需要考虑下面两个问题:

  • 假设我们拿【被删除节点】的左节点顶替上去,那【被删除节点】的右节点需要放到哪里去?
  • 假设我们拿【被删除节点】的右节点顶替上去,那【被删除节点】的左节点需要放到哪里去?

image.png

通过上面的分析,发现,最终还是要找前驱节点或者后继节点才能解决问题。

需要这么麻烦,无非就是【顶替节点】的子节点已经被占用了,无法把另一边插入进去充当子节点

那如果我们不拿【被删除节点】的去顶替,直接就找【被删除节点】的【前驱节点】或者【后继节点】顶替,就不会出现这个问题了?因为他们一定是【叶子节点】,子节点都是null,可以插入子节点。

image.png

可以发现,操作更便捷简单了:

  • 把被删除节点的值替换成【前驱节点】或者【后继节点】的值
  • 然后问题就转变成删除这个【前驱节点】或者【后继节点】,就将问题转变成了删除叶子节点了

代码实现

查找前驱节点(小于该节点的最大值):

function findPredecessor(root,targetValue){
    if(!node || root.val === node.val) return null;

    let current = root;
    let predecessor = null;

    // 找到目标值所处的节点是哪个
    while (current !== null) {
        // 如果当前节点的值大于目标值,则继续向左查找前驱节点
        if (targetValue < current.value){
            current = current.left;
        }else if (targetValue > current.value){
            // 当前节点可能是目标节点的前驱节点,先记录下来
            predecessor = current;
            // 继续往右子树查找
            current = current.right;
        }else {
            // 找到了目标节点
            break;
        }
    }

    if (current === null) {
        return null; // 没有找到目标节点
    }

    // 如果目标节点有左子树,找到左子树中的最右节点
    if (current.left !== null) {
        let leftSubtree = current.left;
        while (leftSubtree.right !== null) {
            leftSubtree = leftSubtree.right;
        }
        return leftSubtree;
    }

    // 如果目标节点没有左子树,返回之前记录的前驱节点
    return predecessor;

}

查找后继节点(大于该节点的最小值):

// 查找后继结点: 大于targetValue的最小值
function findSuccessor(root, targetValue) {
    if(!node || root.val === node.val) return null;
    let current = root;
    let successor = null;
    while (current !== null) {

        if(targetValue < current.value){ // 往左查找
              // 当前节点可能是目标节点的后继节点
              successor = current;
              current = current.left;
        }else if (targetValue > current.value) { // 继续往右
            current = current.right;
        }else{
            // 找到目标节点
            break;
        }
    }


    if (current === null) {
        return null; // 没有找到目标节点
    }

    // 如果目标节点有右子树,找到右子树中的最左节点
    if (current.right !== null) {
        let rightSubtree = current.right;
        while (rightSubtree.left !== null) {
            rightSubtree = rightSubtree.left;
        }
        return rightSubtree;
    }

    // 如果目标节点没有右子树,返回之前记录的后继节点
    return successor;

}

删除操作:


//删除
function deleteBST(root,key){
    if (root === null) {
        return root;
    }
    
    let current = root;
    let parent = null;
    let isLeftChild = false;
   // 查找要删除的节点
   while (current !== null && current.val !== key) {
        parent = current;
        if (key < current.val) {
            current = current.left;
            isLeftChild = true;
        } else {
            current = current.right;
            isLeftChild = false;
        }
    }

     // 如果没有找到要删除的节点
     if (current === null) {
        return root;
    }

    // 情况1:要删除的节点是叶子节点
    if (current.left === null && current.right === null) {
         if (isLeftChild) {
            parent.left = null;
        } else {
            parent.right = null;
        }
    }// 情况2:要删除的节点只有一个子节点
    else if (current.right === null) { // 只有左子节点
        if (isLeftChild) {
            parent.left = current.left;
        } else {
            parent.right = current.left;
        }
    }else if (current.left === null) { // 只有右子节点
        if (isLeftChild) {
            parent.left = current.right;
        } else {
            parent.right = current.right;
        }
    }else{
       // 情况3:要删除的节点有两个子节点
       // 找到右子树的最小节点
       const successor = findMin(current.right);
        // 保存后继节点的值
        const successorValue = successor.val;
        // 递归删除后继节点
        root = deleteBST(root,successorValue);
        // 用后继节点的值替换当前节点的值
        current.value = successorValue;
    }
    return root;

}
// 辅助函数:找到以给定节点为根的子树中的最小值节点
function findMin(node) {
    while (node && node.left !== null) {
        node = node.left;
    }
    return node;
}