二叉搜索树(BST)题目汇总🎄(二、改变二叉搜索树)

98 阅读8分钟

本节是二叉搜索树的最后一节,主要内容围绕二叉搜索树的相关操作。

总结

  • 二叉搜索树的插入:插入位置为空节点
  • 二叉搜索树的删除:删除时要考虑被删除节点的度,不同的度操作不同
  • 二叉搜索树不仅仅可以视为一个单调递增数组反中序遍历(右中左)也可以是单调递减数组

LeetCode-701.二叉搜索树中的插入操作

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

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

如何向二叉搜索树中插入值呢?

  • 寻找过程中,如果其某孩子节点为空且可插入,再插入

插入节点并不需要遍历BST,因为插入的路径是可判断的,因此可能使用迭代的方法思路更加清晰明了

迭代

var insertIntoBST = function (root, val) {
  if (!root) return new TreeNode(val);
  //因为要返回二叉树,因此不能改变root,新增指针指向root
  let curNode = root;
  while (curNode) {
    //当前节点有空子树
    if (!curNode.left || !curNode.right) {
      //判断是否可插入
      if (!curNode.left && val < curNode.val) {
        const node = new TreeNode(val);
        curNode.left = node;
      }
      if (!curNode.right && val > curNode.val) {
        const node = new TreeNode(val);
        curNode.right = node;
      }
    }
    //此时curNode无空子树,移动curNode
    if (curNode.val > val) {
      curNode = curNode.left;
    } else {
      curNode = curNode.right;
    }
  }
  return root;
};

递归

递归分析:

  • 返回值和入参:无返回值,入参为当前节点以及待插入值
  • 终止条件:空节点时终止
  • 单层循环逻辑:类似于前序遍历
    • 中:判断当前节点是否有空子树,再判断是否可插入
    • 左/右:依赖二叉搜索树的性质,递归寻找插入位置
function instertRecursive(node, val) {
  if (!node) return;
  if (!node.left || !node.right) {
    const newNode = new TreeNode(val);
    if (!node.left && node.val > val) {
      node.left = newNode;
    } else if (!node.right && node.val < val) {
      node.right = newNode;
    }
  }

  if (node.val > val) {
    insertIntoBST(node.left, val);
  } else {
    insertIntoBST(node.right, val);
  }
}
var insertIntoBST = function (root, val) {
  if (!root) {
    return new TreeNode(val);
  }
  instertRecursive(root, val);
  return root;
};

LeetCode450.删除二叉搜索树中的节点

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

对于在二叉搜索树中删除节点,情况会相对复杂,我们先分析可能遇到的情况:

  • 如果要删除的节点不存在,则直接返回原树
  • 存在:
    • 删除节点的度为0,即叶子节点,可直接删除
    • 删除节点的度为1,将其存在的子树替代目标节点
    • 删除节点的度为2,有两种方法:
      • 方法一:
        • 将该节点的左子树的最大值或右子树的最小值删除(一定是一个度为1的节点,因此递归即可)
        • 然后使用被删除节点值覆盖当前节点值
      • 方法二:
        • 将该节点的左子树挂到右子树的最小值的左子树上
        • 此时当前节点变为一个度为1的节点,将目标结点的右子树替代目标结点

这里再给出方法一和方法二的图示过程:

方法一:

image.png

方法二:

image.png

分析了这么多,终于可以写代码了:

方法一

代码中如何删除某个节点?

  • 删除时通常需要被删除节点的父节点来辅助删除操作,因此我们在寻找当前节点时,要保存其父节点(在二叉搜索树的搜索时,保存父节点实际就是上一个遍历的结点)
  • 如果删除的是根节点,那么此时无父节点,因此需要注意删除前的逻辑判断
var deleteNode = function (root, key) {
  if (!root) return root;
  //寻找到目标节点和其父节点
  let curNode = root;
  let parentNode = null;
  while (curNode) {
    if (curNode.val === key) {
      break;
    }
    parentNode = curNode;
    if (curNode.val > key) {
      curNode = curNode.left;
    } else {
      curNode = curNode.right;
    }
  }

  //没找到目标节点
  if (!curNode) return root;
  if (!curNode.left && !curNode.right) {//curNode 的度为0  通过parentNode将其删除
    if (root === curNode) {//如果删除节点为根节点且为叶子节点,此时parentNode为null,根节点直接置为null
      root = null;
    }
    else if (parentNode.left === curNode) parentNode.left = null;
    else if (parentNode.right === curNode) parentNode.right = null;
  } else if (!curNode.left || !curNode.right) {//curNode 的度为1
    if (root === curNode) {
      root = root.left ? root.left : root.right;
    } else if (parentNode.left === curNode) {
      parentNode.left = curNode.left ? curNode.left : curNode.right;
    } else if (parentNode.right === curNode) {
      parentNode.right = curNode.left ? curNode.left : curNode.right;
    }
  }

  //curNode 度为2
  if (curNode.left && curNode.right) {
    //找到其右子树最小元素
    let temp = curNode.right;
    while (temp.left) {
      temp = temp.left;
    }
    //递归删除最小元素
    deleteNode(root, temp.val);
    //用temp覆盖curNode
    curNode.val = temp.val;
  }
  return root;
};

