Java:如何在BST中找到最接近目标的k值?

时间:2016-12-12 07:22:32

标签: java algorithm traversal inorder

我正在研究如何在BST中找到最接近目标的k值,并且遵循以下规则实施:

  

给定非空二进制搜索树和目标值,在BST中找到最接近目标的k值。

     

注意:

     

给定目标值是浮点数。   您可以假设k始终有效,即:k≤总节点数。   保证BST中只有一组唯一的k值最接近目标。假设BST是平衡的。

实施的想法是:

  

将最近节点的前辈和后继者与目标进行比较,我们可以使用两个堆栈来跟踪前辈和后继者,然后就像我们在合并排序中所做的那样,我们比较并选择最接近目标的一个并放置它到结果列表。如我们所知,inorder遍历为我们提供了已排序的前导,而反向顺序遍历为我们提供了已排序的后继。

代码:

import java.util.*;

class TreeNode {
    int val;
    TreeNode left, right;

    TreeNode(int x) { 
        val = x; 
    }
}


public class ClosestBSTValueII {
    List<Integer> closestKValues(TreeNode root, double target, int k) {
          List<Integer> res = new ArrayList<>();

          Stack<Integer> s1 = new Stack<>(); // predecessors

          Stack<Integer> s2 = new Stack<>(); // successors

          inorder(root, target, false, s1);
          inorder(root, target, true, s2);

          while (k-- > 0) {
            if (s1.isEmpty()) {
              res.add(s2.pop());
            } else if (s2.isEmpty()) {
              res.add(s1.pop());
            } else if (Math.abs(s1.peek() - target) < Math.abs(s2.peek() - target)) {
              res.add(s1.pop());
            } else {
              res.add(s2.pop());
            }
          }

          return res;
        }

    // inorder traversal
    void inorder(TreeNode root, double target, boolean reverse, Stack<Integer> stack) {

      if (root == null) {
          return;
      }

      inorder(reverse ? root.right : root.left, target, reverse, stack);
      // early terminate, no need to traverse the whole tree
      if ((reverse && root.val <= target) || (!reverse && root.val > target)) {
          return;
      }
      // track the value of current node
      stack.push(root.val);
      inorder(reverse ? root.left : root.right, target, reverse, stack);
    }

    public static void main(String args[]) {
        ClosestBSTValueII cv = new ClosestBSTValueII();

        TreeNode root = new TreeNode(53);
        root.left = new TreeNode(30);
        root.left.left = new TreeNode(20);
        root.left.right = new TreeNode(42);
        root.right = new TreeNode(90);
        root.right.right = new TreeNode(100);

        System.out.println(cv.closestKValues(root, 40, 2));
    }
}

我的问题是,有两个堆栈的原因是什么,以及如何有序的方法?每个人的目的是什么?不会用一个堆栈遍历它就足够了吗?

具有reverse布尔值的重点是什么,例如inorder(reverse ? ...);?在if ((reverse && root.val <= target) || (!reverse && root.val > target))的情况下,为什么要提前终止?

提前感谢您,并接受回答/投票。

2 个答案:

答案 0 :(得分:0)

你需要两个堆栈的原因是你必须在两个方向上遍历树,你必须将每个堆栈的当前值与你正在搜索的值进行比较(你可能最终得到的k值大于搜索到的值)值,或k / 2更大,k / 2更低)。

我认为你应该使用堆栈的TreeNodes而不是堆栈的Integer;你可以避免递归。

更新:

我在算法中看到两个阶段:

1)找到树中最接近的值,这将同时构建初始堆栈。

2)制作一个堆栈的副本,​​向后移动一个元素,这将给你第二个堆栈;然后迭代最多k次:看看每个堆栈顶部的两个元素中哪个最接近搜索值,将其添加到结果列表,然后向前或向后移动堆栈。

更新2:一个小代码

