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

时间:2009-04-26 15:28:31

标签: algorithm language-agnostic tree memory-management binary-tree

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

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

11 个答案:

答案 0 :(得分:22)

Geez,我必须从Knuth中输入它。这个解决方案由Joseph M. Morris [ Inf。 PROC。信件 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 ;
}

答案 1 :(得分:6)

如果您有每个孩子的父母的链接,则可以。遇到孩子时,请访问左侧子树。回来时检查你是否是你父母的左孩子。如果是这样,请访问正确的子树。否则,继续往上走,直到你是左孩子或直到你到达树根。

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

如果您不关心遍历树的顺序,可以为根为1的节点分配整数映射,根的子节点为2和3,其子节点为4然后,通过递增计数器并通过其数值访问该节点,循环遍历树的每一行。您可以跟踪可能的最高子元素,并在计数器通过时停止循环。时间方面,这是一种效率极低的算法,但我认为它需要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)空间算法的规定,用于以任何顺序访问树的元素,而不需要子链接到父链接或修改树的结构。

答案 2 :(得分:4)

你可以破坏性地做到这一点,随时取消每一片叶子的链接。这可能适用于某些情况,即之后您不再需要树时。

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

编辑:有办法!您可以通过暂时反转它们来使用节点本身来点亮树的备份方式。当您访问节点时,将其left-child指针指向其父节点,将其right-child指针指向您上次在路径上右转的指针(可在父节点{{1}中找到此时指针),并将其真实子项存储在现在冗余的父{Q}指针或遍历状态中。下一个访问过的孩子的right-child指针。你需要保留一些指向当前节点及其附近的指针,但没有任何“非本地”指针。当你回到树上时,你可以改变这个过程。

我希望我能以某种方式明白这一点;这只是一个粗略的草图。你必须在某个地方查找它(我确信这在“计算机编程艺术”中的某处提到过。)

答案 3 :(得分:2)

要保留树只使用O(1)空间,可以使用...

  • 每个节点都是固定大小。
  • 整棵树位于记忆的连续部分(即阵列)
  • 您不会遍历树,只需遍历数组

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

  • @Svante提出了这个想法,但我想用一种破坏性的方法扩展如何一点。
  • 如何?您可以继续在树中选择最左侧的 leaf 节点(对于(;;)node = node-&gt; left etc ...,然后对其进行处理,然后将其删除。如果是最左边的节点在树中不是叶子节点,然后你处理并删除右边节点的最左边叶子节点。如果右边节点没有子节点,那么你处理并删除它。

方法不起作用......

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

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

答案 4 :(得分:1)

如果节点有指向父节点的指针,则可以实现此目的。当你向上走回树(使用父指针)时,你也传递了你来自的节点。如果您来自的节点是您现在所在节点的左子节点,那么您将遍历正确的子节点。否则你会回到它的父母身边。

编辑以回应问题中的编辑:如果要遍历整个树,那么这不可能。为了爬上树,你需要知道去哪里。但是,如果您只想在树中迭代单个路径,那么可以在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();
}

答案 5 :(得分:1)

使用称为线程树的结构,从节点到其祖先的指针可以没有(每个节点两位)附加存储。在线程树中,空链接由一些状态而不是空指针表示。然后,您可以使用指向其他节点的指针替换空链接:左侧链接指向inorder遍历中的后继节点,右侧链接指向前一个节点。这是一个重量级的Unicode图(X表示用于控制树的头节点):

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

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

Inorder-Successor(p)
    p points to a node.  This routine finds the successor of p in
    an inorder traversal and returns a pointer to that node

    qp.right
    If p.rtag = 0 Then
        While q.ltag = 0 Do
            qq.left
        End While
    End If

    Return q
    

有关线程树的更多信息可以在计算机程序设计第2章第3.1节中找到,或者分散在互联网上。

答案 6 :(得分:1)

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

答案 7 :(得分:0)

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

将您的父节点编码为叶子指针

答案 8 :(得分:0)

是的,这是可能的。这是我的迭代示例,它的时间复杂度为 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); 时,您都应该在其中放置用于值处理的代码。

答案 9 :(得分:-1)

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

答案 10 :(得分:-3)

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

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

此结构将使您能够通过二叉树在任何方向上移动一步 但仍然在迭代中你需要保持路径!