这篇写一个找BST最接近的值的题吧。
题目 | 简介 |
---|---|
700. Search in a Binary Search Tree | 找确切值 |
270. Closest Binary Search Tree Value | BST中找最接近的1个 |
272. Closest Binary Search Tree Value II | BST中找最接近的k个 |
700. Search in a Binary Search Tree
先用一个找确切值的问题热身一下。一旦遇到BST,首先想怎么利用它的左小右大的性质。直觉:用recursive,如果值比root小,就分派给左孩子来处理;如果比root大,就分派给右孩子来处理……直到遇到root==value
//recursive
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if(root == null || root.val == val) {return root;}
return (val < root.val) ? searchBST(root.left, val) : searchBST(root.right, val);
}
}
也可以用iterative做:用一个当前指针cur,检查cur处的值。恰好value值和cur相等就最完美了,如果value值比cur小,挪cur到左孩子,否则挪cur到右孩子。
注意while-loop不能只用val != cur.val作为(不)结束条件,还要加上cur != null,是为了防止要找的点在tree中不存在的情况。
//iterative
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
TreeNode cur = root;
while (cur != null && val != cur.val) {
cur = (val < cur.val) ? cur.left : cur.right;
}
return cur;
}
}
270. Closest Binary Search Tree Value
和上面700思路类似,就是在访问到每个点的时候,都要计算一个gap,然后维护一个最小的gap,以及这个min-gap对应的节点。
这个思路就比较适合用iterative的方法做,因为如果用recursive,则需要传Min-gap值以及min-gap对应的节点。
就是模仿700的iterative做法,额外在每一个点增加一个算gap的操作即可。
class Solution {
public int closestValue(TreeNode root, double target) {
int res = root.val;
while(root != null){
if(Math.abs(target - root.val) < Math.abs(target - res)){
res = root.val;
}
root = root.val > target? root.left: root.right;
}
return res;
}
}
272. Closest Binary Search Tree Value II
求和target值最接近的K个节点
270是找最接近(指的是值接近,不是拓扑上近)的1个节点,272是找最接近的K个节点。
如果用recursive的思路想,要维护k个和target最接近的,你遇到某个节点,算出一个delta,也很难确定是左子树和target比较接近的多,还是右子树和target比较接近的多。于是比较难明晰地给下面的左右孩子分派任务。所以不可行。
另外一个比较直接的想法,遍历一遍这个树,每个节点算一个diff,以diff为key, 节点的reference为value,构建一个新的结构体,放进PriorityQueue里(小顶堆),然后pop出K个。缺点是需要定义新的结构(这个方法我没写代码,但可以参考grandyang这篇的方法四,c++里有个pair很好用)。
这个题是BST,嗯,还没有利用这个性质。想BST在某种意义上是有序的其实,如果in-order traverse,就可以得到有序的序列。上面想的:
- 放进PriorityQueue里:BST已经是“有序”的,如果再使用这种“排序的结构”肯定是冗余的。用in-order traversal,放进一个list里,就和PriorityQueue用处差不多。
- 确定左子树和target近的多还是右子树近的多:这个是不可能轻易知道的。但我们既然“有序”,就可以把比target小的放在一起,把比target大的放在一起,左边离得近就从左边拿出一个,右边离得近就从右边拿出一个,直到攒够K个。
于是,我们用正向的in-order traversal,把比target小的存到一个predecessor stack里(为啥用stack不用queue?因为要离target近的先出),再用反向的in-order traversal,把比target小的存到一个successor stack里。左边离得近就从左边拿出一个,右边离得近就从右边拿出一个,直到攒够K个。
这里要把正向和反向的in-order traversal写到一起(加一个boolean reverse的flag)。就是在一个普通的in-order上修改一下:
//normal in-order
private void inorder(List<Integer> result, TreeNode root) {
if (root == null) {return;}
inorder(result, root.left);//或者root.right
result.add(root.val);
inorder(result, root.right);//或者root.left
}
另外就是这个in-order不是全程走完,只要遇到比target大/小的点,就立马return。
这个其实不是很efficient的一个方法,下面解释why。但高票答案是这么做的,所以还是把代码贴一下(基本抄的jeantimex的,自己写了下)。
class Solution {
public List<Integer> closestKValues(TreeNode root, double target, int k) {
List<Integer> res = new ArrayList<>();
Stack<Integer> s1 = new Stack<>(); // predecessors(inorder traversa)
Stack<Integer> s2 = new Stack<>(); // successors(reverse-inorder traversal)
inorder(root, target, false, s1); //flattern tree to stack
inorder(root, target, true, s2);
while (k-- > 0) {
if (s1.isEmpty()) { //only s2 left
res.add(s2.pop());
} else if (s2.isEmpty()) { //only s1 left
res.add(s1.pop());
} else if (Math.abs(s1.peek()-target) < Math.abs(s2.peek()-target)) {//s1 top closer
res.add(s1.pop());
} else { //s2 top closer
res.add(s2.pop());
}
}
return res;
}
private void inorder(TreeNode root, double target, boolean reverse, Stack<Integer> stack) {
if (root == null) {return;}
inorder(reverse ? root.right : root.left, target, reverse, stack);
if ((reverse && target >= root.val) || (!reverse && target < root.val)) {return;}//early terminate
stack.push(root.val);
inorder(reverse ? root.left : root.right, target, reverse, stack);
}
}
为啥这个不是很efficient呢?因为这个用两个栈的方法,本质上就是把BST拍扁成为一个sorted array。空间复杂度上用栈和array也没有什么区别,栈可以从离target近的地方pop,同样array也可以下标任意访问(target处的下标可以在访问BST的时候顺便求得)。
刚好有个follow-up问能否减小时间复杂度:
Follow up:
Assume that the BST is balanced, could you solve it in less than O(n) runtime (where n = total nodes)?
找到了哇呀呀…生气啦~的一个用滑动窗口做的,就是相当于直接拍扁在array里找的K个最接近的。(或者grandyang这篇里的方法二)。说是先拍扁,其实不用单独把拍扁的结果用额外的memory存,就是一边遍历BST,假装是在遍历一个有序数组,然后把需要的加进result里。
下面的时间复杂度分析是抄的哇呀呀的。
方法 | 两个栈 | 滑动窗口 |
---|---|---|
时间 | O(n) inorder traversal + O(k)的POP()出K个元素 = O(N) + O(k) | O(n) 最差要整个BST搂一遍, 一般可以提前停止 |
空间 | 2个Stack,总共是O(n) + inorder的recursion memory O(lgn) = O(n) + O(lgN) | O(k) 的窗口 O(lgN)的memory stack, 因为在用recursion. |
代码是自己写的,用Deque实现的滑动窗口。新访问到的这个root.val作为右边界,如果比窗口左边界dq.peekFirst()更接近target,则扔掉左边界那个元素,把当前root加入右边界。如果长度还不够K当然是肯定加入窗口。
等平移扫描完整个窗口,最后留下的那个窗口里的K个元素,就是离target最近的K个元素。
class Solution {
public List<Integer> closestKValues(TreeNode root, double target, int k) {
Deque<Integer> result = new LinkedList<>();
inorder(result, root, target, k);
return new ArrayList<Integer>(result);
}
private void inorder(Deque<Integer> result, TreeNode root, double target, int k) {
if (root == null) {return;}
inorder(result, root.left, target, k);
if (result.size() < k) {
result.add(root.val);
} else if (Math.abs(root.val-target) < Math.abs(result.peekFirst()-target)) {//右边界(新访问到的元素)closer
result.pollFirst();
result.add(root.val);
} else {//左边界closer,新访问到的元素远,直接扔掉
return;
}
inorder(result, root.right, target, k);
}
}