使用 O(1) 辅助空间迭代二叉树

Posted

技术标签:

【中文标题】使用 O(1) 辅助空间迭代二叉树【英文标题】:Iterating over a Binary Tree with O(1) Auxiliary Space 【发布时间】:2009-04-26 15:28:30 【问题描述】:

是否可以在 O(1) 辅助空间中迭代二叉树(不使用堆栈、队列等),或者这已被证明是不可能的?如果可以,怎么做?

编辑:如果有指向父节点的指针,我得到的关于这可能的响应很有趣,我不知道这可以做到,但取决于你如何看待它,这可能是 O( n) 辅助空间。此外,在我的实际用例中,没有指向父节点的指针。从现在开始,请在回答时假设这一点。

【问题讨论】:

我认为链接到父级是 O(n) 辅助空间。您应该编辑您的问题以明确提及您的二叉树没有这样的属性。 你怎么能没有指向你的父节点的指针呢?没有它们,你怎么能遍历你的树呢?那是堆栈进来的地方吗?无论如何,您都需要父母的指导来进行平衡。 【参考方案1】:

天哪,我得从 Knuth 那里实际输入它。该解决方案由 Joseph M. Morris [Inf.过程。 Letters 9 (1979), 197-200]。据我所知,它在 O(NlogN) 时间内运行。

static void VisitInO1Memory (Node root, Action<Node> preorderVisitor) 
  Node parent = root ;
  Node right  = null ;
  Node curr ;
  while (parent != null) 
    curr = parent.left ;
    if (curr != null) 
      // search for thread
      while (curr != right && curr.right != null)
        curr = curr.right ;

      if (curr != right) 
        // insert thread
        assert curr.right == null ;
        curr.right = parent ;
        preorderVisitor (parent) ;
        parent = parent.left ;
        continue ;
       else
        // remove thread, left subtree of P already traversed
        // this restores the node to original state
        curr.right = null ;
     else
      preorderVisitor (parent) ;

    right  = parent ;
    parent = parent.right ;
  


class Node

  public Node left  ;
  public Node right ;

【讨论】:

