Преобразование двоичного дерева в связанный список, сначала ширину, постоянное хранение / деструктивное - PullRequest
18 голосов
/ 05 августа 2010

Это не домашняя работа, и мне не нужно на нее отвечать, но теперь я стал одержимым:)

Проблема:

  • Разработка алгоритма для деструктивного выравнивания бинарного дерева в связанном списке в ширину. Ладно, достаточно просто. Просто создайте очередь и делайте то, что вам нужно.
  • Это была разминка. Теперь реализуйте его с постоянным хранилищем (рекурсия, если вы можете найти ответ, используя его, является логарифмическим хранилищем, а не постоянным).

Я нашел решение этой проблемы в Интернете около года назад, но теперь я забыл его, и я хочу знать:)

Хитрость, насколько я помню, заключалась в том, чтобы использовать дерево для реализации очереди, используя деструктивную природу алгоритма. Когда вы связываете список, вы также помещаете элемент в очередь.

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

Даже ссылка на эту оригинальную статью / пост была бы мне полезна :) Google не доставляет мне радости.

Edit:

Жереми указал, что есть довольно простой (и хорошо известный ответ), если у вас есть родительский указатель. Хотя теперь я думаю, что он прав в отношении исходного решения, содержащего указатель на родительский элемент, я действительно хотел решить проблему без него:)

Уточненные требования используют это определение для узла:

struct tree_node
{
  int value;
  tree_node* left;
  tree_node* right;
};

Ответы [ 9 ]

22 голосов
/ 05 августа 2010

Идея:

Вы можете построить связанный список вдоль левых дочерних элементов дерева. В то же время правые дочерние элементы этого списка используются для хранения очереди следующих поддеревьев для вставки в хвост.


Алгоритм в псевдокоде:

( редактировать: переписано для ясности )

  • Узел имеет три компонента: значение, ссылку на его левый дочерний элемент и ссылку на его правый дочерний элемент. Ссылки могут быть нулевыми.
  • Функция преобразования двоичного дерева таких узлов в единый связанный список
    • принимает ссылку на корневой узел в качестве параметра root,
    • циклов, с tail начиная с левого потомка root:
      • поменяйте местами левого ребенка tail с правым ребенком root,
      • loop ( O (n) вставка очереди), с bubble-to начиная с root и bubble-from всегда левый потомок bubble-to,
        • поменяйте местами правого ребенка bubble-to с правым ребенком 'пузыря-от',
        • продвижение bubble-to и bubble-from к оставленным детям,
        • пока bubble-from не достигнет tail,
      • продвижение tail к левому ребенку,
      • , в то время как tail не равно нулю.
    • Наконец, верните head. Единый связанный список теперь проходит по левым дочерним элементам.

Иллюстрация

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

              1
          /       \
      2               3
    /   \           /   \
  4       5       6       7
 / \     / \     / \     / \
8   9   A   B   C   D   E   F

Обратите внимание, что это не обязательно значения узлов, они просто нумеруются для отображения.

Дерево после 1 итерации (обратите внимание, что список формируется от 1 до 3 и очередь поддеревьев с корнями в 4 и 5):

                head
                  v
                  1
               /    \
    tail    2         4
      v  /    \      / \
      3         5   8   9
    /   \      / \
  6       7   A   B
 / \     / \
C   D   E   F

после 3 итераций (список теперь составляет от 1 до 5, а очередь содержит поддеревья с корнями в 6 до 9):

                   head
                     v
                     1
                  /     \
               2          6
             /   \       / \
           3       7    C   D
          / \     / \
         4   8   E   F
        / \
tail > 5   9
      / \
     A   B

и после 8 итераций (почти готово):

                       head
                         v
                         1
                        / \
                       2   B
                      / \
                     3   C
                    / \
                   4   D
                  / \
                 5   E
                / \
               6   F
              /
             7
            /
           8
          /
         9
        /
tail > A

Реальный код (Лисп)

Это класс узла:

(defclass node ()
  ((left :accessor left)
   (right :accessor right)
   (value :accessor value)))

