二叉搜索树特性及相关算法详解

  1. 二叉搜索树BST

    1. 特性

      1. 对于BST的每一个节点node,左子树节点的值都比node的值要小,右子树节点的值都比node的值要大
      2. 对于BST的每一个节点node,他的左侧子树和右侧子树都是BST
      3. BST的中序遍历结果是有序的(升序)
    2. 关于LeetCode230 二叉搜索树中第K小的元素的优化

      1. 不难发现,如下解法中,算法时间复杂度为O(N),而基于BST的各种自平衡BST增删查改都是O(logN),因此如下解法过于低效

      2. BST的操作为什么这么高效

        就拿搜索某一个元素来说,BST能够在对树时间内找到该元素的根本原因还是在BST的定义里,左子树小于根节点,右子树大于根节点,因此搜索范围直接变为一半,这就给我们的优化提供了思路

      3. 想找到第k小的元素,或者说找到排名为k的元素,关键在于每个节点得知道他自己排名第几

        比如找排名为k的元素,当前节点知道自己排名第m,那么就有如下几种情况

        1. 如果 m == k,即为找到了第k个元素,返回即可
        2. 如果 k < m,即排名为k的元素在左子树,所以可以去左子树中搜索第k个元素
        3. 如果 k > m,即排名为k的元素在右子树,所以可以去右子树中搜索第k - m个元素
      4. 实现方法:在二叉树节点中维护额外信息 size即以自己为根的这棵二叉树有多少个节点

        有了size字段,对于每个节点就可以通过node.left 推导出 node 的排名

        class TreeNode {
            int val;
            // 以该节点为根的树的节点总数
            int size;
            TreeNode left;
            TreeNode right;
        }
        
  2. LeetCode230 二叉搜索树中第K小的元素

    image-20220605220535179
    1. 思路分析

      这道题思路十分明确,因为BST的中序遍历结果是有序的(升序),因此只要借助外部变量rank记录递归当前元素的排名,遍历一遍二叉树就可以得到结果

    2. 代码实现

      class Solution {
          private int res;
          private int rank;
          public 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);
              //中序遍历位置
              rank++;
              if (k == rank) {
                  res = root.val;
                  return;
              }
              traverse(root.right, k);
          }
      }
      
  3. LeetCode 538 把二叉搜索树转换为累加树

    image-20220605222413945
    1. 思路分析

      1. 题意分析

        让每个节点node的值变为原树中大于或等于node.val的值的和,例如对于节点5: 新值 = 5 + 6 + 7 + 8 = 26

      2. 思路分析

        利用二叉搜索树中序遍历为升序的特性,先遍历右子树,再遍历左子树,就是降序排列,维护一个累加和sum并更新root.val,即遍历一遍二叉树

    2. 代码实现

      class Solution {
          private int sum;
          public TreeNode convertBST(TreeNode root) {
              traverse(root);
              return root;
          }
      
          void traverse(TreeNode root) {
              if (root == null) {
                  return;
              }
              //先遍历右子树,再遍历左子树可以降序打印
              traverse(root.right);
              //中序遍历位置
              //维护累加和
              sum += root.val;
              //将二叉查找树转换为累加树
              root.val = sum;
              traverse(root.left);
          }
      }
      

    二叉搜索树的基本操作

    1. 判断BST的合法性 LeetCode98
    2. BST的搜索 LeetCode700
  4. LeetCode98 验证二叉搜索树的合法性

    image-20220605224500886
    1. 思路分析

      1. 利用二叉搜索树的特性:左子树小于根节点,右子树大于根节点,因此遍历左子树时,root节点为上界upper;遍历右子树时,root节点为下界lower
      2. 小技巧:通过使用辅助函数,增加函数参数列表,在参数重携带额外信息,将这种约束传递给子树的所有节点,这也是二叉树算法的一个小技巧
    2. 代码实现

      class Solution {
          public boolean isValidBST(TreeNode root) {
              //root节点没有上界和下界
              return isValidBST(root, null, null);
          }
      
          boolean isValidBST(TreeNode root, TreeNode lower, TreeNode upper) {
              if (root == null) {
                  return true;
              }
      				//有下界且当前值小于下界值
              if (lower != null && root.val < lower.val) return false;
              if (upper != null && root.val > upper.val) return false;
              //左子树中,将root设为上界;右子树中,将root设为下界
              return isValidBST(root.left, lower, root) && isValidBST(root.right, root, upper);
          }
      }
      
  5. LeetCode700 二叉搜索树中的搜索

    image-20220605225313138
    1. 思路分析

      类似于二分查找,根据targer和root.val的大小比较,就能排除一边

    2. 代码实现

      class Solution {
          public TreeNode searchBST(TreeNode root, int val) {
              if (root == null) {
                  return null;
              }
      
              if (root.val > val) {
                  return searchBST(root.left, val);
              }
              if (root.val < val) {
                  return searchBST(root.right, val);
              }
              return root;
          }
      }
      
  6. LeetCode701 二叉搜索树中的插入操作

    image-20220605230132023
    1. 思路分析

      对于数据结构的操作无非就是 遍历 + 访问,遍历就是 找 ,访问就是 改 ,搜索就是找的框架,在搜索的基础上加上改即可

      一旦涉及改,就类似二叉树的构造问题,函数要返回TreeNode类型,并且要对递归调用的返回值进行接收

    2. 代码实现

      class Solution {
          public TreeNode insertIntoBST(TreeNode root, int val) {
              if (root == null) {
                  return new TreeNode(val);
              }
              if (root.val > val) {
                  root.left = insertIntoBST(root.left, val);
              }
              if (root.val < val) {
                  root.right = insertIntoBST(root.right, val);
              }
              return root;
          }
      }
      
  7. LeetCode450 删除二叉搜索树中的节点
    image-20220605234902718

    1. 思路分析:删除节点有三种情况

      1. 待删除节点为叶子节点,没有左右子树,则直接删除

      2. 待删除节点只有一棵子树,则要让这棵子树接替自己的位置

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

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

        以上代码解决了1 和 2

      3. 待删除节点有左右子树

        这种情况下有两种选择,其一找到左子树中最大的那个节点,其二找到右子树中最小的那个节点接替自己,这里我偏向于使用2

        TreeNode minNode = getMin(root.right);

        然后要删除这个最小节点

        root.right = deleteNode(root.right, minNode.val);

        然后用这个最小节点接替自己

        minNode.left = root.left;

        minNode.right = root.right;

        root = minNode;

        至于为什么不直接用minNode.val 替换root.val, 这是因为在实际应用中,BST节点内部的数据域是用户自定义的,可以非常复杂,而BST作为数据结构(一个工具人),其操作应该和内部存储的数据域解耦,所以我们倾向于直接用指针操作来交换节点,根本没必要关心内部数据

    2. 框架

      TreeNode deleteNode(TreeNode root, int key) {
          if (root.val == key) {
              // 找到啦,进行删除
          } else if (root.val > key) {
              // 去左子树找
              root.left = deleteNode(root.left, key);
          } else if (root.val < key) {
              // 去右子树找
              root.right = deleteNode(root.right, key);
          }
          return root;
      }
      
    3. 代码实现

      class Solution {
          public TreeNode deleteNode(TreeNode root, int key) {
              if (root == null) {
                  return null;
              }
              if (root.val == key) {
                  //将没有子树和只有一个子树的情况处理
                  if (root.right == null) return root.left;
                  if (root.left == null) return root.right;
                  //处理有两个子树的情况
                  //取右子树中的最小值
                  TreeNode minNode = getMin(root.right);
                  //删除右子树中的最小值
                  root.right = deleteNode(root.right, minNode.val);
                  //用右子树中的最小值替换root
                  minNode.left = root.left;
                  minNode.right = root.right;
                  root = minNode;
              } else if (root.val > key) {
                  root.left = deleteNode(root.left, key);
              } else if (root.val < key) {
                  root.right = deleteNode(root.right, key);
              }
      
              return root;
          }
      
          TreeNode getMin(TreeNode node) {
              while (node.left != null) {
                  node = node.left;
              }
              return node;
          }
      }
      

    二叉搜索树的构造

  8. LeetCode96 不同的二叉搜索树

    image-20220606002509896
    1. 思路分析

      1. 对于整数n,节点值从1到n互不相同的 二叉搜索树 的个数即为以1到n为根的二叉搜索树之和

      2. 以i(1 <= i <= n)为根的二叉搜索树 = i 的左子树个数 * i 的右子树个数

        i的左子树个数为左边 i - 1个节点能构造成多少个二叉搜索树

        i的右子树个数为右边 n - i个节点能构造出多少个二叉搜索树

      3. 使用HashMap作为备忘录memo,存储n和子树数量的映射

    2. 代码实现

      class Solution {
          private HashMap<Integer, Integer> memo = new HashMap<>();
          public int numTrees(int n) {
              //0个或者1个节点也是一棵子树
              if (n == 0 || n == 1) {
                  return 1;
              }
      
              if (memo.containsKey(n)) {
                  return memo.get(n);
              }
              int count = 0;
              for (int i = 1; i <= n; i++) {
                  //左子树有多少种
                  int leftCount = numTrees(i - 1);
                  //右子树有多少种
                  int rightCount = numTrees(n - i);
                  //以i为根节点的二叉搜索树的种数一共有 leftCount * rightCount种
                  count += leftCount * rightCount;
      
              }
              //将n个节点能有多少个子树存储到memo中
              memo.put(n, count);
              return count;
          }
      }
      
  9. LeetCode95 不同的二叉搜索树II

    image-20220606002734760
    1. 思路分析

      1. 与上一题不同的二叉搜索树(有多少不同二叉搜索树)思路一致,不过增加了构造二叉树过程

      2. 具体思路:对于整数n,节点值从1到n互不相同的 二叉搜索树 的个数即为以1到n为根的二叉搜索树之和,因此穷举root节点的所有可能;

        得到所有可能的左子树,得到所有可能的右子树

        List<TreeNode> leftTree = build(lo, i - 1);
        //得到所有可能的右子树
        List<TreeNode> rightTree = build(i + 1, hi);
        

        以root为根,左右子树两两组合,然后记录每一种二叉搜索树

        for (TreeNode left : leftTree) {
                for (TreeNode right : rightTree) {
                //i作为根节点root的值
                TreeNode root = new TreeNode(i);
                root.left = left;
                root.right = right;
                //记录每一种二叉搜索树
                res.add(root);
                }
        }
        
    2. 代码实现

      class Solution {
          public List<TreeNode> generateTrees(int n) {
              if (n == 0) {
                  return new LinkedList();
              }
              return build(1, n);
          }
      
          List<TreeNode> build(int lo, int hi) {
              List<TreeNode> res = new LinkedList<>();
              if (lo > hi) {
                  res.add(null);
                  return res;
              }
              //穷举root节点的所有可能
              for (int i = lo; i <= hi; i++) {
                  //递归构造左右子树的所有合法BST
                  //得到所有可能的左子树
                  List<TreeNode> leftTree = build(lo, i - 1);
                  //得到所有可能的右子树
                  List<TreeNode> rightTree = build(i + 1, hi);
                  //给root节点穷举所有左右子树的组合,即左右子树两两组合
                  for (TreeNode left : leftTree) {
                      for (TreeNode right : rightTree) {
                          //i作为根节点root的值
                          TreeNode root = new TreeNode(i);
                          root.left = left;
                          root.right = right;
                          //记录每一种二叉搜索树
                          res.add(root);
                      }
                  }
              }
      
              return res;
          }
      }
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值