这会破坏树,不是吗? 不,它只是临时添加某些叶子的反向引用。 这个解决方案简直太棒了。 其实应该是O(n):你只接触了一个节点常数的次数(注意,内部的while循环只接触向右的路径,并且只有当curr是路径最左边的节点时parent 进入外部 while 循环;当您进入子树和离开子树时会发生一次。然后,size of those paths combined = O(size of the whole tree) @Rufflewind 链接已损坏【参考方案2】:

如果您在每个孩子中都有指向父母的链接,这是可能的。当你遇到一个孩子时,访问左子树。当回来检查你是否是你父母的左孩子。如果是这样,请访问右子树。否则,一直往上走,直到你成为左孩子,或者直到你碰到树根为止。

在此示例中,堆栈的大小保持不变,因此不会消耗额外的内存。当然,正如 Mehrdad 所指出的,到父节点的链接可以被认为是 O(n) 空间,但这更像是树的属性,而不是算法的属性。

如果您不关心遍历树的顺序,您可以将整数映射分配给根为 1 的节点,根的子节点为 2 和 3,子节点为 4 , 5, 6, 7 等等。然后通过递增计数器并通过其数值访问该节点来循环遍历树的每一行。您可以跟踪可能的最高子元素并在计数器通过时停止循环。在时间上,这是一个效率极低的算法,但我认为它需要 O(1) 空间。

(我借鉴了堆编号的想法。如果你有节点 N,你可以在 2N 和 2N+1 找到孩子。你可以从这个数字倒推找到孩子的父母。)

这是该算法在 C 中的一个示例。请注意,除了创建树之外没有任何 malloc,并且没有递归函数,这意味着堆栈占用恒定空间:

#include <stdio.h>
#include <stdlib.h>

typedef struct tree

  int value;
  struct tree *left, *right;
 tree;

tree *maketree(int value, tree *left, tree *right)

  tree *ret = malloc(sizeof(tree));
  ret->value = value;
  ret->left = left;
  ret->right = right;
  return ret;


int nextstep(int current, int desired)

  while (desired > 2*current+1)
      desired /= 2;

  return desired % 2;


tree *seek(tree *root, int desired)

  int currentid; currentid = 1;

  while (currentid != desired)
    
      if (nextstep(currentid, desired))
    if (root->right)
      
        currentid = 2*currentid+1;
        root = root->right;
      
    else
      return NULL;
      else
    if (root->left)
      
        currentid = 2*currentid;
        root = root->left;
      
    else
      return NULL;
    
  return root;  



void traverse(tree *root)

  int counter;    counter = 1; /* main loop counter */

  /* next = maximum id of next child; if we pass this, we're done */
  int next; next = 1; 

  tree *current;

  while (next >= counter)
       
      current = seek(root, counter);
      if (current)
      
          if (current->left || current->right)
              next = 2*counter+1;

          /* printing to show we've been here */
          printf("%i\n", current->value);
      
      counter++;
    


int main()

  tree *root1 =
    maketree(1, maketree(2, maketree(3, NULL, NULL),
                            maketree(4, NULL, NULL)),
                maketree(5, maketree(6, NULL, NULL),
                            maketree(7, NULL, NULL)));

  tree *root2 =
      maketree(1, maketree(2, maketree(3,
          maketree(4, NULL, NULL), NULL), NULL), NULL);

  tree *root3 =
      maketree(1, NULL, maketree(2, NULL, maketree(3, NULL,
          maketree(4, NULL, NULL))));

  printf("doing root1:\n");
  traverse(root1);

  printf("\ndoing root2:\n");
  traverse(root2);

  printf("\ndoing root3:\n");
  traverse(root3);

我为代码质量道歉 - 这主要是一个概念证明。此外,该算法的运行时间并不理想,因为它做了很多工作来弥补无法维护任何状态信息的问题。从好的方面来说,这确实符合 O(1) 空间算法的要求,可以以任何顺序访问树的元素,而无需子链接到父链接或修改树的结构。 p>

【讨论】:

布莱恩,数字的每一位都会告诉你你要去哪个方向。 但是你如何在不使用额外空间的情况下访问节点#7?递归查找这样的节点会占用空间,如果将所有节点存储在另一个数据结构中,也会占用额外的空间。 @Brian 您可以减去 1 并除以 2 以查看 3 是 7 的父级。同样,您可以减去 1 并再次除以 2 以查看 1 是 3 的父级。因此,您所需要的只是一个循环,它将计算从当前节点到所需节点的路径的下一步。 @nobody_:你的想法很好,但你必须注意它只适用于 complete 二叉树,从纯理论的角度来看,你仍然需要“O(height) bit " 存储您所在位置的空间。 @Mehrdad:我不明白您对位方法的确切含义,但即使它确实有意义.... O(N) = O(C*N)。即使 N 是 1 bi,你仍然有 O(N) 空间。【参考方案3】:

你可以破坏性地做到这一点,在你去的时候断开每一片叶子的链接。这可能适用于某些情况,即当您不再需要树时。

通过扩展,您可以在销毁第一个二叉树时构建另一棵二叉树。您将需要一些内存微管理来确保峰值内存永远不会超过原始树的大小加上可能有点恒定。不过,这可能会产生相当多的计算开销。

编辑:有办法!您可以使用节点本身通过暂时反转它们来照亮备份树的路径。当你访问一个节点时,你将它的left-child指针指向它的父节点,它的right-child指针指向你最后一次右转路径(此时可以在父节点的right-child指针中找到) ),并将其真正的子代存储在现在冗余的父代的right-child 指针中或您的遍历状态中。下一个访问儿童的left-child 指针。您需要保留一些指向当前节点及其附近的指针,但不要保留“非本地”。当您回到树上时,您将反转该过程。

我希望我能以某种方式说清楚;这只是一个粗略的草图。您必须在某处查找它(我确信在计算机编程艺术中某处提到了这一点)。

