Between work, free time and other side projects I've worked withbinary treesđź‡§đź‡· They are a very useful data structure in many areas, but most relevant in this case, they lend themselves to all sorts of tweaks and tweaks. Among the many possible questions is the seemingly simple "How do you serialize a tree?" for interprocess communication, a web-based systemAPIor simply for the joy of it. And with such a serialization, how do you reconstruct the original tree from it?

One way is to express each node as a list with three elements: the node's value, its left child, and its right child. Each of these child elements is its own three-element list (or some kind of*file*value) and so on. The end of this serialization will have a special one*the business*:`]]]]]]]`đź‡§đź‡· something weI've seen it before.

I don't mind square brackets per se, but as some have said, "flat is better than nested".[1]And there are wonderfully flat ways to describe binary trees. This article describes how to plot the how and then build a binary tree from some lists without requiring any additional structure information.

## depth-first search

But before we get into the actual construction of trees, we need to briefly explain how to serialize a tree. A favorite among pollsters, depth-first search is elegant as a recursive algorithm. It's the easiest way to traverse all the nodes in a tree, extracting their values â€‹â€‹along the way.

A simple binary search tree

The traversal starts at the root node (

`4`

) and recursively visits the left children before the right children. Small differences in the order of getting the values â€‹â€‹of the current node and the child node produce significantly different results.

Starting at the root of the tree, the algorithm visits each node by first visiting the left child and proceeding recursively there. Upon completion, the correct child is visited and returned to. This corecursion creates a path descending along a left edge, methodically jumping to the next unexplored right branch, and repeating this process until all nodes are covered.

The algorithm avoids costly comparisons because it only requires simple equality checks to determine that there is a child node to descend to and that memory usage is linear with the height of the tree. In the worst case, this height is equal to the number of nodes in the tree (each child is a left or right child), but as the tree grows larger*balanced*, the height is approaching*The (record n)*, as well as the memory requirements.

Traversing this path returns the visited nodes (or their values, depending on the requirement). For a left-to-right scrolling algorithm, there are three different options here:

**an order**: creates the node itself from the left child and then from the right child; for a binary search tree, the nodes are returned in ascending order;**make a request**: creates the node itself, the left child, then the right child; that describes the*order of visits*Knot, also known asTopological Classification;**release order**: Returns the left child, the right child, and then the node itself. For expression trees, this gives a reverse-polished notation that can be easily evaluated with a small stack.[2]

For simplicity, we keep the traversals in order and pre-sorted, and run them through a simple binary search tree as follows:

from ... to __Future__ import Remarksfrom ... to Datenklassen import Datenklassefrom ... to typewriting import none, Optionalclass @dataClass Es: valeria: none there went: Optional[Es] = none Right: Optional[Es] = nonefinally an order(es): e es es Not none: acting an order(es.there went) Production es.valeria acting an order(es.Right)finally make a request(es): e es es Not none: Production es.valeria acting make a request(es.there went) acting make a request(es.Right)

It should be clear from the code that the time complexity of these functions is linear with the number of nodes in the tree. Each recursion either creates a single node or does nothing as it goes through the leaf node. There are exactly n+1 instances of the last type, which asymptotically brings the total number of operations to 2n*An)*.

the result of`an order`is a generator that outputs the tree values â€‹â€‹from left to right. Caught in a list it seems`[1, 2, 3, 4, 5, 7]`đź‡§đź‡· On the other hand,`make a request`returns the nodes in the order visited, resulting in a sequence like this:`[4, 2, 1, 3, 5, 7]`.

## Construction only on pre-order

No before-after order sequencing unambiguously describes the underlying tree. In a tree with different elements, both preorder and postorder paired with inorder are sufficient to uniquely describe the tree.

â€”Wikipedia,Kerachse

This seems like a pretty bold claim if we look at the pre-order sequence we generated for the binary search tree example. It is quite possible to create an algorithm for reconstructing the tree, provided of course that it only has distinct elements (if this is not the case, an unambiguous reconstruction without structure information is impossible).

Remember that a pre-order sequencing has the nodes in the order visited, so we can build an algorithm to append each next node to the tree as we build it. We need to backtrack a little (after the behavior of the depth-first search algorithm) for which we remain stacked:

