[Leetcode] 一文玩转二叉搜索树

前言

二叉搜索树的结点定义和普通的二叉树一样,参见 一文玩转二叉树的遍历。构造它的目的是为了提高查找、插入和删除的速度。

性质
  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
  • 若它的右子树不空,则右子树上所有结点的值均小于它的根结点的值
  • 它的左、右子树也分别为二叉搜索树
基本操作
查找
/* 递归 */
public TreeNode searchRecursively(TreeNode root, int val) {
	if (root == null) return null;
	if (val < root.val) {
		return searchRecursively(root.left, val);
	} else if (val > root.val) {
		return searchRecursively(root.right, val);
	} else {
		return root;
	} 
}

/* 迭代 */
public TreeNode searchIteratively(TreeNode root, int val) {
	while (root != null) {
		if (val < root.val) {
			root = root.left;
		} else if (val > root.val) {
			root = root.right;
		} else {
			return root;
		}
	}
	return null;
}
插入
/* 递归 */
public TreeNode insertRecursively(TreeNode root, int val) {
	if (root == null) return new TreeNode(val);
	if (val < root.val) {
		root.left = insertRecursively(root.left, val);
	} else if (val > root.val) {
		root.right = insertRecursively(root.right, val);
	}
	return root;	
}

/* 迭代 */
public TreeNode insertIteratively(TreeNode root, int val) {
	if (root == null) return new TreeNode(val);
	TreeNode curNode = root;
	while (curNode != null) {
		if (val < root.val) {
			if (curNode.right == null) {
				curNode.right = new TreeNode(val);
				return root;
			} else {
				curNode = curNode.right;
			}
		} else if (val > root.val) {
			if (curNode.left == null) {
				curNode.left = new TreeNode(val);
				return root;
			} else {
				curNode = curNode.left;
			}
		}
	}
	return root;
}
删除
/* 递归 */
public TreeNode deleteRecursively(TreeNode root, int val) {
	if (root == null) return null;
	if (val < root.val) {
		root.left = deleteRecursively(root.left, val);
	} else if (val > root.val) {
		root.right = deleteRecursively(root.right, val);
	} else {
		// 要删除的结点只有左子树,则将右子树整个移动到当前位置
		if (root.left == null) return root.right;
		// 要删除的结点只有右子树,则将左子树整个移动到当前位置 
		if (root.right == null) return root.left; 
		// 要删除的结点有两个子树,则有两种方式
		// (1) 使用前驱结点(左子树中最大的结点)代替当前结点
		// (2) 使用后继结点(右子树中最小的结点)代替当前结点
		// 本例中使用方式 (2)
		TreeNode curNode = root.right;
		while (curNode.left != null) {
			curNode  = curNode.left; // 找到右子树中最小的结点
		}
		root.val = curNode.val;
		root.right = deleteRecursively(root.right, curNode.val);
	}		
}

/* 迭代 */
public TreeNode deleteIteratively(TreeNode root, int val) {
	// preNode 记录上一个位置的结点
	TreeNode preNode = null, curNode = root;
	while (curNode != null && curNode.val != val) {
		preNode = curNode;
		if (val < curNode.val) {			
			curNode = curNode.left;
		} else {
			curNode = curNode.right;
		}
	}
	// 没有找到要删除的结点
	if (curNode == null) return root; 
	// 如果 preNode 不存在,说明要删除的是根结点
	if (preNode == null) return delete(curNode);
	// 如果要删除的结点在 preNode 左子树中,那么 preNode 的左子结点连上删除后的结点
	if (preNode.left != null && preNode.left.val == val) {
		preNode.left = delete(curNode);
	} 
	// 反之 preNode 的右子结点连上删除后的结点
	else {
		preNode.right = delete(curNode);
	}
	return root;
}

