浅析二叉搜索树

138 阅读4分钟

---

主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black

贡献主题:github.com/xitu/juejin…

theme: juejin highlight:

概念

二叉搜索树(BST)是一种特殊的二叉树,它满足如下特性:

  1. 每个节点的值必须大于(或等于)其左子树中的任意值
  2. 每个节点的值必须小于(或等于)其右子树中的任意值

下面是一个二叉搜索树的示例:

验证二叉搜索树

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

节点的左子树只包含小于当前节点的数。 节点的右子树只包含大于当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:
输入:
    2
   / \
  1   3
输出: true

示例 2:
输入:
    5
   / \
  1   4
     / \
    3   6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
     根节点的值为 5 ,但是其右子节点值为 4

链接:leetcode-cn.com/leetbook/re… 来源:力扣(LeetCode)

递归

class Solution {
    public boolean isValidBST(TreeNode root) {
        return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    public boolean dfs(TreeNode root, long lower, long upper) {
        if (root == null) return true;
	      // 根节点 <= 下边界或 >= 上边界
        if (root.val <= lower || root.val >= upper) return false;
      	// 递归判断左子树和右子树是否都满足二叉搜索树
        return dfs(root.left, lower, root.val) && dfs(root.right, root.val, upper);
    }
}
  • 时间复杂度:O(n) 其中 n 是二叉树节点的个数。在递归调用时二叉树的每个节点最多被访问一次。因此时间复杂度是O(n)
  • 空间复杂度:O(n) 其中 n 是二叉树节点的个数。栈中最多存储 n 个节点变量,所以空间复杂度是O(n)

迭代

public boolean iterate(TreeNode root) {
        Deque<TreeNode> stack = new LinkedList<>();
        long pred = Long.MIN_VALUE;
        while (!stack.isEmpty() || root != null) {
          	//遍历到左子树尽头
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.poll();
          	//中序遍历二叉搜索树得到的是升序数组。root小于等于前驱节点说明不是二叉搜索树
            if (root.val <= pred) return false;
            pred = root.val;
            root = root.right;
        }
        return true;
    }

二叉搜索树迭代器

实现一个二叉搜索树迭代器。你将使用二叉搜索树的根节点初始化迭代器。

调用 next() 将返回二叉搜索树中的下一个最小的数。

示例:

img

BSTIterator iterator = new BSTIterator(root);
iterator.next();    // 返回 3
iterator.next();    // 返回 7
iterator.hasNext(); // 返回 true
iterator.next();    // 返回 9
iterator.hasNext(); // 返回 true
iterator.next();    // 返回 15
iterator.hasNext(); // 返回 true
iterator.next();    // 返回 20
iterator.hasNext(); // 返回 false

提示:

next() 和 hasNext() 操作的时间复杂度是 O(1),并使用 O(h) 内存,其中 h 是树的高度。 你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 中至少存在一个下一个最小的数

链接:leetcode-cn.com/leetbook/re… 来源:力扣(LeetCode)

class BSTIterator {

    Deque<TreeNode> stack;

    public BSTIterator(TreeNode root) {
        stack = new LinkedList<>();
        help(root);
    }

	  // root及左子树入栈
    private void help(TreeNode root) {
        while (root != null) {
            stack.push(root);
            root = root.left;
        }
    }
    
    public int next() {
        TreeNode root = stack.poll();
      	// 如果右子树不为空,右子树入栈
        if (root.right != null) {
            help(root.right);
        }
        return root.val;
    }
    
    public boolean hasNext() {
        return stack.size() > 0;
    }
}
  • 时间复杂度:O(n) 其中 n 是二叉树节点个数。需要遍历所有二叉树节点入栈,所以时间复杂度是 O(n)
  • 空间复杂度:O(n) 其中 n 是二叉树节点个数。在单调二叉树的最坏情况下,栈中存储需要存储所有二叉树节点。空间复杂度是O(n)

二叉搜索树的基本操作

二叉树中的搜索

给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。

例如,

给定二叉搜索树:

    4
   / \
  2   7
 / \
1   3

和值: 2 你应该返回如下子树:

