BST-找最接近的值

这篇写一个找BST最接近的值的题吧。

题目简介
700. Search in a Binary Search Tree找确切值
270. Closest Binary Search Tree ValueBST中找最接近的1个
272. Closest Binary Search Tree Value IIBST中找最接近的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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值