private TreeNode delete(TreeNode node) {
	// 如果左右子结点都不存在,那么返回空
	if (node.left == null && node.right == null) return null;
	// 如果有一个不存在,那么我们返回另一个存在的
	if (node.left == null) return node.right;
	if (node.right == null) return node.left;
	TreeNode preNode = node, curNode = node.right;
	// 找到需要删除结点的右子树中的最小值
	while (curNode.left != null) {
		preNode = curNode;
		curNode = curNode.left;
	}
	// 把最小值赋值给要删除节点
	node.val = curNode.val;
	if (preNode == node) {
		// 如果要删除结点的右子结点没有左子结点了的话,那么最小值的右子树直接连到要删除节点的右子结点上即可
		// 因为此时原本要删除的结点的值已经被最小值替换了,所以现在其实是要删掉最小值结点
		node.right = curNode.right;
	} else {
		// 否则就把最小值结点的右子树连到其父节点的左子结点上
		preNode.left = curNode.right;
	}
	return node;
}
具体问题
二叉搜索树结点最小距离

给定一个二叉搜索树的根结点,返回树中任意两节点的差的最小值。

/* 递归 */
private int preVal = -1, minDiff = Integer.MAX_VALUE;

public int minDiffInBST(TreeNode root) {
	inorder(root);
	return minDiff;			
}

private void inorder(TreeNode root) {
	if (root == null) return;
	inorder(root.left);
	if (preVal != -1) minDiff = Math.min(minDiff, root.val - preVal); // 中序遍历二叉搜索树时后结点一定比前面的值大,所以不需要绝对值运算
	preVal = root.val;
	inorder(root.right);
}

/* 迭代 */
public int minDiffInBST(TreeNode root) {
	int preVal = -1, minDiff = Integer.MAX_VALUE;
	Deque<TreeNode> stack = new LinkedList<>();
	TreeNode curNode = root;
	while (!stack.isEmpty() || curNode != null) {
		if (curNode != null) {
			stack.push(curNode);
			curNode = curNode.left;
		} else {
			curNode = stack.pop();
			if (preVal != -1) minDiff = Math.min(minDiff, curNode.val - preVal);
			preVal = curNode.val;
			curNode = curNode.right;
		}
	}
	return minDiff;
}

还有和这题重复的一道题 二叉搜索树的最小绝对差。使用类似解法的还有 把二叉搜索树转换为累加树二叉搜索树中的众数二叉搜索树中第K小的元素二叉搜索树中的顺序后继

二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
最近公共祖先的定义为:对于有根树 T T T 的两个结点 p p p q q q,最近公共祖先表示为一个结点 x x x,满足 x x x p p p q q q 的祖先且 x x x 的深度尽可能大(一个节点也可以是它自己的祖先)。

/* 递归 */
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    // 因为是二叉搜索树,如果根结点比 p,q 中较大的结点大,那必然大于另一个
    // 同理,如果比 p,q 中较小的结点小,那必然小于另一个
    // 这两种情况下 p,q 肯定在根结点的同一侧子树内
    if (root.val > Math.max(p.val, q.val)) {
        return lowestCommonAncestor(root.left, p, q);
    } else if(root.val < Math.min(p.val, q.val)) {
        return lowestCommonAncestor(root.right, p, q);
    } 
	// 若根结点介于两者之间,说明 p,q 在不同的子树内,最近的公共祖先就是根结点
	else {
        return root;
    }  
}

/* 迭代 */
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    while (true) {
        if (root.val > Math.max(p.val, q.val)) {
            root = root.left;
        } else if(root.val < Math.min(p.val, q.val)) {
            root = root.right;
        } else {
            break;
        }
    }
    return root;
}

和这道题类似的还有 二叉搜索树的范围和修剪二叉搜索树

最接近的二叉搜索树值 II

给定一个不为空的二叉搜索树和一个目标值 t a r g e t target target,请在该二叉搜索树中找到最接近目标值 t a r g e t target target k k k 个值。
注意:

  • 给定的目标值 t a r g e t target target 是一个浮点数
  • 你可以默认 k k k 值永远是有效的,即 k k k 不大于总结点数
  • 题目保证该二叉搜索树中只会存在一种 k k k 个值集合最接近目标值
public List<Integer> closestKValues(TreeNode root, double target, int k) {
	LinkedList<Integer> resultList = new LinkedList<>();
	inorder(root, target, k, resultList);
	return resultList;
}