【讨论】:

+1,我在回答中参考了您的回答,对此方法进行了更多评论。 巧妙,但令人震惊。只需在每个元素中都有一个向上指针。它更简单,痛苦更少,并且得到了很好的理解和认可。【参考方案4】:

为了保留树并且只使用 O(1) 空间,如果...

每个节点的大小都是固定的。 整棵树位于内存的连续部分(即数组)中 您无需遍历树,只需遍历数组即可

或者,如果您在处理树时销毁它...:

@Svante 提出了这个想法,但我想用破坏性的方法稍微扩展一下如何。 如何?你可以继续取一棵树中最左边的leaf节点(for(;;) node = node->left etc...,然后处理它,然后删除它。如果最左边的节点在树不是叶子节点,则处理并删除右节点最左边的叶子节点。如果右节点没有子节点,则处理并删除它。

它不起作用的方式...

如果您使用递归,您将隐式使用堆栈。对于某些算法(不适用于此问题),尾递归将允许您使用递归并具有 O(1) 空间,但由于任何特定节点可能有多个子节点,因此在递归调用之后还有工作要做,O(1 ) 空间尾递归是不可能的。

您可以尝试一次解决 1 个级别的问题,但是如果没有辅助(隐式或显式)空间,就无法访​​问任意级别的节点。例如,您可以递归查找所需的节点,但这会占用隐式堆栈空间。或者您可以将所有节点存储在每个级别的另一个数据结构中,但这也需要额外的空间。

【讨论】:

你错了,Knuth TAOCP I.2.3.1 练习 21 指的是至少三种算法,用于在 O(1) 空间中遍历树而不破坏原始树(尽管他们确实在-地点)。 @nobody_:我没有考虑允许您修改树的情况。但我确实给出了 2 个例子,所以肯定有一些 :) 无论如何我修改了我的答案以删除无效部分。 对于那些没有 TAOCP 的人,@AntonTykhyy 指的是 Morris 树遍历算法,它在 O(n) 时间和 O(1) 辅助空间中运行。此页面可能会有所帮助:***.com/questions/5502916/…。另外,有人在下面发布了预购 Morris 算法。【参考方案5】:

使用称为线程树的结构,可以拥有从节点指向其祖先的指针,而无需(每个节点两个位)额外存储。在线程树中,空链接由位状态而不是空指针表示。然后,您可以将空链接替换为指向其他节点的指针:左链接指向中序遍历中的后继节点,右链接指向前驱节点。这是一个 Unicode-heavy 图(X 表示用于控制树的头节点):

╭─┬──────────────────────────────────────╮ ╭──────────────────────────▶┏━━━┯━━━┯━━▼┓│ │ │ ╭─╂─ │ X │ ─╂╯ │ │ ▼ ┗━━━┷━━━┷━━━┛ │ │ ┏━━━┯━━━┯━━━┓ │ │ ╭────╂─ │ A │ ─╂──╮ │ │ ▼ ┗━━━┷━━━┷━━━┛ │ │ │ ┏━━━┯━━━┯━━┓ ▲ │ ┏━━━┯━━━┯━━━┓ │ │ ╭─╂─ │ B │ ─╂────┤ ├────────╂─ │ C │ ─╂──────╮ │ │ ▼ ┗━━━┷━━━┷━━━┛ │ ▼ ┗━━━┷━━━┷━━━┛ ▼ │ │┏━━━┯━━━┯━━┓ ▲ │ ┏━━━┯━━┯━━━┓ ▲ ┏━━━┯━━┯━━━┓ │ ╰╂─ │ D │ ─╂─╯ ╰───╂ │ E │ ─╂╮ │ ╭╂─ │ F │ ─╂╮ │ ┗━━━┷━━┷━━┛ ┗━━━┷━━━┷━━━┛▼ │ ▼┗━━━┷━━━┷━━━┛▼ │ ▲ ┏━━━┯━━━┯━━┓ │ ┏━━━┯━━━┯━━━┓ ▲ ┏━━━┯━━━┯━━━┓│ ╰─╂─ │ G │ ╂──┴─╂─ │ H │ ─╂─┴─╂─ │ J │ ─╂╯ ┗━━━┷━━━┷━━━┛ ┗━━━┷━━━┷━━━┛ ┗━━━┷━━━┷━━━┛