  2     
 / \   
1   3

在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。

链接:leetcode-cn.com/leetbook/re… 来源:力扣(LeetCode)

迭代

public TreeNode searchBST(TreeNode root, int val) {
        while (root != null) {
            if (root.val == val) break;
            root = val < root.val ? root.left : root.right;
        }
        return root;
    }
  • 平均时间复杂度:O(logn),最坏时间复杂度:O(n)。
  • 空间复杂度:O(1)

递归

public TreeNode searchBST(TreeNode root, int val) {
        if (root == null || root.val == val) return root;
        return root.val > val ? searchBST(root.left, val) : searchBST(root.right, val);
    }
  • 平均时间复杂度:O(logn),最坏时间复杂度:O(n)。
  • 空间复杂度:O(1)

二叉树中的插入

给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。

示例 1:

输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]

示例 2:

输入:root = [40,20,60,10,30,50,70], val = 25 输出:[40,20,60,10,30,50,70,null,null,25]

链接:leetcode-cn.com/leetbook/re… 来源:力扣(LeetCode)

递归

public TreeNode insertIntoBST(TreeNode root, int val) {
        // 空树-创建新节点
        if (root == null) return new TreeNode(val);
        if (root.val > val) {
            // 左子树中创建新节点,并返回新的root
            root.left = insertIntoBST(root.left, val);
        } else if (root.val < val) {
            // 右子树中创建新节点,并返回新的root
            root.right = insertIntoBST(root.right, val);
        }
        return root;
    }
  • 时间复杂度:O(n) 其中 n 是二叉树节点个数。最坏情况下我们需要将节点插入树的最深叶子节点,而叶子节点最深是O(n) 。
  • 空间复杂度:O(1) 只是用到了常数大小的空间。

迭代

public TreeNode insertIntoBST(TreeNode root, int val) {
  			// 空树-创建新节点
        if (root == null) return new TreeNode(val);
        TreeNode node = root;
        while (node != null) {
            if (val < node.val) {
              	// 左子树为空-新建节点做为左子树
                if (node.left == null) {
                    node.left = new TreeNode(val);
                    break;
                } else {
                  	// 迭代左子树寻找插入位置
                    node = node.left;
                }
            } else {
              	// 右子树为空-新建节点做为右子树
                if (node.right == null) {
                    node.right = new TreeNode(val);
                    break;
                } else {
                  	// 迭代右子树寻找插入位置
                    node = node.right;
                }
            }
        }
        return root;
    }
  • 时间复杂度:O(n) 其中 n 是二叉树节点个数。最坏情况下我们需要将节点插入树的最深叶子节点,而叶子节点最深是O(n) 。
  • 空间复杂度:O(1) 只是用到了常数大小的空间。

二叉树中的删除

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

首先找到需要删除的节点; 如果找到了,删除它。 说明: 要求算法时间复杂度为 O(h),h 为树的高度。

示例:

root = [5,3,6,2,4,null,7]
key = 3

    5
   / \
  3   6
 / \   \
2   4   7

给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。

一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。

    5
   / \
  4   6
 /     \
2       7

另一个正确答案是 [5,2,6,null,4,null,7]。

    5
   / \
  2   6
   \   \
    4   7

链接:leetcode-cn.com/leetbook/re… 来源:力扣(LeetCode)

删除节点分为三种情况:

  1. 删除的是叶子节点,直接删除即可
  2. 删除的是非叶子结点且只有一个孩子节点,将孩子节点和删除节点做替换
  3. 删除的是非叶子结点且有两个孩子节点,可以选择它中序的前驱结点或后继节点来替换,然后删除目标节点。
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) return root;
        if (root.val > key) {
          	// 目标节点在左子树中,递归到左子树删除该节点。重新赋值root.left因为有可能root.left被删除了
            root.left = deleteNode(root.left, key);
        } else if (root.val < key) {
            root.right = deleteNode(root.right, key);
        } else {
            if (root.left == null && root.right == null) {
                // 删除的是叶子节点root赋值null即可
                root = null;
            } else if (root.left != null) {
              	// 目标节点左孩子不为空,查找中序前驱节点赋值给root.val,然后左子树中删除找到的前驱结点完成替换。
                root.val = predcessor(root);
                root.left = deleteNode(root.left, root.val);
            } else {
              	// 情况2 目标节点只有一个右孩子节点。
               //查找中序后继节点赋值给root.val,然后再右子树中删除找到的后继节点完成替换
                root.val = successor(root);
                root.right = deleteNode(root.right, root.val);
            }
        }
        return root;
    }

    /**返回前驱节点值*/
    public int predcessor(TreeNode root) {
        root = root.left;
        while (root.right != null) {
            root = root.right;
        }
        return root.val;
    }

    /**返回后继节点值*/
    public int successor(TreeNode root) {
        root = root.right;
        while (root.left != null) {
            root = root.left;
        }
        return root.val;
    }
}