Хотите сохранить бинарное дерево на диск для игры 20 вопросов

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

Я реализую игру "20 вопросов". Я написал бинарное дерево, внутренние узлы которого — вопросы, а листья — ответы. Левый дочерний элемент узла — это путь, по которому вы пойдете, если кто-то ответит «да» на ваш текущий вопрос, а правый дочерний элемент — это ответ «нет». Обратите внимание, что это не бинарное дерево search, а просто бинарное дерево, у которого левый дочерний элемент — «да», а правый — «нет».

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

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

Я думал о сохранении дерева в виде представления массива (для узла i левый дочерний элемент равен 2i+1, а правый 2i+2, (i-1)/2 для родителя), но это не чисто, и я получаю много потерянного места.

Любые идеи для элегантного решения для сохранения разреженного двоичного дерева на диск?


person Rich    schedule 03.12.2008    source источник


Ответы (8)


Вы можете сохранить его рекурсивно:

 void encodeState(OutputStream out,Node n) {
        if(n==null) {
            out.write("[null]");
        } else {
           out.write("{");
           out.write(n.nodeDetails());
           encodeState(out, n.yesNode());
           encodeState(out, n.noNode());
           out.write("}");
        }
  }

Разработайте свой собственный менее текстовый формат вывода. Я уверен, что мне не нужно описывать метод чтения результирующего вывода.

Это обход в глубину. Сперва в ширину тоже работает.

person slim    schedule 03.12.2008
comment
Точнее, это обход предзаказа. [Кнут, т. 1, с. 1], что еще? - person John R. Strohm; 09.12.2009

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

У вас есть:

  1. Создайте очередь с вставленным в нее корневым элементом
  2. Удалите элемент из очереди, назовите его E
  3. Добавьте левого и правого потомка E в очередь. Если нет левого или правого, просто поместите нулевое представление узла.
  4. записать узел E на диск.
  5. Повторите с шага 2.

альтернативный текст

Последовательность обхода по уровням: F, B, G, A, D, I, C, E, H

Что вы будете хранить на диске: F, B, G, A, D, NullNode, I, NullNode, NullNode, C, E, H, NullNode

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

Шаг 1 чтение в:

F

Шаг 2, чтение в:

  F 
B

Шаг 3, чтение в:

  F 
 B  G

Шаг 4, чтение в:

   F 
 B  G
A

И так далее ...

Примечание. Если у вас есть представление узла NULL, вам больше не нужно перечислять его дочерние элементы на диск. При обратной загрузке вы будете знать, что нужно перейти к следующему узлу. Так что для очень глубоких деревьев это решение все равно будет эффективным.

person Brian R. Bondy    schedule 03.12.2008
comment
Я думаю, вы также можете обрезать все NullNodes с конца. По какой-то причине вы обрезали все, кроме 1. - person Eyal; 07.03.2010
comment
Когда вы загружаете его обратно с диска, как вы подключаете дочерние элементы обратно к родительскому узлу? - person Peter Lee; 29.09.2013
comment
Загрузка на самом деле НЕ проста. Нам все еще нужна очередь, чтобы запомнить родителей верхнего уровня...... и затем подключить их соответствующих дочерних элементов. - person Peter Lee; 30.09.2013

Простой способ сделать это — пройтись по дереву, выводя каждый элемент по мере того, как вы это делаете. Затем, чтобы загрузить дерево обратно, просто выполните итерацию по вашему списку, вставляя каждый элемент обратно в дерево. Если ваше дерево не является самобалансирующимся, вы можете переупорядочить список таким образом, чтобы итоговое дерево было разумно сбалансированным.

person Community    schedule 03.12.2008
comment
Самые простые решения обычно лучше :) также может помочь сериализация - person Marian Polacek; 03.12.2008
comment
Хорошая идея, за исключением того, что она не будет работать, если дерево не является бинарным деревом поиска (как узнать, куда вставить следующий элемент?) - person Rich; 03.12.2008
comment
Когда вы строите дерево, вы знаете, куда его вставить, поэтому чтение обратно — это то же самое. Я почти предложил то же самое, что и ответ Slim, но без кода. - person ; 03.12.2008
comment
Понятно, если бы была информация о том, где заканчивается каждый уровень, вы бы знали. Ваш комментарий о балансировке сбил меня с толку и заставил меня подумать, что вы говорите о вставке BST. - person Rich; 09.12.2008
comment
Что ж, если это BST, то чтение их в любом порядке, в котором вы их проходили, вероятно, приведет к несбалансированному дереву, поэтому я и упомянул об этом. В вопросе упоминалось бинарное дерево, поэтому я предположил, что балансировка может быть проблемой. Как вы этого добились в итоге? Мне было бы интересно узнать. - person ; 12.12.2008

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

При сохранении на диск просматривайте дерево, сохраняя каждый идентификатор узла, идентификатор ссылки «да», идентификатор ссылки «нет» и текст вопроса или ответа. Для нулевых ссылок используйте ноль в качестве нулевого значения. Вы можете либо добавить флаг, чтобы указать, является ли вопрос или ответ, или, проще говоря, проверить, являются ли обе ссылки нулевыми. Вы должны получить что-то вроде этого:

1,2,3,"Does it have wings?"
2,0,0,"a bird"
3,4,0,"Does it purr?"
4,0,0,"a cat"

Обратите внимание, что если вы используете подход с последовательными целыми числами, сохранение идентификатора узла может быть излишним, как показано здесь. Вы можете просто расположить их по ID.