一旦你有了结构,进行中序遍历就非常非常容易了:

顺序后继(p) p 指向一个节点。该例程在 中序遍历并返回指向该节点的指针 qp.right 如果 p.rtag = 0 那么 q.ltag = 0 qq.left 结束时 如果结束 返回 q

更多关于线程树的信息可以在 计算机编程艺术 Ch.2 §3.1 中找到,也可以在 Internet 上找到。

【讨论】:

【参考方案6】:

如果节点具有指向其父节点的指针,则可以实现此目的。当您返回树(使用父指针)时,您还传递了您来自的节点。如果您来自的节点是您现在所在节点的左孩子,那么您将遍历右孩子。否则,您会回到它的父级。

编辑以回应问题中的编辑:如果您想遍历整个树,那么不,这是不可能的。为了爬回树上,你需要知道去哪里。但是,如果您只想遍历树下的 single 路径,那么这可以在 O(1) 额外空间中实现。只需使用 while 循环遍历树,保持一个指向当前节点的指针。继续沿着树向下,直到找到所需的节点或找到叶节点。

编辑:这是第一个算法的代码(检查 iterate_constant_space() 函数并与标准 iterate() 函数的结果进行比较):

#include <cstdio>
#include <string>
using namespace std;

/* Implementation of a binary search tree. Nodes are ordered by key, but also
 * store some data.
 */
struct BinarySearchTree 
  int key;      // they key by which nodes are ordered
  string data;  // the data stored in nodes
  BinarySearchTree *parent, *left, *right;   // parent, left and right subtrees

  /* Initialise the root
   */
  BinarySearchTree(int k, string d, BinarySearchTree *p = NULL)
    : key(k), data(d), parent(p), left(NULL), right(NULL) ;
  /* Insert some data
   */
  void insert(int k, string d);
  /* Searches for a node with the given key. Returns the corresponding data
   * if found, otherwise returns None."""
   */
  string search(int k);
  void iterate();
  void iterate_constant_space();
  void visit();
;

void BinarySearchTree::insert(int k, string d) 
  if (k <= key)  // Insert into left subtree
    if (left == NULL)
      // Left subtree doesn't exist yet, create it
      left = new BinarySearchTree(k, d, this);
    else
      // Left subtree exists, insert into it
      left->insert(k, d);
   else  // Insert into right subtree, similar to above
    if (right == NULL)
      right = new BinarySearchTree(k, d, this);
    else
      right->insert(k, d);
  


string BinarySearchTree::search(int k) 
  if (k == key) // Key is in this node
    return data;
  else if (k < key && left)   // Key would be in left subtree, which exists
    return left->search(k); // Recursive search
  else if (k > key && right)
    return right->search(k);
  return "NULL";


void BinarySearchTree::visit() 
  printf("Visiting node %d storing data %s\n", key, data.c_str());


void BinarySearchTree::iterate() 
  visit();
  if (left) left->iterate();
  if (right) right->iterate();


void BinarySearchTree::iterate_constant_space() 
  BinarySearchTree *current = this, *from = NULL;
  current->visit();
  while (current != this || from == NULL) 
    while (current->left) 
      current = current->left;
      current->visit();
    
    if (current->right) 
      current = current->right;
      current->visit();
      continue;
    
    from = current;
    current = current->parent;
    if (from == current->left) 
      current = current->right;
      current->visit();
     else 
      while (from != current->left && current != this) 
        from = current;
        current = current->parent;
      
      if (current == this && from == current->left && current->right) 
        current = current->right;
        current->visit();
      
    
  


