【问题标题】:Why store the points in a binary tree?为什么将点存储在二叉树中?
【发布时间】:2015-02-20 21:35:30
【问题描述】:

本题涉及软件算法,来自On topic

我正在处理来自Amazon Software Question 的面试问题, 具体
“给定一组点 (x,y) 和一个整数“n”,返回接近原点的 n 个点”

这是此问题的示例高级伪代码答案,来自Sample Answer
第 1 步:设计一个名为 point 的类,它具有三个字段 - int x、int y、int distance
第 2 步:对于给定的所有点,找出它们与原点之间的距离
步骤 3:将值存储在二叉树中
第 4 步:堆排序
第 5 步:打印二叉树的前 n 个值

我同意第 1 步和第 2 步,因为就面向对象设计而言,拥有一个数据软件包 Point 封装 x、y 和距离字段是有意义的。Ensapsulation

有人可以解释从 3 到 5 的设计决策吗?

这是我将如何执行 3 到 5 步的方法
第 3 步:将所有点存储在一个数组中
第 4 步:根据距离对数组进行排序(我在这里使用了一些内置排序,例如 Arrays.Sort
第 5 步:将数组按升序排序,打印前 n 个值

为什么该响应的作者使用更复杂的数据结构,二叉树,而不是像我使用的数组这样简单的东西?我知道二叉树是什么 - 具有两个指针的节点的分层数据结构。在他的算法中,您是否必须使用 BST?

【问题讨论】:

  • 也许他们的意思是堆排序通常使用的隐式二叉树?
  • 正如@harold 所说,听起来他们的意思是您从二进制堆中插入和提取,该堆的B​​ig-O 为log(n),这比您使用的n log (n) 更好排序数组方法。
  • @jpriebe 但你必须插入 n 项......所以它仍然是 n log n
  • 可能是因为 Arrays.sort 的时间和空间复杂度比堆排序(最坏情况下为 nlogn)要多。 stackoverflow.com/a/22571601/2344337
  • 你说得对,阿德里安,我的错

标签: java arrays algorithm sorting tree


【解决方案1】:

首先,我不会说拥有Point(x, y, distance) 是好的设计或封装。 distance 并不是一个点的一部分,它可以从 xy 计算出来。在设计方面,我肯定会有一个函数,即来自Point 的静态方法或一个辅助类Points

double distance(Point a, Point b)

那么对于具体的问题,我其实很同意你的解决方案,将数据放入一个数组中,对这个数组进行排序,然后先提取N。 该示例可能暗示的是,堆排序实际上经常使用二叉树结构要排序的数组中,如here 所述:

堆通常放置在一个完全二叉树布局的数组中。

当然,如果到原点的距离没有存储在Point 中,出于性能原因,必须将其与数组中对应的Point 对象或任何允许获取Point对象距离排序后的距离(参考、索引),例如

List<Pair<Long, Point>> distancesToOrigin = new ArrayList<>();

Comparator&lt;Pair&lt;Long, Point&gt;&gt;排序

【讨论】:

  • 计算一次距离并将其存储为字段而不是每次需要时都从 x 和 y 重新计算不是更好吗?
  • 更好的是,您可以使用距离的平方来比较它们与原点的距离。保存平方根。
  • @committedandroider,这正是对distancePoint 中的Pair 进行排序的目的。每个Point 只计算一次距离。这样,您实际上可以执行@harold 建议的操作:使用平方距离而不仅仅是距离。它节省了一些计算。
  • @AbbéRésina 那对是做什么的?
  • 这只是在排序时将distance 链接到Point 的便捷方式:Comparator 将仅使用距离字段。您还可以将计算出的距离缓存作为Pointtransient 字段,并通过distanceFromOrigin() 方法计算/访问。
【解决方案2】:

没有必要使用 BST。但是,当需要自排序的结构时,使用 BST 是一种很好的做法。我认为不需要同时使用 BST 并对其进行堆排序(不知何故)。您可以只使用 BST 并检索前 n 个点。您还可以使用数组,对其进行排序并使用前 n 个点。 如果要对 Point 类型的数组进行排序,则可以实现接口 Comparable(Point 将使用该接口)并重载默认方法。 您永远不必选择任何数据结构,但通过确定您的需求,您也可以轻松确定最佳结构。

【讨论】:

  • 为了性能,你会选择二叉搜索树还是二叉树和堆排序?
  • 我几乎总是将二叉树实现为 BST,具体取决于搜索条件。 BST 是一个字典,因此它有利于数据的插入、删除和搜索操作。在这种情况下,困境宁愿(在我看来)在字典和顺序数据结构(如数组或数组列表)之间。在这种情况下,我会使用数组列表,因为您不知道需要存储的确切点数。
【解决方案3】:

本文中描述的方法比此类问题所需的复杂。正如您所指出的,按距离进行简单排序就足够了。但是,为了帮助解释您对示例答案作者试图获得的内容的困惑,不妨考虑 k 最近邻 问题,该问题可以通过 k-d tree 解决,该结构将空间分区应用于kd 数据集。对于二维空间,这确实是一棵二叉树。这棵树本质上是排序的,不需要任何“堆排序”。

应该注意构建 k-d 树将花费 O(n log n),并且只有在您需要对结构进行重复的最近邻搜索时才值得付出代价。如果您只需要执行一次搜索即可从原点找到 k 个最近的邻居,则可以通过简单的 O(n) 搜索来完成。

如何直接从 Wiki 构建 k-d 树:

向 k-d 树添加新点的方式与向任何其他搜索树添加元素的方式相同。首先,遍历树,从根开始,根据要插入的点是在分割平面的“左侧”还是“右侧”,移动到左侧或右侧。到达子节点应位于的节点后,将新点添加为叶节点的左子节点或右子节点,同样取决于节点分割平面的哪一侧包含新节点。

以这种方式添加点会导致树变得不平衡,从而导致树性能下降。树性能下降的速率取决于添加的树点的空间分布,以及添加的点数与树的大小有关。如果树变得过于不平衡,则可能需要重新平衡以恢复依赖树平衡的查询的性能,例如最近邻搜索。

一旦构建了树,您可以在 O(k log n) 时间内找到某个点(在您的情况下为原点)的 k 个最近邻居。

直接来自维基:

在k-d树中搜索最近邻的过程如下:

  1. 从根节点开始,算法以递归方式向下移动树,与插入搜索点时的方式相同(即,它向左或向右移动取决于该点是小于还是大于分割维度中的当前节点)。
  2. 一旦算法到达叶节点,它就会将该节点点保存为“当前最佳”
  3. 算法展开树的递归,在每个节点执行以下步骤:
    1. 如果当前节点比当前最佳节点更接近,则它成为当前最佳节点。
    2. 算法检查分割平面的另一侧是否有任何点比当前最佳点更靠近搜索点。从概念上讲,这是通过将分割超平面与搜索点周围的超球面相交来完成的,该超球面的半径等于当前最近的距离。由于超平面都是轴对齐的,这被实现为一个简单的比较,以查看搜索点和当前节点的分割坐标之间的差异是否小于从搜索点到当前最佳位置的距离(整体坐标)。
      1. 如果超球面穿过平面,则平面的另一侧可能有更近的点,因此算法必须从当前节点向下移动树的另一个分支,以寻找更近的点,遵循与以下相同的递归过程整个搜索。
      2. 如果超球面不与分割平面相交,则算法将继续向上遍历树,并消除该节点另一侧的整个分支。
  4. 当算法完成根节点的这个过程时,搜索就完成了。

这是一个非常棘手的算法,我不想将其描述为面试问题!幸运的是,正如您在帖子中指出的那样,这里的一般情况比需要的更复杂。但我相信这种方法可能接近于您的(错误)示例答案试图描述的内容。

【讨论】:

  • FWIW,因为查询点是固定的,所以你不需要。
  • 对。正如我所指出的,如果 k 也是固定的,除了简单的线性搜索之外,您甚至不需要做任何事情。但我认为 k-d 树很有趣,更令人痛心的是,我认为它们可能是 OP 中试图得到的错误答案。对于固定的“最近”来源(即来源)和一般 k,尽管我同意您的 O(n) 快速选择可能是无与伦比的。
猜你喜欢
  • 2013-02-16
  • 2012-05-29
  • 1970-01-01
  • 2012-03-17
  • 2023-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-23
相关资源
最近更新 更多