【问题标题】:Printing Successor and Predecessor in a BST在 BST 中打印继任者和前任者
【发布时间】:2013-01-07 03:40:17
【问题描述】:

我的问题如下:

我有一个带有键的二叉搜索树:a1<a2<...<an,问题是打印树中的所有 (a_i, a_i+1) 对(其中 i={1,2,3,...})在 O(n) 时间内使用递归算法,没有任何全局变量并使用 O(1) 额外空间。一个例子: 让键为:1,2, ..., 5 将打印的对:(1,2) (2,3) (3, 4) (4, 5)

所以你不能在树中进行中序遍历并找到每个节点的后继/前继。因为这将花费 O(nh) 时间,并且如果树是平衡的,那么整棵树将是 O(nlgn)。

【问题讨论】:

  • 按序遍历不是 O(nlgn)...
  • 是的,中序遍历是 O(n),但平均情况下后继函数是 O(h),在最坏的情况下是 O(n),因此如果您平均为每个节点调用后继或前任,它将是 O(nlgn) 但在最坏的情况下它是二次的。
  • 这不是真的。在完整的遍历中,每个节点最多“访问”3 次(下降一次,返回两次)。
  • 其实节点“访问”的总数正好是2n;每个节点进入一次(从上面),退出一次(到上面)。
  • 所以用大符号表示它将是 O(2n)=O(n) 还是我遗漏了什么?

标签: algorithm data-structures binary-tree traversal binary-search-tree


【解决方案1】:

虽然你是对的,找到一个有序的后继者或前驱者可能需要 O(h) 时间,但事实证明,如果你从 BST 中的最小值开始并重复找到它的后继者,则完成的工作总量无论树的形状如何,最终都会出现 O(n)。

对此的直觉是考虑在迭代进行后继查找时遍历树中每条边的次数。具体来说,您将准确地访问树中的每条边两次:一次是当您下降到子树时,一次是当您从子树中走出来访问该子树中的每个节点时。由于 n 节点树有 O(n) 条边,这需要 O(n) 时间才能完成。

如果您对此持怀疑态度,请尝试编写一个简单的程序来验证它。我记得当我第一次听到这个结果时,我并不相信这个结果,直到我写了一个程序来确认它。 :-)

在伪代码中,逻辑如下所示:

  1. 通过从根开始并重复跟随左子指针直到不存在这样的指针,找到树中的最低值。
  2. 直到所有节点都被访问过,记住当前节点并找到它的后继节点如下:
    1. 如果当前节点有右孩子:
      1. 向右走。
      2. 向左走,直到没有剩下的孩子。
      3. 输出你开始的节点,然后是这个节点。 2:否则:
      4. 一直走到父节点,直到发现您开始的节点是其父节点的左子节点。
      5. 如果您找到了根,但从未发现您是从左孩子向上遍历的,那么您就完成了。
      6. 否则,输出你记得的节点,然后是当前节点。

希望这会有所帮助!

【讨论】:

  • 我敢说这与我的回答中的逻辑相同:-)
  • 嗨@templatetypedef 当我看到它的证明时我相信这个算法是O(n) :) (personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/…)。是的,这个算法有效,但它的递归实现有点复杂。我有一个直觉,它有一个不太复杂的递归实现和普通的中序遍历。
【解决方案2】:

Oli 是对的,中序遍历是 O(n),但你是对的,使用一般的后继/前继例程会增加算法的复杂性。所以:

一个简单的解决方案是使用有序遍历来遍历树,跟踪您最后一次被带入右指向边(例如,使用名为 last_right_ancestor_seen 的变量指向到它的父节点)和你见过的最后一个叶子节点(比如说,在 last_leaf_seen 中(实际上是任何没有右孩子的节点)。每次处理一个叶子节点时,它的前身是 last_right_ancestor,而且每次碰到一个非叶子节点,它的前身就是last_leaf_seen,你打印这两个。O(n)时间,O(1)空间。

希望够清楚,如果没有我可以给你画个图。

编辑:这是未经测试的,但可能是正确的:

walk(node* current, node* last_right_ancestor_seen, node* last_leaf_seen) {

    if(current->left != null) {
        walk(current->left, last_right_ancestor_seen, last_leaf_seen);
    }

    if(current->is_leaf()) {
            if(last_right_ancestor_seen != null)
                print(last_right_ancestor_seen->value, current->value);
    }
    else {
        print(last_leaf_seen->value, current->value);
    }

    if(current->right != null) {
        *last_right_ancestor_seen = *current;
        walk(current->right, last_right_ancestor_seen, last_leaf_seen);
    }
    else {
        *last_leaf_seen = *current;
    }

}

【讨论】:

  • 请注意,递归算法隐式使用存储来存储调用堆栈,在本例中为 O(h),但问题陈述可能选择忽略这一点
  • 但是如何在不使用 last_right_ancesstor_seen 和 last_leaf_seen 的全局变量的情况下存储这些变量呢?我可能会误解。但是你能给出一个伪代码吗?堆栈空间并不重要。
  • 您可以将它们作为参数传递给每个递归调用:-)
  • 对不起,这是一个愚蠢的问题 :)。我要试试你的算法谢谢。
  • 这似乎不符合 if(current->is_leaf()) { print(last_right_ancestor_seen->value, current->value);} 我得到的 last_right_ancestor_seen->value null 是有道理的,因为您仅在右转时设置 last_right_ancestor_seen,但如果树是左倾的它可能永远不会有一个正确的祖先。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-04
  • 1970-01-01
相关资源
最近更新 更多