int main() 
  BinarySearchTree tree(5, "five");
  tree.insert(7, "seven");
  tree.insert(9, "nine");
  tree.insert(1, "one");
  tree.insert(2, "two");
  printf("%s\n", tree.search(3).c_str());
  printf("%s\n", tree.search(1).c_str());
  printf("%s\n", tree.search(9).c_str());
  // Duplicate keys produce unexpected results
  tree.insert(7, "second seven");
  printf("%s\n", tree.search(7).c_str());
  printf("Normal iteration:\n");
  tree.iterate();
  printf("Constant space iteration:\n");
  tree.iterate_constant_space();

【讨论】:

“当你回到树上时”......你如何到达树中每个节点的底部? 当前=根;而 current->left 存在: current = current->left @marcog:是的,你肯定可以到达单个节点......但我说你如何到达树中每个节点的底部? 当你刚刚从左孩子上来时,使用我提到的条件让右孩子下降。 一旦你处理了右边的孩子,你如何得到第二个最左边的孩子?然后是最左边的右边孩子? ...你需要存储你走过的路径。【参考方案7】:

Harry Lewis 和 Larry Denenberg 的“数据结构及其算法”描述了二叉树的常数空间遍历的链接反转遍历。为此,您不需要每个节点的父指针。遍历使用树中现有的指针来存储用于回溯的路径。需要 2-3 个额外的节点引用。在每个节点上加上一点,以便在我们向下移动时跟踪遍历方向(向上或向下)。在我对书中这些算法的实现中,分析表明这种遍历的内存/处理器时间要少得多。 java中的一个实现是here。

【讨论】:

您需要一个额外的数据结构来保存该位数组,这不是 O(1) 空间。【参考方案8】:

http://en.wikipedia.org/wiki/XOR_linked_list

将父节点编码为叶指针

【讨论】:

【参考方案9】:

是的,这是可能的。这是我的迭代示例,它具有 O(n) 时间复杂度和 O(1) 空间复杂度。

using System;
                    
public class Program

    public class TreeNode 
      public int val;
      public TreeNode left;
      public TreeNode right;
      public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) 
          this.val = val;
          this.left = left;
          this.right = right;
      
    
    public static void Main()
    
        TreeNode left = new TreeNode(1);
        TreeNode right = new TreeNode(3);
        TreeNode root = new TreeNode(2, left, right);
        
        TreeNode previous = null;
        TreeNode current = root;
        TreeNode newCurrent = null;
        
        while(current != null) 
            if(current.left == null) 
                if(current.right == null) 
                    if(previous == null) 
                        Console.WriteLine(current.val);
                        break;
                    
                    Console.WriteLine(current.val);
                    current = previous;
                    previous = previous.left;
                    current.left = null;
                 else 
                    newCurrent = current.right;
                    current.right = null;
                    current.left = previous;
                    previous = current;
                    current = newCurrent;
                
             else 
                newCurrent = current.left;
                current.left = previous;
                previous = current;
                current = newCurrent;
            
        
    

每次看到Console.WriteLine(current.val); 时,您都应该在其中放置代码以进行值处理。

【讨论】:

【参考方案10】:

我认为你无法做到这一点,因为你应该以某种方式找到你在路径中离开的节点并确定你总是需要 O(height) 空间。

【讨论】:

【参考方案11】:

是否可以在 O(1) 辅助空间中迭代二叉树。

struct node  node * father, * right, * left; int value; ;

这种结构将使您能够在二叉树的任何方向上移动 1 步。 但仍在迭代中,您需要保持路径!

【讨论】:

以上是关于使用 O(1) 辅助空间迭代二叉树的主要内容,如果未能解决你的问题,请参考以下文章

数据结构 第5章 树的二叉树 单元小结遍历二叉树和线索二叉树

普通二叉树二叉查找树平衡二叉树常见操作汇总

树二叉树存储结构二叉数遍历& 数据结构基本概念和术语

树二叉树存储结构二叉数遍历& 数据结构基本概念和术语

二叉树的Morris遍历算法

二叉树二叉树基本操作通用接口