方法二

var deleteNode = function (root, key) {
  if (!root) return root;
  if (root.val > key) {
    root.left = deleteNode(root.left, key);
  } else if (root.val < key) {
    root.right = deleteNode(root.right, key);
  }
  if (root.val === key) {
    //叶子节点
    if (!root.left && !root.right) {
      return null;
    }
    //只有一个子树
    if (!root.left && root.right) {
      return root.right;
    } else if (root.left && !root.right) {
      return root.left;
    }

    //两个都在 将左子树挂到右子树的最小值的左子树上,再将当前节点置为右子树
    if (root.left && root.right) {
      let rightMinNode = root.right;
      while (rightMinNode.left) {
        rightMinNode = rightMinNode.left;
      }
      rightMinNode.left = root.left;
      root.left = null;
      root = root.right;
      return root;
    }
  }
  return root;
};

总的来说,方法一的思路更加清晰简单,而方法二的代码更加干净。

LeetCode-669.修剪二叉搜索树

给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。

解决问题的关键在于分析出我们在修建过程中会遇到的情况:

  • 如果当前结点大于范围,那么该节点与其右子树是要删减的,但其左子树不确定---继续修剪左子树,右子树无用
  • 如果当前结点小于范围,那么该节点与其左子树是要删减的,但其右子树不确定---继续修剪右子树,左子树无用
  • 如果当前结点在范围内,左右子树都不确定---左右子树都继续修剪

如何修剪?

  • 实际上利用递归就可以完成修剪,我们可以将修剪完成的左/右子树赋值给该节点左右子树;

递归

递归分析:

  • 返回值和入参:返回的是修剪好的子树根节点,入参为当前结点以及范围
  • 终止条件:
    • 空节点 null
  • 单层循环逻辑:
    • 如果当前结点在范围外,递归修剪其左子树右子树,并只返回修建好的子树,因为另一边是无用的
    • 如果当前节点在范围内,递归修剪其左子树右子树,并且将修剪好的左子树和右子树分别挂到当前结点上

代码实现:

var trimBST = function (root, low, high) {
  if (!root) return null;
  //在范围左边,说明其右子树中可能存在有效值,修剪右子树并返回
  if (root.val < low) {
    return trimBST(root.right, low, high);
  }
  //在范围右边,说明其左子树中可能存在有效值,修剪左子树并返回
  if (root.val > high) {
    return trimBST(root.left, low, high);
  }
  //在范围内的值,其左右都需要修剪,修剪完成后再挂上
  root.left = trimBST(root.left, low, high);
  root.right = trimBST(root.right, low, high);
  return root;
};

LeetCode-108.将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

思路

这不就是构造二叉树吗?相比做过二叉树构造那一章节题目的同学会觉得很简单,这里除了构造二叉树,还有就是维持高度平衡,然而对于有序数组,每次取中间的结点作为根,就不会打破平衡了。这么一说是不是很简单了。

直接上代码:

function buildTreeRecursive(nums) {
  if (!nums.length) return null;

  //创建根节点
  const mid = Math.floor(nums.length / 2);//中间节点索引
  const rootNode = new TreeNode(nums[mid]);

  //将nums从mid处分成左子树和右子树数组
  const leftNums = nums.slice(0, mid);
  const rightNums = nums.slice(mid + 1);

  //递归连接左右子树
  rootNode.left = buildTreeRecursive(leftNums);
  rootNode.right = buildTreeRecursive(rightNums);

  return rootNode;
}
var sortedArrayToBST = function (nums) {
  return buildTreeRecursive(nums);
};

LeetCode-538.把二叉搜索树转换为累加树

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

起初我想的是,那不就是每个节点的新值是右子树的和吗?然后忽然想起来,对于一个节点的左子树上的节点,比他大的值不仅仅是右子树,还有其右边的父节点,那就有点难办了。

转换思路,我们说过二叉搜索树中序遍历有序: 假设我们现在有一个中序遍历结果:[1,2,5,6],那么每个节点的新值等于什么?

  • 从左向右看就是:1的新值是1+2+5+6,2的新值是2+5+6....
  • 从右向左看就是:6的新值是6,5的新值是6+5,2的新值是(6+5)+2....

发现了吗,如果从右向左看,该节点的新值等于上一个值加当前节点值(上一个值在上一次已经被更新了)

那么问题就变得简单了,我们需要解决的问题是:

  • 从右向左,也就是遍历出递减结果--------反中序呗,右中左就好了
  • 保存上一个值,之前我们专门讲过,很简单吧----中间节点处理逻辑后更新preNode

递归

var convertBST = function (root) {
  let preNode = null;
  //递归函数
  function reverseInorderRecursive(node) {
    if (!node) return;
    //右
    reverseInorderRecursive(node.right);
    //中
    if (preNode) {
      node.val += preNode.val;
    }
    //更新preNode
    preNode = node;
    //左
    reverseInorderRecursive(node.left);
  }

  reverseInorderRecursive(root);
  return root;
};