Чтобы восстановить с диска, прочитайте строку, затем добавьте ее в дерево. Вам, вероятно, понадобится таблица или массив для хранения узлов с прямой ссылкой, например. при обработке узла 1 вам нужно будет отслеживать 2 и 3, пока вы не сможете заполнить эти значения.

person Frank Ames    schedule 03.12.2008

Самый произвольный простой способ — это всего лишь базовый формат, который можно использовать для представления любого графа.

<parent>,<relation>,<child>

Ie:

"Is it Red", "yes", "does it have wings" 
"Is it Red", "no" , "does it swim"

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

Единственное, что вам действительно нужно смотреть, это то, что вы случайно не сгенерируете цикл;)

Если только ты этого не хочешь.

Проблема здесь заключается в восстановлении дерева впоследствии. Если я создаю объект «есть ли у него крылья» после прочтения первой строки, мне нужно каким-то образом найти его, когда позже я столкнусь со строкой, читающей «есть ли у него крылья», «да», «есть ли у него клюв?»

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

[0x1111111 "Is It Red"           => [ 'yes' => 0xF752347 , 'no' => 0xFF6F664 ], 
 0xF752347 "does it have wings"  => [ 'yes' => 0xFFFFFFF , 'no' => 0x2222222 ], 
 0xFF6F664 "does it swim"        => [ 'yes' => "I Dont KNOW :( " , ... etc etc ]

Тогда связь «дочерний/родительский» — это просто метаданные.

person Kent Fredric    schedule 03.12.2008
comment
Проблема здесь заключается в восстановлении дерева впоследствии. Если я создаю объект «Есть ли у него крылья» после прочтения первой строки, мне нужно каким-то образом найти его, когда позже я столкнусь со строкой, читающей «Есть ли у него крылья, да, есть ли у него клюв?» - person slim; 03.12.2008

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

person Aaron    schedule 09.12.2009

Я бы сохранил дерево так:

<node identifier>
node data
[<yes child identfier>
  yes child]
[<no child identifier>
  no child]
<end of node identifier>

где дочерние узлы являются просто рекурсивными экземплярами вышеперечисленного. Биты в [] являются необязательными, а четыре идентификатора являются просто константами/значениями перечисления.

person Skizz    schedule 03.12.2008

Вот код C++ с использованием PreOrder DFS:

void SaveBinaryTreeToStream(TreeNode* root, ostringstream& oss)
{
    if (!root)
    {
        oss << '#';
        return;
    }

    oss << root->data;
    SaveBinaryTreeToStream(root->left, oss);
    SaveBinaryTreeToStream(root->right, oss);
}
TreeNode* LoadBinaryTreeFromStream(istringstream& iss)
{
    if (iss.eof())
        return NULL;

    char c;
    if ('#' == (c = iss.get()))
        return NULL;

    TreeNode* root = new TreeNode(c, NULL, NULL);
    root->left  = LoadBinaryTreeFromStream(iss);
    root->right = LoadBinaryTreeFromStream(iss);

    return root;
}

В main() вы можете сделать:

ostringstream oss;
root = MakeCharTree();
PrintVTree(root);
SaveBinaryTreeToStream(root, oss);
ClearTree(root);
cout << oss.str() << endl;
istringstream iss(oss.str());
cout << iss.str() << endl;
root = LoadBinaryTreeFromStream(iss);
PrintVTree(root);
ClearTree(root);

/* Output:
               A

       B               C

   D               E       F

     G           H   I
ABD#G###CEH##I##F##
ABD#G###CEH##I##F##
               A

       B               C

   D               E       F

     G           H   I
 */

DFS легче понять.

*********************************************************************************

Но мы можем использовать сканирование уровня BFS с использованием очереди

ostringstream SaveBinaryTreeToStream_BFS(TreeNode* root)
{
    ostringstream oss;

    if (!root)
        return oss;

    queue<TreeNode*> q;
    q.push(root);

    while (!q.empty())
    {
        TreeNode* tn = q.front(); q.pop();

        if (tn)
        {
            q.push(tn->left);
            q.push(tn->right);
            oss << tn->data;
        }
        else
        {
            oss << '#';
        }
    }

    return oss;
}
TreeNode* LoadBinaryTreeFromStream_BFS(istringstream& iss)
{
    if (iss.eof())
        return NULL;

    TreeNode* root = new TreeNode(iss.get(), NULL, NULL);
    queue<TreeNode*> q; q.push(root); // The parents from upper level
    while (!iss.eof() && !q.empty())
    {
        TreeNode* tn = q.front(); q.pop();

        char c = iss.get();
        if ('#' == c)
            tn->left = NULL;
        else
            q.push(tn->left = new TreeNode(c, NULL, NULL));

        c = iss.get();
        if ('#' == c)
            tn->right = NULL;
        else
            q.push(tn->right = new TreeNode(c, NULL, NULL));
    }

    return root;
}

В main() вы можете сделать:

root = MakeCharTree();
PrintVTree(root);
ostringstream oss = SaveBinaryTreeToStream_BFS(root);
ClearTree(root);
cout << oss.str() << endl;
istringstream iss(oss.str());
cout << iss.str() << endl;
root = LoadBinaryTreeFromStream_BFS(iss);
PrintVTree(root);
ClearTree(root);

/* Output:
               A

       B               C

   D               E       F

     G           H   I
ABCD#EF#GHI########
ABCD#EF#GHI########
               A

       B               C

   D               E       F

     G           H   I
 */
person Peter Lee    schedule 29.09.2013