**context**: Create an empty stack and set the root of the tree- Take the first value from the pre-order sequencing, this is the
*Those* - Create a node from it and place it on the stack
- save as
*Those*j*Real*es

- Take the first value from the pre-order sequencing, this is the
**Exit**: Take the next value and compare it to the current node value**kleiner**: descent along the left bank- Create a new node from this value and push it onto the stack
- Set it as the left child of the current node and make it the current node

**greater**: Go back to the correct branch point and go right- Look at the stack, if the top node is smaller, remove it and make it current. repeat until the stack is empty or has a larger value at the top
- Create a new node from this value and push it onto the stack
- Set it as the right child of the current node and make it the current node

**Hand back**: Once all values â€‹â€‹in the sequence have been consumed, the is returned*Those*es.

Put that all in Python and it looks like this:

from ... to collections import for this reasonfinally construct_from_preorder(Values): Values = repeated(Values) Those = es = Es(next(Values)) align = for this reason([Those]) Pro valeria no Values: e valeria < es.valeria: es.there went = es = Es(valeria, Pater=es) align.append left(es) most: Tempo align j valeria > align[0].valeria: es = align.slam to the left() es.Right = es = Es(valeria, Pater=es) align.append left(es) hand back Those

In terms of complexity, the memory footprint is the same as drilling down, branching deeper, or more general.*The (record n)*assuming a well balanced balanced tree. In terms of time complexity, there is the outer loop, which is clearly linear (no recursion, all function calls are*O(1)*đź‡§đź‡· Changing the water is a setback`Tempo`

Loop that can be any length at any point in the process. However, we cannot go further back than we went down the tree (in other words,*append*j*Pop*each node at most once), then this limit must also be linear, for an asymptotic time complexity of*An)*.

What you'll notice here is that we can reconstruct the tree based on a property specific to binary search trees: the order of the children. Children with smaller values â€‹â€‹go to the left, larger ones to the right. The quote at the beginning of this section refers to*generic*Binary trees where there are no guarantees of descending order.

For a generic binary tree it is impossible to unambiguously reconstruct it from its pre-order sequencing alone, since different trees can be the source of the pre-order sequencing and there is not enough information to clarify:

