知识储备
二叉搜索树(Binary Search Tree-BST)的定义:
1、对于 BST 的每一个节点 node,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。
2、对于 BST 的每一个节点 node,它的左侧子树和右侧子树都是 BST。
(这意味着BTS中没有重复值)
重要性质:
BST 的中序遍历结果是有序的(升序)。
如果输入一棵 BST,以下代码可以将 BST 中每个节点的值升序打印出来:
void traverse(TreeNode root) {
if (root == null) return;
traverse(root.left);
// 中序遍历代码位置
print(root.val);
traverse(root.right);
}
230-二叉搜索树中第K小的元素
题目
利用“BST 的中序遍历结果是有序的(升序)”的特性
public int kthSmallest(TreeNode root, int k) {
traverse(root,k);
return result;
}
int result;
int rank=0;//当前节点的排名
//利用二叉搜索树中序遍历的特性:中序遍历的节点值为升序
public void traverse(TreeNode root,int k){
if(root == null){
return;
}
traverse(root.left,k);
/**中序遍历的位置**/
rank++;
if(rank == k){
result = root.val;
return;
}
traverse(root.right,k);
}
538-BST 转化累加树
题目
还是利用“BST 的中序遍历结果是有序的(升序)”的特性,但是要把这个特性转换一下,中序遍历是先遍历左子树、再遍历右子树,在两次遍历中间访问节点值,如果先遍历右子树、再遍历左子树的话,遍历结果就应该是降序了,用额外的数据结构做下累加就可以了。
public TreeNode convertBST(TreeNode root) {
traverse(root);
return root;
}
int sum = 0;
void traverse(TreeNode root){
if(root == null){
return;
}
traverse(root.right);
sum += root.val;
root.val = sum;
traverse(root.left);
}
98-验证二叉搜索树
题目
BTS的定义是“root的整个左子树都要小于root.val,整个右子树都要大于root.val”,而不是“root的左节点小于root.val,右节点大于root.val”,所以每次递归只比较root和左右节点是不充分的,这一点要注意。
当前节点root,所能触及到的其他节点只有root.left和root.right两个,怎么能触及到其他节点呢(整个左子树、整个右子树)?
“整个左子树的值都<root.val”,也就是“左子树的最大值<root.val”;
“整个右子树都要大于root.val”,也就是“右子树的最小值>root.val”。
所以当前节点不需要触及到整个左子树、整个右子树,而是只要知道max(左子树),max(右子树)就行了,那么我们在递归函数里增加这两个参数max、min,让这两个参数不断传递下去,max、min记录了目前所能了解的节点的取值范围。
public boolean isValidBST(TreeNode root) {
return isValidBST(root,null,null);
}
boolean isValidBST(TreeNode root,TreeNode min,TreeNode max){
if(root == null){
return true;
}
if(min != null && root.val <= min.val){
return false;
}
if(max != null && root.val >= max.val){
return false;
}
return isValidBST(root.left,min,root)&&isValidBST(root.right,root,max);
}
700-二叉搜索树中的搜索
题目
在普通二叉树中寻找元素:
public TreeNode searchBST(TreeNode root, int val) {
if(root == null){
return null;
}
if(root.val == val){
return root;
}
//左子树里找该值
TreeNode leftSearchResult = searchBST(root.left,val);
//右子树里找该值
TreeNode rightSearchResult = searchBST(root.right,val);
return leftSearchResult != null?leftSearchResult:rightSearchResult;
}
然而这样完全没有用到二叉搜索树的特点,肯定不是最优解。如果我们利用上其特点,如果当前节点的值>target值,则target节点肯定位于当前节点的左子树上,只搜索左子树就行;果当前节点的值<target值,则target节点肯定位于当前节点的右子树上,只搜索右子树就行。
public TreeNode searchBST(TreeNode root, int val) {
if(root == null){
return null;
}
if(root.val == val){
return root;
}
TreeNode leftSearchResult = null;
TreeNode rightSearchResult = null;
if(root.val > val){
//左子树里找该值
leftSearchResult = searchBST(root.left,val);
}
if(root.val < val){
//右子树里找该值
rightSearchResult = searchBST(root.right,val);
}
return leftSearchResult != null?leftSearchResult:rightSearchResult;
}
可以提炼出针对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);
}
701-二叉搜索树中的插入操作
题目
将新节点插入到叶子节点后。
public TreeNode insertIntoBST(TreeNode root, int val) {
//返回新节点,以连接到叶子节点
if(root == null){
return new TreeNode(val);
}
//新节点应在root的左子树
if(root.val > val){
root.left = insertIntoBST(root.left,val);
}
//新节点应在root的右子树
if(root.val < val){
root.right = insertIntoBST(root.right,val);
}
return root;//返回当前根节点
}
450-删除二叉搜索树中的节点
删除某个节点,要先“找”再“删”,这个“找”的过程直接套用之前总结出来的BST遍历框架,所以代码的结构就是:
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;
}
剩下的事情就是“删”,节点位置的不同会导致不同的“删”法,我们来逐个分析每种情况。
情况1:要删除的节点是叶子节点
如图所示,蓝色箭头的方向代表节点值从小到大,删除叶子节点1、3、7都不会导致该BST失去BST特性,所以直接删除就行。
情况2:要删除的节点只有1个子节点
如图所示,当节点6只有右节点7时,根据BST特性,7比6左边的节点们都大,所以可以将7替换掉6;当节点7只有左节点6时,根据BST特性,6比7左边的节点们都大,所以可以直接将6替换掉7。
牢记BST特性:右子树的节点们>根节点>左子树的节点们
所以对于这种情况,就是用待删节点的子节点替换掉待删节点。
情况3:要删除的节点有2个子节点
如图所示,要删除的节点2,根据BST特性:节点A的左子树的节点们<节点A<节点A的右子树的节点们,可以推理出:由于节点2的右节点的左节点(节点3)由于位于节点2的右子树上,所以节点3>节点2的左子树的节点们;由于节点3位于节点4的左子树上,所以节点3<节点4。
也就是说,节点1<节点3<节点4,推而广之,如果待删节点为节点A,A的左节点<A的右节点的左节点<A的右节点,所以可以用“A的右节点的左节点”替换掉节点A。
如果A的右节点的左节点不是叶子节点呢?即节点3还有子节点怎么办?我们可以选择删除A的右子树的最靠左的那个节点,也满足:A的左节点<A的右子树的最靠左的那个节点<A的右节点。删除叶子节点比删除有两个子节点的节点更简单。
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null){
return null;
}
if(root.val == key){
//找到了,进行删除
//情况1:要删除的节点是叶子节点,直接删除
if(root.left == null && root.right == null){
return null;
}
//情况2:要删除的节点只有1个子节点,用子节点替代
if(root.right == null && root.left != null){
return root.left;
}
if(root.left == null && root.right != null){
return root.right;
}
//情况3:要删除的节点有2个子节点,用待删除节点的右子树的最左边的节点替代
TreeNode mostLeftNode = findMostLeftNode(root.right);
root.val = mostLeftNode.val;
//删除掉mostLeftNode
root.right = deleteNode(root.right,mostLeftNode.val);
}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 findMostLeftNode(TreeNode node){
while(node.left != null){
node = node.left;
}
return node;
}
返回值可能有点难理解,可以这么理解:递归方法传入root,返回的还是root本身,当root是待删除的值时,返回的是其他节点进行替代。
总结
230-二叉搜索树中第K小的元素 和 538-BST 转化累加树 这两个题目都跟节点值的大小有关系,所以解题思路都用了二叉搜索树的重要特性:BST 的中序遍历结果是有序的(升序)。
针对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);
}