常见二叉搜索树算法

二叉搜索树

二叉搜索树(Binary Search Tree,简写 BST)

BST 特性

1、对于 BST 的每一个节点 node ,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。

2、对于 BST 的每一个节点 node ,它的左侧子树和右侧子树都是 BST。

从做算法提的角度来看 BST,除了它的定义,还有一个重要的性质:BST 的中序遍历结果是有序的(升序)

也就是说,如果输入一棵 BST,以下代码可以将 BST 中每个节点的值升序打印出来:

void traverse(TreeNode root) {
    if(root == null) {
        return;
    }
    traverse(root.left);
    // 中序遍历代码位置
    print(root.val);
    traverse(root.right);
}

第一题:寻找第 K 小的元素

我们根据上面的性质,我们可以进行一下中序遍历,就可以直接找出第 K 小的元素

int res = 0;
int index = 0;
int kthSmallest(TreeNode root, int k) {
    traverse(root, k);
    return res;
}
void traverse(TreeNode root, int k) {
    if (root == null) {
        return;
    }
    traverse(root.left, k);
    index++;
    if(index == k) {
        res = root.val;
        return;
    }
    traverse(root.right, k);
}

第二题:转化累加树

题目要求:给出 搜索树的根节点,该树的节点值各不相同,请你将其转化为累加树,使每个节点 node 的值等于原树中大于或等于 node.val 的值之和。

image-20220208192902566

比如图中的节点 5,转化成累加树的话,比 5 大的节点有 6,7,8,加上 5 本身,所以累加树上这个节点的值应该是 5+6+7+8=26。

这个问题并不复杂,就是累加计算大于该节点的所有数字之和。

TreeNode convertBST(TreeNode root) {
    build(root);
    return root;
}
int sum = 0;
public void build(TreeNode root) {
    if (root == null) {
        return;
    }
    build(root.right);
    sum += root.val;
    root.val = sum;
    build(root.left);
}

这道题目,核心还是 BST 的中序遍历特性,只不过我们修改了递归顺序,降序遍历 BST 的元素值,从而契合题目累加树的要求。


有了 BST 的这种特性,就可以在二叉树中做类似二分搜索的操作,搜索一个原树的效率很高。

一棵合法的二叉树:

image-20220208195348579

对于 BST 相关的问题,可能会经常看到类似下面这样的代码逻辑

void BST(TreeNode root, int target) {
    if(root.val == target) {
        // 找到目标,做点什么
    }
    if(root.val < target) {
        BST(root.right, target);
    }
    if(root.val > target) {
        BST(root.left, target);
    }
}

这个代码框架其实和二叉树的遍历框架差不多,无非就是利用了 BST 左小右大的特征而已。

第三题:判断 BST 的合法性

按照我们的思路:节点的左小右大的特征写代码

boolean isValidBST(TreeNode root) {
    if(root == null) {
        return true;
    }
    if(root.left != null && root.val <= root.left.val) {
        return false;
    }
    if(root.right != null && root.val >= root.right.val) {
        return false;
    }
    return isValidBST(root.left) && isValidBST(root.right);
}

上面的代码看似没有问题,实际上是错误的,看下图中的 6 和 10

image-20220208200944096

出现问题的原因在于,对于每个节点 root ,代码之检查了它的左右孩子节点是否符合左小右大的原则;但是根据BST的定义,root 的整个左子树都要小于 root.val,整个右子树都要大于 root.val

正确代码

boolean isValidBST(TreeNode root) {
    return isValidBST(root, null, null);
}
boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) {
    if(root == null) {
		return true;
    }
    // 若 root.val 不符合 max 和 min 的限定,说明是不合法 BST
    if(min != null && root.val <= min.val) {
        return false;
    }
    if(max != null && root.val >= max.val) {
        return false;
    }
    // 限定左子树的最大值是 root.val, 右子树的最小值是 root.val
    return isValidBST(root.left, min, root) &&
        isvalidBST(root.right, root, max);
}

我们通过辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点,这也是二叉树算法的一个技巧

第四题:在 BST 中搜索元素

如果在一棵普通的二叉树中,可以这样写代码

TreeNode searchBST(TreeNode root, int target) {
    if(root == null) {
        return null;
    }
    if(root.val == target) {
        return root;
    }
    searchBST(root.left, target);
    searchBST(root.right, target);
    return left != null ? left : right
}

这样写的话就穷举了所有节点,适用于所有普通二叉树

利用 BST 特性,加一些判断,在 BST 中进行搜索元素

TreeNode searchBST(TreeNode root, int target) {
    if(root == null) {
        return null;
    }
    // 去右子树所搜
    if(root.val < target) {
        searchBST(root.right, target);
    }
    if(root.val > target) {
        SearchBST(right.left, target);
    }
    return root;
}

第五题:在 BST 中插入一个数

TreeNode insertIntoBST(TreeNode root, int val) {
    if (root == null) {
        return new TreeNode(val);
    }
    if (root.val > val) {
        return insertIntoBST(root.left, val);
    }
    if (root.val < val) {
        return insertIntoBST(root.right, val);
    }
    return root;
}

第六题:在 BST 中删除一个数

首先是找到目标数字,然后进行删除。

代码框架

TreeNode deleteNode(TreeNode root, int key) {
    if (root.val == key) {
        // 找到位置,进行删除
    }
    if (root.val < key) {
        return deleteNode(root.right, key);
    }
    if (root.val > key) {
        return deleteNode(root.left, key);
    }
    return root;
}

找到目标节点了,比如说是 A ,如果这个节点没有左右子节点,那么直接删除即可,如果这个节点有一个子结点,那么将子节点来代替自己的位置即可,如果有两个,就需要进行判断处理了。

情况1:A 恰好是末端节点,两个子节点都为空,那么它可以直接被删掉

image-20220209195800830
if (root.left == null && root.right == null) {
    return null;
}

情况2:A 只有一个非空子节点,那么它要让这个孩子接替自己的位置

if(root.left == null) {
    return root.right;
}
if(root.right == null) {
    return root.left;
}

情况3:A 有两个子节点,为了不破坏 BST 的性质,A 必须找到左子树中最大的哪个节点,或者右子树中最小的哪个节点来代替自己。

if(root.left != null && root.right != null) {
    // 找到右子树的最小节点
    TreeNode minNode = getMin(root.right);
    // 把 root 改成 minNode
    root.val = minNode.val;
    // 转而去删除 minNode
    root.right = deleteNode(root.right, minNode.val);
}
if(root.left != null && root.right != null) {
    // 找到左子树的最大节点
    TreeNode maxNode = getMax(root.left);
    // 把 root 改成 maxNode
    root.val = maxNode.val;
    // 转而去删除 maxNode
    root.left = deleteNode(root.left, maxNode.val);
}

三种情况,完整代码

TreeNode deleteNode(TreeNode root, int key) {
    if(root == null) {
        return null;
    }
    if(root.val == key) {
        // 这两个 if 就可以将情况1与情况2给处理了
        if(root.left == null) {
            return root.right;
        }
        if(root.right == null) {
            return root.left;
        }
        // 情况3处理
        // 获得右子树的最小节点
        TreeNode minNode = getMin(root.right);
        root.right = deleteNode(root.right, minNode.val);
        // 用右子树最小的节点替换 root 节点
        minNode.left = root.left;
        minNode.left = root.right;
        root = minNode;
    } else if(root.val > key) {
        return deleteNode(root.left, key);
    } else if(root.val < key) {
        return deleteNode(root.right, key);
    }
    return root;
}
TreeNode getMin(TreeNode node) {
   	while(node.left != null) {
        node = node.left;
    }
    return node;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值