Как сериализовать двоичное дерево
Сегодня я пошел на собеседование, где меня попросили сериализовать двоичное дерево. Я реализовал подход, основанный на массивах, где дочерние элементы узла i (нумерация в порядке обхода уровня) имели индекс 2*i для левого дочернего элемента и 2*i + 1 для правого дочернего элемента. Интервьюер казался более или менее довольным, но мне интересно, что конкретно означает сериализация? Относится ли это конкретно к сглаживанию дерева для записи на диск, или же сериализация дерева также включает в себя просто превращение дерева в связанный список, скажем. Кроме того, как бы мы сгладили дерево в (дважды) связанный список, а затем реконструировали его? Можете ли вы воссоздать точную структуру дерева из связанного списка?
12 ответов
Все эти статьи говорят в основном о части сериализации. Часть десериализации немного сложнее сделать за один проход.
Я также внедрил эффективное решение для десериализации.
Проблема: Сериализация и десериализация двоичного дерева, содержащего положительные числа.
Часть сериализации:
- Используйте 0, чтобы представить ноль.
- Сериализуйте в список целых чисел, используя обход предзаказа.
Часть десериализации:
- Входит в список целых чисел и использует рекурсивный вспомогательный метод для десериализации.
- Рекурсивный десериализатор возвращает пару (узел BTNode, int nextIndexToRead), где узел - это построенный до сих пор узел дерева, а nextIndexToRead - это позиция следующего числа, которое нужно прочитать в сериализованном списке чисел.
Ниже приведен код на Java:
public final class BinaryTreeSerializer
{
public static List<Integer> Serialize(BTNode root)
{
List<Integer> serializedNums = new ArrayList<Integer>();
SerializeRecursively(root, serializedNums);
return serializedNums;
}
private static void SerializeRecursively(BTNode node, List<Integer> nums)
{
if (node == null)
{
nums.add(0);
return;
}
nums.add(node.data);
SerializeRecursively(node.left, nums);
SerializeRecursively(node.right, nums);
}
public static BTNode Deserialize(List<Integer> serializedNums)
{
Pair pair = DeserializeRecursively(serializedNums, 0);
return pair.node;
}
private static Pair DeserializeRecursively(List<Integer> serializedNums, int start)
{
int num = serializedNums.get(start);
if (num == 0)
{
return new Pair(null, start + 1);
}
BTNode node = new BTNode(num);
Pair p1 = DeserializeRecursively(serializedNums, start + 1);
node.left = p1.node;
Pair p2 = DeserializeRecursively(serializedNums, p1.startIndex);
node.right = p2.node;
return new Pair(node, p2.startIndex);
}
private static final class Pair
{
BTNode node;
int startIndex;
private Pair(BTNode node, int index)
{
this.node = node;
this.startIndex = index;
}
}
}
public class BTNode
{
public int data;
public BTNode left;
public BTNode right;
public BTNode(int data)
{
this.data = data;
}
}
Используя обратный порядок, сериализуйте двоичное дерево. Используйте тот же обход предзаказа для десериализации дерева. Будьте осторожны с крайними случаями. Здесь нулевые узлы представлены как "#"
public static String serialize(TreeNode root){
StringBuilder sb = new StringBuilder();
serialize(root, sb);
return sb.toString();
}
private static void serialize(TreeNode node, StringBuilder sb){
if (node == null) {
sb.append("# ");
} else {
sb.append(node.val + " ");
serialize(node.left, sb);
serialize(node.right, sb);
}
}
public static TreeNode deserialize(String s){
if (s == null || s.length() == 0) return null;
StringTokenizer st = new StringTokenizer(s, " ");
return deserialize(st);
}
private static TreeNode deserialize(StringTokenizer st){
if (!st.hasMoreTokens())
return null;
String val = st.nextToken();
if (val.equals("#"))
return null;
TreeNode root = new TreeNode(Integer.parseInt(val));
root.left = deserialize(st);
root.right = deserialize(st);
return root;
}
Подход 1: Выполните обход Inorder и Preorder для поиска данных в дереве. При десериализации используйте Pre-order и сделайте BST для Inorder, чтобы правильно сформировать дерево.
Вы должны оба, потому что A -> B -> C может быть представлен как предварительный заказ, даже если структура может отличаться.
Подход 2: используйте # в качестве дозорного, где левый или правый ребенок равен нулю.....
Я пытался понять суть этого. Итак, вот моя реализация Java. Как уже упоминалось, это двоичное дерево, а не BST. Для сериализации обход по предзаказу, кажется, работает легче (для строки с "NULL" для нулевых узлов). Пожалуйста, проверьте код ниже с полным примером рекурсивных звонков. Для десериализации строка преобразуется в LinkedList, где remove(0) получает верхний элемент за время выполнения O(1). Также смотрите полный пример в комментариях к коду для десериализации. Надеюсь, что это поможет кому-то бороться меньше, чем я:) Общее время выполнения для каждого метода (сериализации и десериализации) такое же время выполнения для обхода двоичного дерева, т. Е. O(n), где n - количество узлов (записей) в дереве
import java.util.LinkedList; import java.util.List;
открытый класс SerDesBinTree {
public static class TreeEntry<T>{
T element;
TreeEntry<T> left;
TreeEntry<T> right;
public TreeEntry(T x){
element = x;
left = null;
right = null;
}
}
TreeEntry<T> root;
int size;
StringBuilder serSB = new StringBuilder();
List<String> desList = new LinkedList<>();
public SerDesBinTree(){
root = null;
size = 0;
}
public void traverseInOrder(){
traverseInOrder(this.root);
}
public void traverseInOrder(TreeEntry<T> node){
if (node != null){
traverseInOrder(node.left);
System.out.println(node.element);
traverseInOrder(node.right);
}
}
public void serialize(){
serialize(this.root);
}
/*
* 1
* / \
* 2 3
* /
* 4
*
* ser(1)
* serSB.append(1) serSB: 1
* ser(1.left)
* ser(1.right)
* |
* |
* ser(1.left=2)
* serSB.append(2) serSB: 1, 2
* ser(2.left)
* ser(2.right)
* |
* |
* ser(2.left=null)
* serSB.append(NULL) serSB: 1, 2, NULL
* return
* |
* ser(2.right=null)
* serSB.append(NULL) serSB: 1, 2, NULL, NULL
* return
*
* |
* ser(1.right=3)
* serSB.append(3) serSB: 1, 2, NULL, NULL, 3
* ser(3.left)
* ser(3.right)
*
* |
* ser(3.left=4)
* serSB.append(4) serSB: 1, 2, NULL, NULL, 3, 4
* ser(4.left)
* ser(4.right)
*
* |
* ser(4.left=null)
* serSB.append(NULL) serSB: 1, 2, NULL, NULL, 3, 4, NULL
* return
*
* ser(4.right=null)
* serSB.append(NULL) serSB: 1, 2, NULL, NULL, 3, 4, NULL, NULL
* return
*
* ser(3.right=null)
* serSB.append(NULL) serSB: 1, 2, NULL, NULL, 3, 4, NULL, NULL, NULL
* return
*
*/
public void serialize(TreeEntry<T> node){
// preorder traversal to build the string
// in addition: NULL will be added (to make deserialize easy)
// using StringBuilder to append O(1) as opposed to
// String which is immutable O(n)
if (node == null){
serSB.append("NULL,");
return;
}
serSB.append(node.element + ",");
serialize(node.left);
serialize(node.right);
}
public TreeEntry<T> deserialize(TreeEntry<T> newRoot){
// convert the StringBuilder into a list
// so we can do list.remove() for the first element in O(1) time
String[] desArr = serSB.toString().split(",");
for (String s : desArr){
desList.add(s);
}
return deserialize(newRoot, desList);
}
/*
* 1
* / \
* 2 3
* /
* 4
*
* deser(root, list) list: 1, 2, NULL, NULL, 3, 4, NULL, NULL, NULL
* root = new TreeEntry(1) list: 2, NULL, NULL, 3, 4, NULL, NULL, NULL
* root.left = deser(root.left, list) // **
* root.right = deser(root.right, list) // *-*
* return root // ^*^
*
*
* so far subtree
* 1
* / \
* null null
*
* deser(root.left, list)
* root.left = new TreeEntry(2) list: NULL, NULL, 3, 4, NULL, NULL, NULL
* root.left.left = deser(root.left.left, list) // ***
* root.left.right = deser(root.left.right, list) // ****
* return root.left // eventually return new TreeEntry(2) to ** above after the two calls are done
*
* so far subtree
* 2
* / \
* null null
*
* deser(root.left.left, list)
* // won't go further down as the next in list is NULL
* return null // to *** list: NULL, 3, 4, NULL, NULL, NULL
*
* so far subtree (same, just replacing null)
* 2
* / \
* null null
*
* deser(root.left.right, list)
* // won't go further down as the next in list is NULL
* return null // to **** list: 3, 4, NULL, NULL, NULL
*
* so far subtree (same, just replacing null)
* 2
* / \
* null null
*
*
* so far subtree // as node 2 completely returns to ** above
* 1
* / \
* 2 null
* / \
* null null
*
*
* deser(root.right, list)
* root.right = new TreeEntry(3) list: 4, NULL, NULL, NULL
* root.right.left = deser(root.right.left, list) // *&*
* root.right.right = deser(root.right.right, list) // *---*
* return root.right // eventually return to *-* above after the previous two calls are done
*
* so far subtree
* 3
* / \
* null null
*
*
* deser(root.right.left, list)
* root.right.left = new TreeEntry(4) list: NULL, NULL, NULL
* root.right.left.left = deser(root.right.left.left, list) // *(*
* root.right.left.right = deser(root.right.left.right, list) // *)*
* return root.right.left // to *&*
*
* so far subtree
* 4
* / \
* null null
*
* deser(root.right.left.left, list)
* // won't go further down as the next in list is NULL
* return null // to *(* list: NULL, NULL
*
* so far subtree (same, just replacing null)
* 4
* / \
* null null
*
* deser(root.right.left.right, list)
* // won't go further down as the next in list is NULL
* return null // to *)* list: NULL
*
*
* so far subtree (same, just replacing null)
* 4
* / \
* null null
*
*
* so far subtree
* 3
* / \
* 4 null
* / \
* null null
*
*
* deser(root.right.right, list)
* // won't go further down as the next in list is NULL
* return null // to *---* list: empty
*
* so far subtree (same, just replacing null of the 3 right)
* 3
* / \
* 4 null
* / \
* null null
*
*
* now returning the subtree rooted at 3 to root.right in *-*
*
* 1
* / \
* / \
* / \
* 2 3
* / \ / \
* null null / null
* /
* 4
* / \
* null null
*
*
* finally, return root (the tree rooted at 1) // see ^*^ above
*
*/
public TreeEntry<T> deserialize(TreeEntry<T> node, List<String> desList){
if (desList.size() == 0){
return null;
}
String s = desList.remove(0); // efficient operation O(1)
if (s.equals("NULL")){
return null;
}
Integer sInt = Integer.parseInt(s);
node = new TreeEntry<T>((T)sInt);
node.left = deserialize(node.left, desList);
node.right = deserialize(node.right, desList);
return node;
}
public static void main(String[] args) {
/*
* 1
* / \
* 2 3
* /
* 4
*
*/
SerDesBinTree<Integer> tree = new SerDesBinTree<>();
tree.root = new TreeEntry<Integer>(1);
tree.root.left = new TreeEntry<Integer>(2);
tree.root.right = new TreeEntry<Integer>(3);
tree.root.right.left = new TreeEntry<Integer>(4);
//tree.traverseInOrder();
tree.serialize();
//System.out.println(tree.serSB);
tree.root = null;
//tree.traverseInOrder();
tree.root = tree.deserialize(tree.root);
//tree.traverseInOrder();
// deserialize into a new tree
SerDesBinTree<Integer> newTree = new SerDesBinTree<>();
newTree.root = tree.deserialize(newTree.root);
newTree.traverseInOrder();
}
}
Вот поздний ответ в Python. Он использует (в первую очередь глубину) сериализацию предзаказа и возвращает список strings
, Десериализация возвращает дерево.
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
# This method serializes the tree into a string
def serialize(root):
vals = []
def encode(node):
vals.append(str(node.val))
if node.left is not None:
encode(node.left)
else:
vals.append("L")
if node.right is not None:
encode(node.right)
else:
vals.append("R")
encode(root)
print(vals)
return vals
# This method deserializes the string back into the tree
def deserialize(string_list):
def create_a_tree(sub_list):
if sub_list[0] == 'L' or sub_list[0] == 'R':
del sub_list[0]
return
parent = Node(sub_list[0])
del sub_list[0]
parent.left = create_a_tree(sub_list)
parent.right = create_a_tree(sub_list)
return parent
if len(string_list) != 0:
root_node = create_a_tree(string_list)
else:
print("ERROR - empty string!")
return 0
return root_node
Тестировать:
tree1 = Node('root', Node('left'), Node('right'))
t = deserialize(serialize(tree1))
print(str(t.right.val))
Вот еще один способ сериализации двоичного дерева с использованием (модифицированного) обхода порядка уровней. [Просто скопируйте и вставьте, все работает]. Покрывает все несбалансированное, сбалансированное, наклоненное вправо, наклоненное дерево влево.
class TreeNode():
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def getHeight(root):
if root == None:
return 0
return max(getHeight(root.left), getHeight(root.right)) + 1
treeArray = []
def levelOrderTraversal(root, level, numOfNodes):
if level <= 0 and numOfNodes <=0:
return
numOfNodes -= 1
if root != None and level == 1:
treeArray.append(root.val)
elif root == None and level == 1:
treeArray.append("$")
if root != None:
levelOrderTraversal(root.left, level-1, numOfNodes)
levelOrderTraversal(root.right, level-1, numOfNodes)
else:
levelOrderTraversal(root, level-1, numOfNodes)
levelOrderTraversal(root, level-1, numOfNodes)
def treeToIntArray(root):
h = getHeight(root)
for i in range(1, h+1):
levelOrderTraversal(root,i, i*2)
return treeArray
def intArrayToTree():
n = len(treeArray)
treeArrayOfObjects = [0]*len(treeArray)
for i in range(n):
if treeArray[i] != "$":
root = TreeNode(treeArray[i])
treeArrayOfObjects[i] = root
#Linking the child nodes
for i in range(n):
if treeArray[i] != "$":
root = treeArrayOfObjects[i]
if 2 * i + 1 < n:
root.left = treeArrayOfObjects[2*i + 1]
if 2 * i + 2 < n:
root.right = treeArrayOfObjects[2*i + 2]
treeArray[i] = root
return treeArrayOfObjects[0]
"""
root = TreeNode(7)
root.left = TreeNode(3)
root.right = TreeNode(9)
root.left.left = TreeNode(1)
root.left.right = None
root.right.left = None
root.right.right = TreeNode(4)
"""
root = TreeNode(7)
root.right = TreeNode(9)
root.right.right = TreeNode(4)
root.right.right.right = TreeNode(5)
print treeToIntArray(root)
root = intArrayToTree()
print root.val
print root.right.val
print root.right.right.val
print root.right.right.right.val
Сериализовать десериализацию двоичного дерева:
public class BTSerialization {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
serialize(root, sb);
return sb.toString();
}
private void serialize(TreeNode root, StringBuilder sb) {
if (root == null) {
sb.append("()");
return;
}
sb.append('(').append(root.val);
serialize(root.left, sb);
serialize(root.right, sb);
sb.append(')');
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if ("()".equals(data)) return null;
data = data.substring(1, data.length() - 1); // Unwrap the two parenthesis (root(left)(right))
int i = 0;
while (data.charAt(i) != '(') i++;
TreeNode root = new TreeNode(Integer.parseInt(data.substring(0, i)));
data = data.substring(i);
i = -1;
int open = 0;
while (true) { // Find left child- starts with parenthesis
i++;
if (data.charAt(i) == '(') open++;
else if (data.charAt(i) == ')') {
open--;
if (open == 0) break;
}
}
root.left = deserialize(data.substring(0, i + 1));
data = data.substring(i + 1); // The rest is right child- starts with parenthesis
root.right = deserialize(data);
return root;
}
public static void main(String[] args) {
BTSerialization b = new BTSerialization();
TreeNode node1 = new TreeNode(1);
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(5);
node1.left = node2;
node1.right = node3;
node3.left = node4;
node3.right = node5;
TreeNode root = b.deserialize(b.serialize(node1));
System.out.println();
}
}
Сериализовать десериализацию N-арного дерева: с помощью этого кода вы можете сериализовать, десериализовать любое n-арное дерево. По сути, бинарное дерево - это 2-арное дерево. N показывает максимальное количество потомков всех узлов во всем дереве.
public class CodecNry {
class Node {
public int val;
public List<Node> children;
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
}
public String serialize(Node root) {
if (root == null) return ""; // Serialization base case
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(root.val).append(",").append(root.children.size()); // Add root val+,+num of children
for (Node child : root.children)
stringBuilder.append(",").append(serialize(child)); // Add children recursively, one by one
return stringBuilder.toString(); // Return result
}
int i;
public Node deserialize(String data) {
if (data.isBlank()) return null; // Base case root is null
i = 0; // The index on the tokens
return recursiveDeserialize(data.split(",")); // Recursively build the tree from the tokenized string
}
private Node recursiveDeserialize(String[] nums) {
if (i == nums.length) return null; // Base case- no more child
Node root = new Node(Integer.parseInt(nums[i++]), new ArrayList<>()); // Build the root
int childrenCount = Integer.parseInt(nums[i++]); // Get number of children
for (int j = 0; j < childrenCount; j++) { // Add children recursively one by one
Node child = recursiveDeserialize(nums);
if (child != null) root.children.add(child); // If child is not null, add it to the children of root
}
return root; // Return the root of the tree
}
}
Я не использую предварительный заказ, но использую BFS. Это вопрос от leetcode
Большинство людей при использовании предзаказа неверны: ожидаемый результат должен быть
"[1,2,3,null,null,4,5]", но вместо этого большинство людей выводят результат как "[1,2,3,null,null,4,5,null,null]", поскольку они не считая уровней.
Вот моя реализация с правильным результатом.
class Node(object):
def __init__(self,data):
self.left = None
self.right = None
self.data = data
def serialize(root):
queue = [(root,0)]
result = []
max_level_with_value = 0
while queue:
(node,l) = queue.pop(0)
if node:
result.append((node.data,l))
queue.extend([(node.left,l+1),
(node.right,l+1)
])
max_level_with_value = max(max_level_with_value,l)
else:
result.append(('null',l))
filter_redundant(result,max_level_with_value)
def filter_redundant(result,max_level_with_value):
for v,l in result:
if l<= max_level_with_value:
print(v)
root = Node(1)
root.left = Node(2)
root.right = Node(3)
root.right.left = Node(4)
root.right.right = Node(5)
serialize(root)
Преобразовать его в массив с размером (2n + 1), каждый левый сын и правый сын будут вместо (2 * номер узла) и ((2 * номер узла) + 1 соответственно.
Лучший способ - использовать специальный символ (например, #, как упоминалось в предыдущем комментарии) в качестве стража. Это лучше, чем создание массива обхода по порядку и массива обхода по предзаказу / после заказа, как с точки зрения сложности пространства, так и с точки зрения сложности времени. это также намного проще в реализации.
Связанный список здесь не очень подходит, так как для восстановления дерева лучше иметь время доступа к элементу const
Как насчет выполнения упорядоченного обхода и помещения корневого ключа и всех ключей узла в std::list или другой контейнер по вашему выбору, который выравнивает дерево. Затем просто сериализуйте std::list или контейнер по вашему выбору, используя библиотеку boost.
Обратное просто и затем перестройте дерево, используя стандартную вставку в двоичное дерево. Это может быть не совсем эффективно для очень большого дерева, но время выполнения для преобразования дерева в std::list - самое большее O(n), а для перестройки дерева - самое большее O(log n).
Я собираюсь сделать это для сериализации дерева, которое я только что закодировал в C++, когда я конвертирую свою базу данных из Java в C++.
Сериализация - это процесс преобразования структуры данных или объекта в последовательность битов, чтобы их можно было сохранить в файле или буфере памяти или передать по ссылке сетевого подключения для последующего восстановления в той же или другой компьютерной среде.
Десериализация - это процесс преобразования строки обратно в исходную древовидную структуру.
Концепция сериализации и десериализации очень похожа на то, что компилятор делает с кодом. Весь процесс компиляции состоит из нескольких этапов, но мы постараемся сделать его абстрактным.
По заданному фрагменту кода компилятор разбивает различные четко определенные компоненты на токены (например, int - токен, double - другой токен, {- один токен, } - другой токен и т. Д.). [Ссылка на демонстрацию абстрактного уровня компиляции][1].
Сериализация: мы используем логику обхода предварительного заказа для сериализации дерева в строку. Мы добавим "X" для обозначения нулевого указателя / узла в дереве. Кроме того, чтобы иметь в виду нашу логику десериализации, нам нужно добавить "," после каждого сериализованного значения узла, чтобы процесс десериализации мог получить доступ к каждому значению узла, разделенному с помощью ",".
Ссылка Leetcode: https://leetcode.com/problems/serialize-and-deserialize-binary-tree/
Пояснение на канале Back To Back SWE на Youtube: https://www.youtube.com/watch?v=suj1ro8TIVY
For example:
You may serialize the following tree:
1
/ \
2 3
/ \
4 5
as "[1,2,null,null,3,4,null,null,5,null,null,]"
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root == null)
return "X,";
String leftSerialized = serialize(root.left);
String rightSerialized = serialize(root.right);
return root.val + "," + leftSerialized + rightSerialized;
}
private TreeNode deserializeHelper(Queue<String> queue)
{
String nodeValue = queue.poll();
if(nodeValue.equals("X"))
return null;
TreeNode newNode = new TreeNode(Integer.valueOf(nodeValue));
newNode.left = deserializeHelper(queue);
newNode.right = deserializeHelper(queue);
return newNode;
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
Queue<String> queue = new LinkedList<>();
queue.addAll(Arrays.asList(data.split(",")));
return deserializeHelper(queue);
}
}
//Codec object will be instantiated and called as such:
//Codec codec = new Codec();
//codec.deserialize(codec.serialize(root));