前言
二叉搜索树的结点定义和普通的二叉树一样,参见 一文玩转二叉树的遍历。构造它的目的是为了提高查找、插入和删除的速度。
性质
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
- 若它的右子树不空,则右子树上所有结点的值均小于它的根结点的值
- 它的左、右子树也分别为二叉搜索树
基本操作
查找
/* 递归 */
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;
}
总结
二叉搜索树就讨论到这里了,总体来说解题的思路和普通二叉树无异。特别的,由于二叉搜索树中序遍历有序性的特点,在解题时我们应该首先考虑利用这一性质来进行剪枝,加快遍历的速度。细心的读者不难看出,对于树类的总结,博主在题目中都给出了递归和迭代两种实现。因为在面试中,往往会同时考察多种实现方式来判断你是否掌握树的知识点。为此,博主提供了两种方式来作为参考。对于树类问题,递归的解答往往比较清晰,代码更简洁。但我们同时也应该掌握迭代的写法,帮助我们更好地理解树的结构。