private void inorder(TreeNode root, double target, int k, LinkedList<Integer> resultList) {
	if (root == null) return;
	inorder(root.left, target, k, resultList);
	// 当遍历到一个结点时,如果此时结果集不到 k 个,直接将此结点值加入结果集
	if (resultlist.size() < k) {
		resultList.add(root.val);	
	} 
	// 如果该结点值和 target 差的绝对值小于结果集中首元素和 target 差的绝对值
	// 说明当前值更靠近 target,则将结果集中首元素删除,末尾加上当前结点值
	else if (Math.abs(root.val - target) > Math.abs(resultList.getFirst() - target)) {
		resultList.removeFirst();
		resultList.add(root.val);
	} 
	// 反之,说明当前值比结果集中所有的值都更偏离 target
	// 由于二叉搜索树中序遍历的性质,之后的值会更加的偏离(更大),所以直接返回
	else {
		return;
	}
	inorder(root.right, target, k, resultList);
}
将二叉搜索树转化为排序的双向链表

将一个二叉搜索树就地转化为一个已排序的双向循环链表。可以将左右孩子指针作为双向循环链表的前驱和后继指针。为了让您更好地理解问题,以下面的二叉搜索树为例:
在这里插入图片描述
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
在这里插入图片描述
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
下图显示了转化后的二叉搜索树,实线表示后继关系,虚线表示前驱关系。
在这里插入图片描述

/* 中序递归 */
Node* treeToDoublyList(Node* root) {
	if (!root) return NULL;
	// head 记录最左结点,pre 记录上一个遍历到的结点
	Node* head = NULL, *pre = NULL;
	inorder(root, pre, head);
	pre->right = head;
	head->left = pre;
	return head;	
}

void inorder(Node* node, Node* &pre, Node* &head) {
	if (!node) return NULL;
	// 对左子结点调用递归,这样会先一直递归到最左结点
	inorder(node->left, pre, head);
	if (!head) {
		// head 为空的话,说明当前就是最左结点,赋值给 head 和 pre
		head = node;
		pre = node;
	} else {
		// 对于之后的遍历到的结点,那么可以和 pre 相互连接上
		// 然后 pre 赋值为当前结点 node
		pre->right = node;
		node->left = pre;
		pre = node;
	}
	inorder(node->right, pre, head);
}

/* 分治 */
Node* treeToDoublyList(Node* root) {
	if (!root) return NULL;
	// 递归了两个各自循环的有序双向链表
	Node *leftHead = treeToDoublyList(root->left);
	Node *rightHead = treeToDoublyList(root->right);
	// 然后把根结点跟左右子结点断开,将其左右指针均指向自己
	// 这样就形成了一个单个结点的有序双向链表
	root->left = root;
	root->right = root;
	return connect(connect(leftHead, root), rightHead);
}

Node* connect(Node* node1, Node* node2) {
	// 首先判空,若一个为空,则返回另一个
	if (!node1) return node2;
	if (!node2) return node1;
	// 如果两个都不为空
	Node *tail1 = node1->left, *tail2 = node2->left;
	// 把第一个链表的尾结点的右指针链上第二个链表的首结点
	tail1->right = node2;
	// 把第二个链表的首结点的左指针链上第一个链表的尾结点
	node2->left = tail1;
	// 把第二个链表的尾结点的右指针链上第一个链表的首结点
	tail2->right = node1;
	// 把第一个链表的首结点的左指针链上第二个链表的尾结点
	node1->left = tail2;
	return node1;
}
总结

二叉搜索树就讨论到这里了,总体来说解题的思路和普通二叉树无异。特别的,由于二叉搜索树中序遍历有序性的特点,在解题时我们应该首先考虑利用这一性质来进行剪枝,加快遍历的速度。细心的读者不难看出,对于树类的总结,博主在题目中都给出了递归和迭代两种实现。因为在面试中,往往会同时考察多种实现方式来判断你是否掌握树的知识点。为此,博主提供了两种方式来作为参考。对于树类问题,递归的解答往往比较清晰,代码更简洁。但我们同时也应该掌握迭代的写法,帮助我们更好地理解树的结构。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值