Полезный помощник:

(defmacro swap (a b)
  `(psetf ,a ,b
          ,b ,a))

Функция преобразования ( редактировать: переписано для ясности ):

(defun flatten-complete-tree (root)
  (loop for tail = (and root (left root)) then (left tail)
        while tail
        do (swap (left tail) (right root))
           (loop for bubble-to = root then (left bubble-to)
                 for bubble-from = (left bubble-to)
                 until (eq bubble-from tail)
                 do (swap (right bubble-to) (right bubble-from))))
  root)

Необратимая операция для зубчатых деревьев:

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

(defun flatten-tree (root)

;; Внутренний цикл содержит head в корне еще не раскрытого поддерева,
;; то есть узел первой ветви,
;; в то же время гладя все от корневых неразветвленных уровней влево.
;; Это заканчивается nil, когда дерево полностью сплющено.

  (loop for head = (loop for x = (or head root) then (left x)
                         do (when (and x (null (left x)))
                              (swap (left x) (right x)))
                         until (or (null x) (right x))
                         finally (return x))
        for tail = (and head (left head)) then (left tail)
        while head
        do (swap (left tail) (right head))

;; Этот внутренний цикл - O (n) вставка очереди

           (loop for bubble-to = head then (left bubble-to)
                 for bubble-from = (left bubble-to)
                 until (eq bubble-from tail)
                 do (swap (right bubble-to) (right bubble-from))))

;; Наконец, верните оригинальный рут.

  root)
5 голосов
/ 06 августа 2010

Только для записи, рекурсивная версия прекрасна (это на C #):

[Отредактировано: как указывает st0le, моя первая версия содержит 'new's!Извините, я провел последние двадцать лет, программируя на декларативных языках.Вот исправленная версия.]

[Правка: двойные крысы.Это не ширина вначале.]

public static Tree<T> Flatten(Tree<T> t, Tree<T> u = null)
{
    if (t == null) return u;
    t.R = Flatten(t.L, Flatten(t.R, u));
    t.L = null;
    return t;
}
2 голосов
/ 05 августа 2010

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

Существует хорошо известныйитерация порядка, которая является итерационной и в пространстве O (1).Предположим, например, что вы хотите напечатать элементы по порядку.По сути, когда вы проходите бинарное дерево, вы должны в любой момент, в любом данном узле, решить, хотите ли вы переместиться вверх (к родителю), влево (к левому потомку) или вправо.Идея этого алгоритма состоит в том, чтобы основывать это решение на том, откуда вы пришли:

  1. , если вы выходите из родительского узла, тогда вы явно посещаете узел в первый раз, поэтому вы идете ВЛЕВО;
  2. если вы пришли от левого потомка, то вы посетили все узлы, которые меньше текущего узла, поэтому вы можете напечатать (помните, что мы хотим напечатать узлы по порядку здесь) текущий узел, а затемgo RIGHT;
  3. наконец, если вы пришли от правильного дочернего элемента, это означает, что вы посетили все поддерево с корнем в этом конкретном узле, и поэтому вы можете вернуться к родительскому элементу.

ОК: это алгоритм, который вы берете за основу.Теперь, разумеется, если вы деструктивно модифицируете это в связанный список, вам следует изменить узел, только если вы больше не собираетесь его посещать.Это когда вы подходите справа.В этот момент вы должны решить, каким будет преемник этого узла ...?

Что ж, вам нужно постоянно отслеживать два указателя: один на самый маленький узел, который выпосетил, и один к самому большому узлу, который вы посетили в текущем корневом поддереве.Вы используете наименьшее в качестве преемника для узлов, которые вы в последний раз посещаете, когда вы подходите от правильного дочернего элемента, и используете наибольшее в качестве преемника для узлов, которые вы в последний раз посещаете, приходящих откуда-то еще, потому что у них нет правильного дочернего элемента!

РЕДАКТИРОВАТЬ 1 : я забыл сказать, что неявно считаю, что поле «родитель» в двоичном дереве становится полем «следующий» в связанном списке - это то, что я изменяюна последнем шаге.

РЕДАКТИРОВАТЬ 3 : следующая часть моего ответа оказалась не совсем ответить на исходный вопрос (но то, что предшествует все еще уместно).


РЕДАКТИРОВАТЬ 2 : Следуя понятному желанию Сванте, я высказал свое предложение об использовании левых вращений в некотором коде:

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

typedef struct node *node;

struct node
{
  int value;
  node left;
  node right;
};

node new_node(int value, node left, node right)
{
    node n = (node) malloc(sizeof(struct node));
    n->value = value; n->left = left; n->right = right;
    return n;
}

int rotate_right(node tree)
{
    if(tree != NULL && tree->left != NULL)
    {
        node
            a = tree->left->left,
            b = tree->left->right,
            c = tree->right;
        int tmp = tree->value;
        tree->value = tree->left->value;
        tree->left->value = tmp;

        tree->left->left = b;
        tree->left->right = c;
        tree->right = tree->left;

        tree->left = a;
        return 1;
    }
    return 0;
}

Функция поворота не изящна, но таковалегко запутаться, я попытался следовать тому же наименованию, которое использовалось в статье Википедии о вращениях .Узлы A, B, C названы так в моем коде;узлы P и Q - нет, и поскольку я решил не использовать указатели указателей, я вместо этого прибегнул к хитрости переключения значений P и Q --- в вместо мест переключения.Имея в своем распоряжении «вращение_право», алгоритм преобразования довольно прост:

void convert_to_list(node tree)
{
    if(tree != NULL) {
        while(rotate_right(tree) != 0);
        convert_to_list(tree->right);
    }
}

Полученное дерево является отсортированным связанным списком, где указатель списка следующий используетсябыть указателем вправо в дереве.Наконец, вот тестовая программа:

int main()
{
    node t =
        new_node(4,
              new_node(2, NULL, new_node(3, NULL, NULL)),
              new_node(8, new_node(5, NULL, NULL), NULL));
    convert_to_list(t);
    for(; t != NULL; t = t->right)
        printf("%d, ", t->value);
    return 0;
}
1 голос
/ 05 августа 2010

Ну, я не могу сейчас понять, как это помогает в этой ситуации, но это может дать вам преимущество. Существует методика, называемая «обращение указателя», используемая для итеративного обхода дерева без необходимости использовать стек / очередь для хранения указателей - в основном она использовалась для сборщиков мусора с низким объемом памяти. Идея заключается в том, что когда вы переходите к потомку узла, вы связываете этот указатель на потомка с родителем, чтобы вы знали, куда вернуться, когда закончите работу с этим узлом. Таким образом, информация трассировки, которую вы обычно храните в стеке / очереди, теперь встроена в само дерево.

Я нашел следующее слайд-шоу с примером (к сожалению, в Google нет ничего лучше). В этом примере показано, как пройти через двоичное дерево без дополнительной памяти.

0 голосов
/ 30 ноября 2014

Вот мой подход к проблеме;

struct TreeNode
{
    TreeNode(int in) : data(in)
    {
        left = nullptr;
        right = nullptr;
    }
    int data;
    TreeNode* left;
    TreeNode* right;
};


//Converts left pointer to prev , right pointer to next
// A tree which is like              5 
//                                 11  12

//will be converted to double linked list like 5 -> 12 -> 11 
void finalize(TreeNode* current, TreeNode* parent)
{
    if (parent == nullptr)
    {
        current->left = nullptr;
        return;
    }

    if (parent->left == current)
    {
        if (parent->right == nullptr)
        {
            parent->right = current;
            current->left = parent;
        }
        current->left = parent->right;
    }
    else
    {
        current->left = parent;
        parent->right = current;
        current->right = parent->left;
    }
}


void traverser(TreeNode* current, TreeNode* parent)
{
    if (current->left != nullptr)
        traverser(current->left, current);
    if (current->right != nullptr)
        traverser(current->right, current);

    finalize(current, parent);
}

void start(TreeNode* head)
{
    if (head == nullptr || (head->left == nullptr && head->right == nullptr))
        return;

    traverser(head, nullptr);
}


int main()
{
    TreeNode* n1 = new TreeNode(5);
    TreeNode* n2 = new TreeNode(11);
    TreeNode* n3 = new TreeNode(12);



    n1->left = n2;
    n1->right = n3;

    start(n1);
}
0 голосов
/ 30 января 2013

Существует простая реализация Java с первым описанным методом.

http://www.dsalgo.com/BinaryTreeToLinkedList.php

0 голосов
/ 24 августа 2012

Вт об этом решении

temp=root;
struct node*getleftmost(struct node* somenode)
{
   while(somenode->left)
   somenode=somenode->left;
   return somenode;
}

 while(temp)
 {
 if(temp->right){
 (getletfmost(temp))->left=temp->right;
 temp->right=NULL;}
 temp=temp->left;
 }
0 голосов
/ 08 августа 2010

Это мой ответ, который работает. Теперь я понимаю, что это тот же подход, что и решение Сванте (!), Хотя я строю дерево справа.

Для справки, вот код C #:

// Flatten a tree into place in BFS order using O(1) space and O(n) time.
// Here is an example of the transformation (the top-row indicates the
// flattened parts of the tree.
//  
//  a
//  |---.
//  b   c
//  |-. |-.
//  d e f g
//  
//  becomes
//  
//  a-b-c
//  | | |-.
//  d e f g
//  
//  becomes
//  
//  a-b-c-d-e-f-g
//  
public static void FlattenBFS(Tree<T> t)
{
    var a = t; // We "append" newly flattened vertices after 'a'.
    var done = (t == null);
    while (!done)
    {
        done = true;
        var z = a; // This is the last vertex in the flattened part of the tree.
        var i = t;
        while (true)
        {
            if (i.L != null)
            {
                var iL = i.L;
                var iLL = iL.L;
                var iLR = iL.R;
                var aR = a.R;
                i.L = iLL;
                a.R = iL;
                iL.L = iLR;
                iL.R = aR;
                a = iL;
                done &= (iLL == null) & (iLR == null);
            }
            if (i == z)
            {
                break; // We've flattened this level of the tree.
            }
            i = i.R;
        }
        a = (a.R ?? a); // The a.R item should also be considered flattened.
    }
}
0 голосов
/ 05 августа 2010

Я не думаю, что нам нужны родительские указатели.Предположим индуктивно, что уровни от 0 до k-1 плюс сторожевой узел были преобразованы в односвязный список на левых дочерних указателях, чьи правые дочерние элементы указывают на узлы уровня k.Выполните итерацию по списку, поочередно захватывая каждый «правый дочерний элемент» (узел уровня k) и вставляя его в конец списка, перезаписывая правый указатель, из которого он вышел, с его вскоре перезаписываемым левым дочерним элементом.Когда мы достигли начального конца списка, мы расширили индуктивную гипотезу до k + 1.

РЕДАКТИРОВАТЬ: код

#include <cstdio>

struct TreeNode {
  int value;
  TreeNode *left;
  TreeNode *right;
};

// for simplicity, complete binary trees with 2^k - 1 nodes only
void Flatten(TreeNode *root) {
  TreeNode sentinel;
  sentinel.right = root;
  TreeNode *tail = &sentinel;
  while (true) {
    TreeNode *p = &sentinel;
    TreeNode *old_tail = tail;
    while (true) {
      if ((tail->left = p->right) == NULL) {
        return;
      }
      tail = p->right;
      p->right = p->right->left;
      if (p == old_tail) {
        break;
      }
      p = p->left;
    }
  }
}

int main() {
  const int n = 31;
  static TreeNode node[1 + n];
  for (int i = 1; i <= n; ++i) {
    node[i].value = i;
    if (i % 2 == 0) {
      node[i / 2].left = &node[i];
    } else {
      node[i / 2].right = &node[i];
    }
  }
  Flatten(&node[1]);
  for (TreeNode *p = &node[1]; p != NULL; p = p->left) {
    printf("%3d", p->value);
  }
  printf("\n");
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...