All these binary trees share a pre-order sequencing (`[2, 1, 3]`đź‡§đź‡· Only one of them is a conforming binary.*Search for*tree, which is what we usually refer to when we talk about a binary tree, but they are all valid trees.

## Another try

Of course, we cannot rely on the order of individual values â€‹â€‹from any individual sequencing. The solution to this problem must come from the inherent properties of the two distinct sequences, or more specifically from the differences between them. Let's look at what we know about each sequence, how they differ, and how we can use these properties to our advantage.[3]

### 1: drop to the left*make a request*parties*an order*

left descender

The initial structure of the order

`[1,2,3,4]`

and pre-order sequence

`[4,2,1,3]`

đź‡§đź‡· Take the order value 1 and build from the pre-order until you reach this value.

- Ă–
*make a request*The sequence starts at the root node - Ă–
*an order*The sequence begins at the leftmost node

This means that we can, in principle, read values â€‹â€‹from the*make a request*Sequence and expand the tree along a left edge to the current one*an order*value is reached. The figure at right shows how the initial left descent of this tree is constructed from this rule.

### 2: Move right when*make a request*parties*an order*

- Ă–
*make a request*the sequence contains nodes along the traced path - Ă–
*an order*Sequence scans the tree horizontally from left to right

While the current values â€‹â€‹of*make a request*j*an order*the sequences are identical, the next node is to the right of the current node, further down the tree or higher up the tree. To accommodate the case where the next node is further up the tree, we need to manage a stack of nodes. We're going to expand this stack each time we traverse the path on the left, so we can go back and append a node on the right.

### 3: Go back and expand to the right

Extension at the back and right

The next value in order is

`2`

, which is at the top of the stack, so let's go back. The next value in order is

`3`

, a new expansion destination. This is also the next value in the*make a request*Sequence and appended as right child.

Expanding on that last observation: If we follow that*make a request*Sequence to "far left" and the values â€‹â€‹of both sequences are now identical, the next value in*an order*the order will be...

- ...the top value on the stack, which means the algorithm must return to that node and keep choosing between the
*an order*Sequence; - ... not on the stack and therefore a right descendant (although not necessarily an immediate child) of the current node.

This last situation is similar to that of the root, with one small difference: the*First*value of*make a request*The sequence is appended to the right of the current node. From there the tree expands along the left edge using values â€‹â€‹from the*make a request*order so far*an order*value is reached. This can be the first value used to create the right node.

At the end of it*make a request*j*an order*the string values â€‹â€‹are equal, which is a covered case. As soon as one of the sequences is completely consumed, the composition is complete.

### Implementing the Algorithm

From these ground rules and observations, we can create a Python implementation that builds a binary tree`make a request`j`an order`Iterate

finally construct_from_preorder_inorder(make a request, an order): pre_iter = repeated(make a request) Those = es = Es(next(pre_iter)) align = for this reason([es]) Right = NOT CORRECT Pro I guess no an order: e align j I guess == align[0].valeria: es = align.slam to the left() Right = Real consequences Pro pvalor no pre_iter: e Right: es.Right = es = Es(pvalor) most: es.there went = es = Es(pvalor) e Right := pvalor == I guess: rest align.append left(es) hand back Those

The structure of this function is very similar to our function that builds a binary search tree from just one*make a request*the defender:

- An iterator is retrieved
*make a request*string (in support`next`

and continuous iteration) - ONE
*Those*the node is created and also assigned as*Real* - A stack is created and used to drive the trace

A novelty in this algorithm is the variable*Right*, which we use to indicate that the next node will be added as the right child node instead of the default left one. O*an order*The sequence only repeats in a single infinite loop, so you don't have to create an explicit iterator for it.

The main loop is divided into two branches, similar to the previous example:

**return**: If he*an order*the value is the same as the current value on the stack, we need to return to that node. We also know that the next node to the right will be appended (from this node or one higher in the tree) because the*an order*the sequence searches the tree from left to right.**extension**: If we don't go backwards, we go downstream*an order*Value. The first of these steps could be a step to the right (if we only went backwards), but the others only go to the left. Once we reach the river*an order*value, we`rest`

and establish`Right = Real`

.

There is a bit of redundancy in the configuration of the`Right`Variable during rollback (rather than just at the end of expansion). This covers the case of a tree where the root is also the leftmost node. If he*an order*The sequence is guaranteed to be a list, the initial value for`Right`can be configured`es.valeria == an order[0]`

However.

## Finally

Sometimes all it takes is a few lines of "banal untruth" to send you down a rabbit hole that will keep you busy for days. I realized my misunderstanding immediately, but I was hooked by then. Finding the relevant algorithm would have been quicker and easier, and all other questions would have been easily resolved with a few keyword searches. From time to time, however, it can be as educational as it is fun to hunt down the rabbit hole and discover its secrets. This is how we learn best.

After this hunt, I've been looking for other solutions or documents on this topic for a long time, but I haven't found many (many algorithms of varying clarity, but few explanations). What I found was an article by Erkki MĂ¤kinen from 1989.Construction of a binary tree from its pathsâ€ť, which offers an algorithm similar to those explained above, but with the*make a request*sequence in the outer loop. This paper mentions two others that are said to be less efficient at one point (*O(n^2)*) or spaces (unspecified), but remain locked behind a paywall.

## footnotes

[1] | SincePython-Zen. |

[2] | Converting expression trees to serialized form has been a recent topic of interest to me.SQLAlchemy-Hybrid-Dienstprogrammetakes SQLALchemy expressions and converts them to a serialized format that can be evaluated against Python objects instead of being run against the database. This allows for a much simpler (shorter) way of defining a particular class of hybrid properties. |

[3] | The process here is much more complicated than the distilled results. It's a lot of trial and error: sheets of paper with lots of graphics scrawled and lots of cards cut out to simulate different approaches until something works, until something works.cliques. |

#### Related posts:

- Building binary trees with relative branch coding
- Create a forest from a single seed
- Set zealous standards for SQLAlchemy ORM models
- Merge ordered lists