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 offilevalue) and so on. The end of this serialization will have a special onethe 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".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.
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 largerbalanced, the height is approachingThe (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 theorder of visitsKnot, 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.
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 2nAn).
the result ofan orderis 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 requestreturns 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.
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 theThose
- Create a node from it and place it on the stack
- save asThosejReales
- 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
- kleiner: descent along the left bank
- Hand back: Once all values in the sequence have been consumed, the is returnedThosees.
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.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 areO(1)🇧🇷 Changing the water is a setback
TempoLoop 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,appendjPopeach node at most once), then this limit must also be linear, for an asymptotic time complexity ofAn).
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 togenericBinary 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 fortree, which is what we usually refer to when we talk about a binary tree, but they are all valid trees.
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.
1: drop to the leftmake a requestpartiesan order
The initial structure of the order
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 requestThe sequence starts at the root node
- Öan orderThe sequence begins at the leftmost node
This means that we can, in principle, read values from themake a requestSequence and expand the tree along a left edge to the current onean ordervalue is reached. The figure at right shows how the initial left descent of this tree is constructed from this rule.
2: Move right whenmake a requestpartiesan order
- Ömake a requestthe sequence contains nodes along the traced path
- Öan orderSequence scans the tree horizontally from left to right
While the current values ofmake a requestjan orderthe 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 is2
, which is at the top of the stack, so let's go back. The next value in order is3
, a new expansion destination. This is also the next value in themake a requestSequence and appended as right child.
Expanding on that last observation: If we follow thatmake a requestSequence to "far left" and the values of both sequences are now identical, the next value inan orderthe order will be...
- ...the top value on the stack, which means the algorithm must return to that node and keep choosing between thean orderSequence;
- ... 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: theFirstvalue ofmake a requestThe sequence is appended to the right of the current node. From there the tree expands along the left edge using values from themake a requestorder so faran ordervalue is reached. This can be the first value used to create the right node.
At the end of itmake a requestjan orderthe 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 treemake a requestjan orderIterate
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.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 onemake a requestthe defender:
- An iterator is retrievedmake a requeststring (in support
nextand continuous iteration)
- ONEThosethe node is created and also assigned asReal
- A stack is created and used to drive the trace
A novelty in this algorithm is the variableRight, which we use to indicate that the next node will be added as the right child node instead of the default left one. Oan orderThe 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 hean orderthe 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 thean orderthe sequence searches the tree from left to right.
- extension: If we don't go backwards, we go downstreaman orderValue. 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 riveran ordervalue, we
Right = Real.
There is a bit of redundancy in the configuration of theRightVariable 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 hean orderThe sequence is guaranteed to be a list, the initial value forRightcan be configured
es.valeria == an orderHowever.
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 themake a requestsequence 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.
|||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.|
|||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.|
- Building binary trees with relative branch coding
- Create a forest from a single seed
- Set zealous standards for SQLAlchemy ORM models
- Merge ordered lists