public static List<Integer> closest(TreeNode root, int val, int k) {
    Stack<TreeNode> right = locate(root, val);
    Stack<TreeNode> left = new Stack<>();
    left.addAll(right);
    moveLeft(left);
    List<Integer> result = new ArrayList<>();
    for (int i = 0; i < k; ++i) {
        if (left.isEmpty()) {
            if (right.isEmpty()) {
                break;
            }
            result.add(right.peek().val);
            moveRight(right);
        } else if (right.isEmpty()) {
            result.add(left.peek().val);
            moveLeft(left);
        } else {
            int lval = left.peek().val;
            int rval = right.peek().val;
            if (Math.abs(val-lval) < Math.abs(val-rval)) {
                result.add(lval);
                moveLeft(left);
            } else {
                result.add(rval);
                moveRight(right);
            }
        }
    }
    return result;
}

private static Stack<TreeNode> locate(TreeNode p, int val) {
    Stack<TreeNode> stack = new Stack<>();
    while (p != null) {
        stack.push(p);
        if (val < p.val) {
            p = p.left;
        } else {
            p = p.right;
        }
    }
    return stack;
}

private static void moveLeft(Stack<TreeNode> stack) {
    if (!stack.isEmpty()) {
        TreeNode p = stack.peek().left;
        if (p != null) {
            do {
                stack.push(p);
                p = p.right;
            } while (p != null);
        } else {
            do {
                p = stack.pop();
            } while (!stack.isEmpty() && stack.peek().left == p);
        }
    }
}

private static void moveRight(Stack<TreeNode> stack) {
    if (!stack.isEmpty()) {
        TreeNode p = stack.peek().right;
        if (p != null) {
            do {
                stack.push(p);
                p = p.left;
            } while (p != null);
        } else {
            do {
                p = stack.pop();
            } while (!stack.isEmpty() && stack.peek().right == p);
        }
    }
}

更新3

  

不会用一个堆栈遍历它就足够了吗?

     

具有反向布尔值的重点是什么,例如for   inorder(反向?...);?在if((反向&amp;&amp; root.val。)的情况下   &lt; = target)|| (!reverse&amp;&amp; root.val&gt; target)),为什么要终止   早?

我不知道你在哪里得到了你提出的解决方案,但总而言之,它建立了两个整数列表,一个是直接顺序,一个是相反的顺序。当达到搜索值时,它会“提前”终止。这种解决方案听起来非常低效,因为它需要遍历整棵树。当然,我的情况要好得多,而且符合既定规则。

答案 1 :(得分:0)

您找到的算法的想法非常简单。它们只是从应该插入target的地方按顺序遍历树。他们使用两个堆栈来存储前辈和后继者。让我们以树为例:

     5
    / \
   3   9
  / \   \
 2   4   11 

让目标为8。完成所有inorder方法调用后,堆栈将为:s1 = {2, 3, 4, 5}s2 = {11, 9}。如您所见,s1包含targets2的所有后续版本的所有前任。此外,两个堆栈都以某种方式排序,每个堆栈的top比堆栈中的所有其他值更接近target。因此,我们可以轻松找到k最接近的值,只需始终比较堆栈的顶部,然后弹出最接近的值,直到我们得到k值。算法的运行时间为O(n)

现在谈谈你的问题。我不知道如何有效地使用唯一的堆栈实现此算法。堆栈的问题是我们只能访问它的顶部。但是用一个数组实现算法非常容易。让我们通常按顺序遍历树。对于我的例子,我们将获得:arr = {2, 3, 4, 5, 9, 11}。然后,我们可以将lr索引放置在距离两边最近的目标值的位置:l = 3r = 4arr[l] = 5arr[r] = 9 )。剩下的就是始终比较arr[l]arr[r]并选择要添加到结果中的内容(绝对相同,就像两个堆栈一样)。这个算法也需要O(n)次操作。

他们对这个问题的解决方法在我看来有点难以理解,尽管它相当优雅。

我想在另一个运行时间引入另一种解决问题的方法。该算法将花费O(k*logn)时间,这对于小k更好,对于更大的算法比以前的算法更差。

让我们在TreeNode类中存储一个指向父节点的指针。然后我们可以在O(logn)时间(if you don't know how)中轻松找到树中任何节点的前导或后继。所以,让我们首先在树的前任和目标的后继中找到(不做任何遍历!)。然后执行与堆栈相同的操作:比较前一个\ successor,选择最接近的一个,并选择最接近它的前任\ successor。

我希望,我回答了你的问题并理解了我的解释。如果没有,请随时询问!