Итерация по двоичному дереву с O (1) вспомогательным пространством - PullRequest
17 голосов
/ 26 апреля 2009

Возможно ли выполнить итерацию по двоичному дереву в O (1) вспомогательном пространстве (без использования стека, очереди и т. Д.), Или это оказалось невозможным? Если это возможно, как это можно сделать?

Edit: ответы, которые я получил о том, что это возможно, если есть указатели на родительские узлы, интересны, и я не знал, что это можно сделать, но в зависимости от того, как вы на это смотрите, это может быть O о) вспомогательное пространство. Кроме того, в моем случае практического использования нет указателей на родительские узлы. Отныне, пожалуйста, примите это при ответе.

Ответы [ 10 ]

22 голосов
/ 26 апреля 2009

Черт возьми, мне придется на самом деле напечатать это от Кнута. Это решение принадлежит Джозефу М. Моррису [ Инф. 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 ;
}
6 голосов
/ 26 апреля 2009

Это возможно, если у вас есть ссылка на родителя у каждого ребенка. Когда вы встретите ребенка, посетите левое поддерево. При возвращении проверьте, не является ли вы левым потомком своего родителя. Если так, посетите правильное поддерево. В противном случае продолжайте идти вверх, пока вы не оставите ребенка или пока не достигнете корня дерева.

В этом примере размер стека остается постоянным, поэтому дополнительная память не используется. Конечно, как отметил Мерад, ссылки на родителей можно рассматривать как пространство 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) для доступа в любом порядке к элементам дерева, не требуя дочерних родительских ссылок или изменения структуры дерева.

4 голосов
/ 26 апреля 2009

Вы можете сделать это деструктивно, разъединяя каждый лист по ходу дела. Это может быть применимо в определенных ситуациях, т.е. когда вам больше не нужно дерево.

По расширению, вы можете построить другое двоичное дерево при уничтожении первого. Вам понадобится некоторое микроуправление памятью, чтобы убедиться, что пиковая память никогда не превышает размер исходного дерева плюс, возможно, небольшая константа. Это, вероятно, будет иметь некоторые вычислительные затраты.

РЕДАКТИРОВАТЬ: Есть способ! Вы можете использовать сами узлы, чтобы осветить путь назад по дереву, временно перевернув их. Когда вы посещаете узел, вы указываете его left-child указатель на его родителя, его right-child указатель на последний раз, когда вы сделали правый поворот на вашем пути (который находится в указателе right-child родителя в данный момент ), и сохраните его реальных потомков либо в указателе right-child теперь избыточного родителя, либо в состоянии обхода, соотв. указатель следующего посещенного ребенка left-child. Вам нужно сохранить некоторые указатели на текущий узел и его окрестности, но ничего «нелокального». Возвращаясь к дереву, вы полностью изменяете процесс.

Я надеюсь, что смогу прояснить это; это всего лишь грубый набросок. Вам придется где-то искать это (я уверен, что это упоминается где-то в «Искусстве компьютерного программирования»).

2 голосов
/ 26 апреля 2009

Чтобы сохранить дерево и использовать только O (1) пробел, это возможно, если ...

  • Каждый узел имеет фиксированный размер.
  • Все дерево находится в непрерывной части памяти (т.е. в массиве)
  • Вы не перебираете дерево, вы просто перебираете массив

Или если вы уничтожите дерево во время его обработки ...:

  • @ Сванте придумал эту идею, но я хотел бы немного подробнее рассказать о , используя , используя деструктивный подход.
  • Как? Вы можете продолжать брать самый левый узел leaf в дереве (for (;;) node = node-> left и т. Д. ..., затем обрабатывать его, затем удалять. Если самый левый узел в Дерево не является листовым узлом, затем вы обрабатываете и удаляете самый левый крайний правый узел. Если правый узел не имеет дочерних узлов, вы обрабатываете и удаляете его.

Способы, что это не сработает ...

Если вы используете рекурсию, вы будете использовать стек неявно. Для некоторых алгоритмов (не для этой проблемы) хвостовая рекурсия позволила бы вам использовать рекурсию и иметь пространство O (1), но, поскольку у любого конкретного узла может быть несколько дочерних элементов, и, следовательно, после рекурсивного вызова есть работа, которую необходимо выполнить, O (1) ) рекурсия космического хвоста невозможна.

Вы можете попытаться решить задачу 1 уровня за раз, но нет способа получить доступ к узлам произвольного уровня без вспомогательного (неявного или явного) пространства. Например, вы можете выполнить поиск нужного узла, но тогда это займет неявное пространство стека. Или вы можете хранить все свои узлы в другой структуре данных для каждого уровня, но это также требует дополнительного места.

1 голос
/ 03 ноября 2013

«Источники данных и их алгоритмы» Гарри Льюиса и Ларри Дененберга описывают обход инверсии связей для постоянного прохождения пространства двоичного дерева. Для этого вам не нужен родительский указатель на каждом узле. Обход использует существующие указатели в дереве для хранения пути для отслеживания назад. Требуется 2-3 дополнительных ссылки на узлы. Плюс немного на каждом узле, чтобы отслеживать направление прохождения (вверх или вниз) при движении вниз. В моей реализации этих алгоритмов из книги профилирование показывает, что этот обход имеет гораздо меньше памяти / процессорного времени. Реализация в Java: здесь .

1 голос
/ 26 апреля 2009

Указатели от узлов к их предкам могут иметься без (ну, по два бита на узел) дополнительного хранилища, используя структуру, называемую резьбовым деревом. В многопоточном дереве нулевые ссылки представлены битом состояния, а не нулевым указателем. Затем вы можете заменить нулевые ссылки указателями на другие узлы: левые ссылки указывают на узел-преемник в обходе inorder, а правые ссылки - на предшественник. Вот диаграмма в Юникоде (X представляет узел заголовка, используемый для управления деревом):

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

Если у вас есть структура, выполнить обход по порядку очень и очень просто:

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

    <i>q</i> ← <i>p</i>.right
    <b>If</b> <i>p</i>.rtag = 0 <b>Then</b>
        <b>While</b> <i>q</i>.ltag = 0 <b>Do</b>
            <i>q</i> ← <i>q</i>.left
        <b>End While</b>
    <b>End If</b>

    <b>Return</b> <i>q</i>
    

Много дополнительной информации о резьбовых деревьях можно найти в Искусство компьютерного программирования Ch.2 §3.1 или разбросано по всему Интернету.

1 голос
/ 26 апреля 2009

Этого можно добиться, если у узлов есть указатели на их родителей. Когда вы идете обратно вверх по дереву (используя родительские указатели), вы также пропускаете узел, с которого пришли. Если узел, с которого вы исходите, является левым дочерним узлом того узла, на котором вы сейчас находитесь, то вы пересекаете правый дочерний узел. В противном случае вы вернетесь к его родителю.

РЕДАКТИРОВАТЬ в ответ на редактирование в вопросе: Если вы хотите выполнить итерацию по всему дереву, то нет, это невозможно. Чтобы залезть на дерево, нужно знать, куда идти. Однако, если вы просто хотите пройти по пути 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();
}
0 голосов
/ 25 апреля 2012

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

закодировать ваш родительский узел в листовые указатели

0 голосов
/ 25 апреля 2012

Возможно ли перебирать двоичное дерево в O (1) вспомогательном пространстве.

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

Эта структура позволит вам перемещаться на 1 шаг в любом направлении через двоичное дерево.
Но все же в итерации вам нужно сохранить путь!

0 голосов
/ 26 апреля 2009

Я думаю, что вы никак не могли бы это сделать, так как вы должны каким-то образом найти узлы, на которых вы остановились на пути, и определить, что вам всегда нужно пространство O (высота).

...