ps. This article is a translation of the article https://llimllib.github.io/pymag-trees/ , the original text is in python language, the translator uses JavaScript instead, and adds the translator’s details at the end of the article For those who need to draw tree structures such as mind maps, don't miss it.
When I needed to draw some trees for my project, I thought there must be a classic and simple algorithm, but eventually I found something interesting: the layout of the tree is not just an NP-complete problem, in the drawing of the tree The algorithm has a long and interesting history behind it. Next, I will introduce the tree drawing algorithms that have appeared in history one by one, try each of them, and finally implement a tree drawing algorithm with a complete complexity of O(n)
.
what is the problem?
Give us a tree T
, all we have to do is try to draw it so that others can understand it at a glance, every algorithm in this article will give the tree node a (x,y)
coordinate, so after the algorithm runs it can Draw it on the screen, or print it out.
To store the results of the tree drawing algorithm, we create a DrawTree
data structure to mirror the tree we draw. The only thing we have to assume is that each tree node can iterate over its children. The basic implementation of DrawTree
is as follows:
// 代码1
class DrawTree {
constructor(tree, depth = 0) {
this.x = -1
this.y = depth
this.tree = tree
this.children = tree.children.map((child) => {
return new DrawTree(child, depth + 1)
})
}
}
As our method becomes more and more complex, the complexity of DrawTree
will also increase. Now, it just assigns the x
coordinate of each node as -1
, and the y
coordinate as its depth in the tree, and stores A reference to the tree node. Then, it recursively creates a DrawTree
for each node, thus building a list of that node's children. In this way, we build a DrawTree
to represent the tree that will be drawn, and add specific drawing information to each node.
As we implement better algorithms in this article, we'll use principles drawn from everyone's experience to help us build the next better algorithm, although generating a "pretty" treemap is a matter of taste, these Principles will still help us optimize program output.
The story begins with Knuth
What we're going to draw is a special type with the root node at the top, its children below it, and so on, this kind of graph, and the solution to this kind of problem, thanks in large part to Donald Knuth
, we'll start with Here he derives the first two principles:
Principle 1: Tree edges should not cross
Principle 2: Nodes of the same depth should be drawn on the same horizontal line, which makes the tree structure clearer
The algorithm of Knuth
is simple and fast, but it is only suitable for binary trees, and it will generate some quite deformed graphs. It is a simple in-order traversal of the tree. Set a global count variable to be used as the coordinate of x
. The counter will follow The node is incremented, the code is as follows:
// 代码2
let i = 0
const knuth_layout = (tree, depth) => {
if (tree.left_child) {
knuth_layout(tree.left_child, depth + 1)
}
tree.x = i
tree.y = depth
i += 1
if (tree.right_child) {
knuth_layout(tree.right_child, depth + 1)
}
}
As you can see in the image above, the tree generated by this algorithm satisfies the principle 1, but it's not very aesthetic, you can see that the graph of
Knuth
horizontally quickly because it doesn't reuse x
coordinates, even though this makes the tree significantly narrower, To avoid wasting space like this, we can derive a third principle:
Principle 3: Trees should be drawn as compactly as possible
a brief review
Before we move on to more advanced algorithms, let's stop and understand some terminology, first, when describing the relationship between our data nodes, we will use the family tree analogy, a node can have children below it, and the left Or there can be sibling nodes on the right, and a parent node on it.
We've discussed in-order traversals of trees, and next we'll see preorder and postorder traversals, three terms you may have seen in a "Data Structures" class a long time ago, but unless you've been You're dealing with trees, otherwise you might have gotten a little fuzzy on them already.
The traversal method only determines the timing of processing on a given node. In-order traversal, which is the Knuth
algorithm above, only accepts a binary tree, which means that we will process the left child first, then the current node, and then the right Child nodes, preorder traversal, means that we process the current node first, and then process all its children, and postorder traversal is just the opposite.
Finally, you may already know the concept of the uppercase O
symbol, which is used to represent the time complexity of an algorithm. In this article, we will mention it from time to time as a simple tool to judge the running time of an algorithm. Can it be accepted. If an algorithm frequently traverses all children of one of its nodes in its main loop, we call its time complexity O(n^2)
, otherwise O(n)
, if you want more details, the paper cited at the end of this article contains a lot of information about the time complexity of these algorithms.
bottom up
The two men Charles Wetherell
and Alfred Shannon
appeared in 1979
, 8 years after Knuth
proposed the tree layout algorithm, they introduced some innovative techniques, first, they showed how to generate as compact as possible that satisfies the previous three principles The tree of , by post-order traversal, only needs to maintain the position of the next node of the same depth:
// 代码3
const nexts = []
const minimum_ws = (tree, depth = 0) => {
if (nexts[depth] === undefined) {
nexts[depth] = 0
}
tree.x = nexts[depth]
tree.y = depth
nexts[depth] += 1
tree.children.forEach((child) => {
minimum_ws(child, depth + 1)
})
}
Although the tree generated by this algorithm satisfies all our principles, you might also agree that it is ugly to actually draw it, and even in a simple tree like the one above, it is difficult to quickly determine the relationship between tree nodes , and the whole tree seems to be crowded together. It's time to introduce the next principle, which will help optimize Knuth
trees and minimum-width trees:
Principle 4: The parent node should be in the middle of the child node
So far, we have been able to use a very simple algorithm to draw the tree because we don't really consider each node itself, we rely on a global counter to prevent nodes from overlapping, in order to satisfy the principle that the parent node is in the middle of the child node, we need to consider Each node's own context state, then some new strategy is needed.
The first strategy introduced by Wetherell
and Shannon
is to build the tree from the bottom through a postorder traversal of the tree, not top to bottom like code 2, or through the middle like
code 3, as long as you start with this Looking at the tree this way, then centering the parent node is a simple operation, just split the
x
coordinates of its child nodes in half.
But we have to remember that when building the right side of the tree, pay attention to the left side of the tree, as shown in the image above, the right side of the tree is pushed to the right to accommodate the left side, to achieve this separation, Wetherell
and Shannon
at The base of code 2 maintains the next available point via an array, but only uses the next available position if centering the parent tree would cause the right side of the tree to overlap the left side.
Mods
and Rockers
Before we look at more code, let's take a closer look at the result of building the tree from the bottom up, if the node is a leaf, we'll give it the next available x
coordinate, and if it's a branch, center it on Above its children, however, if centering the branch would cause it to collide with another part of the tree, we need to move it correctly enough to avoid the collision.
When we move the branch correctly, we need to move all of its children, otherwise we will lose the central parent we've been struggling to maintain. It's easy to write a function that moves the branch and its subtrees correctly:
// 代码4
const move_right = (branch, n) => {
branch.x += n
branch.children.forEach((child) => {
move_right(child, n)
})
}
The above function works, but there is a problem, if we use this function to move a subtree to the right, we will do recursion (move tree) in recursion (place tree node), which means our algorithm is very inefficient , the time complexity is O(n²)
.
In order to solve this problem, we will add a mod
attribute to each node, when we reach a branch we need to move n
spaces correctly, we will add x
coordinates to n
and assign it to the mod
attribute, and continue happily Execute the layout algorithm, since we're moving from the bottom up, we don't need to worry about collisions at the bottom of the tree (which we've proven they don't), we'll wait a bit to move them correctly.
Once the first tree traversal has been performed, we start the second traversal process, moving the branch that needs to be moved correctly, since we've only traversed each node once, and doing just arithmetic, we can be sure of it The time complexity is the same as the first time, which is O(n)
, so the two combined are still O(n)
.
The following code demonstrates centering the parent node and using the mod
attribute to improve the efficiency of the code:
// 代码5
class DrawTree {
constructor(tree, depth = 0) {
this.x = -1;
this.y = depth;
this.tree = tree;
this.children = tree.children.map((child) => {
return new DrawTree(child, depth + 1);
});
this.mod = 0;
}
}
const setup = (tree, depth = 0, nexts = {}, offset = {}) => {
tree.children.forEach((child) => {
setup(child, depth + 1, nexts, offset);
});
tree.y = depth;
let place;
let childrenLength = tree.children.length
if (childrenLength <= 0) {
place = nexts[depth] || 0;
tree.x = place;
} else if (childrenLength === 1) {
place = tree.children[0].x - 1;
} else {
let s = tree.children[0].x + tree.children[1].x;
place = s / 2;
}
offset[depth] = Math.max(offset[depth] || 0, (nexts[depth] || 0) - place);
if (childrenLength > 0) {
tree.x = place + offset[depth];
}
if (nexts[depth] === undefined) {
nexts[depth] = 0;
}
nexts[depth] += 2;
tree.mod = offset[depth];
};
const addmods = (tree, modsum = 0) => {
tree.x = tree.x + modsum;
modsum += tree.mod;
tree.children.forEach((child) => {
addmods(child, modsum);
});
};
const layout = (tree) => {
setup(tree);
addmods(tree);
return tree;
};
Tree as Block
While it does produce good results in many cases, code 5 also produces some weird trees like the one above (sorry, the graph has been lost in the years), another understanding of the
Wetherell-Shannon
algorithm The difficulty is that the same tree structure, when placed in different parts of the tree, may draw different structures. To solve this problem, we will borrow a principle from the papers of Edward Reingold
and John Tilford
:
Principle 5: No matter where the same subtree is in the tree, the result of drawing should be the same
Although this will expand our draw width, this principle will help them convey more information. It also helps to simplify bottom-up traversal, e.g. once we have calculated the x
coordinates of a subtree, we just need to move it left or right as a unit.
The following is the general process of the algorithm of code 6:
- postorder traversal of the tree
- If a node is a leaf node, then give it a
x
coordinate with the value0
Otherwise, move its right subtree as close to the left subtree as possible without conflicting
- Using the same
mod
method as before, move the tree inO(n)
time
- Using the same
- Place a node in the middle of its children
- Traverse the tree one more time and add the accumulated
mode
value to thex
coordinate
This algorithm is simple, but to execute it, we need to introduce some complexity.
contour
The outline of a tree refers to a list of the largest or smallest coordinates on one side of the tree. As shown above, there are two trees that overlap each other. If we go along the left side of the left tree and take the smallest
x
coordinate for each layer, we can get To get [1, 1, 0]
, we call it the left contour of the tree. If we go along the right side and take the
x
coordinate of the far right of each layer, we can get [1, 1, 2]
, which is the right contour of the tree.
In order to find the left contour of the right tree, we also take the x
coordinates of the leftmost node of each layer, and we can get [1, 0, 1]
. At this time, we can see that the contour has an interesting feature, that is, not all nodes are connected by a parent-child relationship. 0
on the second layer is not the parent node of 1
on the third layer.
If I were to connect the two trees according to code 6, we can find the right contour of the left tree, and the left contour of the right tree, then we can easily find what we need to push the right tree to the right so that it doesn't The minimum value that overlaps the tree on the left, the following code is a simple implementation:
// 代码7
const lt = (a, b) => {
return a < b
}
const gt = (a, b) => {
return a > b
}
// [a, b, c],[d, e, f] => [[a, d], [b, e], [c, f]]
const zip = (a, b) => {
let len = Math.min(a.length, b.length)
let arr = []
for(let i = 0; i < len; i++) {
arr.push([a[i], b[i]])
}
return arr
}
const push_right = (left, right) => {
// 左边树的右轮廓
let wl = contour(left, lt)
// 右边树的左轮廓
let wr = contour(right, gt)
let res = zip(wl, wr)
let arr = res.map((item) => {
return item[0] - item[1]
})
return Math.max(...arr) + 1
}
// 获取一棵树的轮廓
const contour = (tree, comp, level = 0, cont = null) => {
// 根节点只有一个,所以直接添加
if (cont === null) {
cont = [tree.x]
} else if (cont.length < level + 1) {// 该层级尚未添加,直接添加
cont.push(tree.x)
} else if (comp(cont[level], tree.x)) {// 该层级已经有值,所以进行比较
cont[level] = tree.x
}
tree.children.forEach((child) => {
contour(child, comp, level + 1, cont)
})
return cont
}
If we run the push_right
method with the two trees above, we can get the right contour of the tree on the left [1, 1, 2]
and the left contour of the tree on the right [1, 0, 1]
, then compare these lists to find the maximum space between them, and add a space for padding. For the two trees in the image above, pushing the right tree two spaces to the right will prevent it from overlapping the left tree.
new thread
Using code 7, we can correctly find the value of how far we need to push the right tree, but in order to do this, we need to scan each node of the two subtrees to get the contour we need, because of the time complexity it takes Most likely
O(n^2)
, Reingold
and Tilford
introduced a confusing concept for this, called threads, which have a completely different meaning than threads used for parallel execution.
Threading is a method that reduces the time required to scan a subtree contour by creating links between nodes on the contour (if one of the nodes is not already a child of the other), as shown above, the dotted line represents a Threads, while solid lines indicate parent-child relationships.
We can also take advantage of the fact that if one tree is deeper than the other, we just need to go down to the shorter tree. Anything deeper does not require the two trees to be separated since there can be no conflict between them.
Using threads and traversing to a shorter tree, we can get the outline of a tree and set up threads in linear time complexity using the code below.
// 代码8
const nextright = (tree) => {
if (tree.thread) {
return tree.thread
} else if (tree.children.length > 0) {
return tree.children[tree.children.length - 1]
} else {
return null
}
}
const nextleft = (tree) => {
if (tree.thread) {
return tree.thread
} else if (tree.children.length > 0) {
return tree.children[0]
} else {
return null
}
}
const contour = (left, right, max_offset = 0, left_outer = null, right_outer = null) => {
if (left_outer === null) {
left_outer = left
}
if (right_outer === null) {
right_outer = right
}
if (left.x - right.x > max_offset) {
max_offset = left.x - right.x
}
let lo = nextleft(left)
let li = nextright(left)
let ri = nextleft(right)
let ro = nextright(right)
if (li && ri) {
return contour(li, ri, max_offset, lo, ro)
}
return max_offset
}
It is obvious to see that this process only visits two nodes at each level in the subtree being scanned.
combine them
Code 8 computes the contours concisely and quickly, but it doesn't work with the
mod
way we discussed earlier, because a node's actual x
coordinate is the node's x
value plus the 0623c25116e8bc value on the path from it to the root node The sum of all mod
values. To solve this problem, we need to add some complexity to the contour algorithm.
The first thing we need to do is to maintain two additional variables, the sum of the mod
mod
on the right subtree, which are used to calculate the actual position of each node on the contour. is required so we can check if it collides with a node on the other side:
// 代码9
const contour = (left, right, max_offset = null, loffset = 0, roffset = 0, left_outer = null, right_outer = null) => {
let delta = left.x + loffset - (right.x + roffset)
if (max_offset === null || delta > max_offset) {
max_offset = delta
}
if (left_outer === null) {
left_outer = left
}
if (right_outer === null) {
right_outer = right
}
let lo = nextleft(left_outer)
let li = nextright(left)
let ri = nextleft(right)
let ro = nextright(right_outer)
if (li && ri) {
loffset += left.mod
roffset += right.mod
return contour(li, ri, max_offset, loffset, roffset, lo, ro)
}
return [li, ri, max_offset, loffset, roffset, left_outer, right_outer]
}
The other thing we want to do is return the current state of the function on exit so we can set the appropriate offset on the thread node. With this information, we can look at this function, which uses code 8 to get the two trees as close together as possible:,
// 代码10
const fix_subtrees = (left, right) => {
let [li, ri, diff, loffset, roffset, lo, ro] = contour(left, right)
diff += 1
diff += (right.x + diff + left.x) % 2
right.mod = diff
right.x += diff
if (right.children.length > 0) {
roffset += diff
}
if (ri && !li) {
lo.thread = ri
lo.mod = roffset - loffset
} else if (li && !ri) {
ro.thread = li
ro.mod = loffset - roffset
}
return (left.x + right.x) / 2
}
After we run the contouring process, we add 1 to the maximum difference between the left and right trees, so they don't collide, and if the middle point is odd, then add 1, which makes it easier for us to test - All nodes have integer x
coordinates without loss of precision.
Then we move the right tree by the corresponding distance to the right, remember that the reason we add diff
to the x
coordinate and also save diff
to the mod
attribute is that the mod
value is only used for nodes below the current node. If the right subtree has more than one child, we add the difference to roffset
, because all the children of the right node have to move that far to the right.
If the left side of the tree is deeper than the right, or vice versa, we need to set up a thread. We just check if the node pointer on one side is advancing further than the node pointer on the other side, and if so, set the thread from the outside of the shallow tree to the inside of the deep tree.
In order to properly handle the mod
value we mentioned earlier, we need to set a special mod
value on the thread node, and since we've updated our right offset value to reflect the movement of the right tree, all we need to do is set the The thread node's mod
value is set to the difference between the offset of the deeper tree and itself.
Now that we have the proper code to find the outline of the tree and place the two trees as close together as possible, we can easily implement the algorithm described above. I'll render the rest of the code uncommented:
// 代码11
const layout = (tree) => {
return addmods(setup(tree))
}
const addmods = (tree, mod = 0) => {
tree.x += mod
tree.children.forEach((child) => {
addmods(child, mod + tree.mod)
})
return tree
}
const setup = (tree, depth = 0) => {
if (tree.children.length === 0) {
tree.x = 0
return tree
} else if (tree.children.length === 1) {
tree.x = setup(tree.children[0], depth + 1).x
return tree
}
left = setup(tree.children[0], depth + 1)
right = setup(tree.children[1], depth + 1)
tree.x = fix_subtrees(left, right)
return tree
}
Extend to N-ary tree
Now that we finally have an algorithm for drawing a binary tree that satisfies all our principles, looks good in most cases, and has linear time complexity, it is natural to think of how to extend it to support arbitrarily many children tree of nodes. If you've been seeing this, you're probably wondering if we just need to apply the wonderful algorithm we just defined to all the children of the node.
Extend the previous algorithm to work on polytrees:
- postorder traversal of the tree
- If the node is a leaf node, then give it a
x
coordinate with a value of0
- Otherwise, iterate over its children, placing their children as close as possible to their left siblings
- Place the parent node in the middle of its leftmost and rightmost children
This algorithm works and is fast, but there is a problem, it fills all the subtrees of the node to the left as far as possible, if the rightmost node collides with the leftmost node, then the middle tree will be filled to the right. Let's solve this problem by adopting the last principle of drawing trees:
Principle 6: Children of the same parent node should be evenly spaced
To draw an N-ary tree symmetrically and quickly, we need to use all the tricks we've learned so far, plus some new ones, thanks to a recent paper by Christoph Buchheim
et al. We already have All the knowledge reserves to do this and still be able to do it in linear time.
Modifying the above algorithm so that it satisfies principle 6, we need a way to separate the trees between two large conflicting trees, the simplest way is to divide the available space by the tree whenever two trees conflict , then move each tree so that it is separated from its neighbors by that distance. For example, in the picture above, there is a distance n
between the big trees on the right and the left, and there are 3 trees between them. If we put the distance between the first tree and the leftmost distance n/3
, the next one and this An interval n/3
, and so on, yields a tree that satisfies Principle 6.
Every simple algorithm we've seen in this article so far, we've found its shortcomings, and this one is no exception, if we had to move all the trees between every two conflicting trees, we An operation of O(n^2)
complexity will be introduced into the algorithm.
The solution to this problem is similar to the shift problem we solved earlier. We introduce mod
, instead of moving each subtree in the middle every time there is a conflict, we save the value of the tree we need to move in the middle, and after placing the Apply after all nodes.
In order to correctly find the distance we need to move the middle node, we need to be able to find the number of trees between two conflicting nodes, when we only have two trees, it is clear that all conflicts occur in the left and right trees In between, when there are arbitrary trees, it becomes a challenge to figure out which tree caused the conflict.
To address this challenge, we'll introduce a default ancestor variable and add another member to the tree's data structure, let's call it ancestor
, ancestor
either points to itself or the root of the tree it belongs to, when we need to find a When the node belongs to which tree, use it if this property is set, otherwise use default_ancestor
.
When we place the first subtree of a node, we point default_ancestor
to this subtree, assuming that if there is a conflict in the next tree, it must happen with the first tree, when we place the second tree After that, we distinguish two cases, if the second subtree is not as deep as the first one, we traverse its right contour and set the ancestor
attribute to the root of the second tree, otherwise, the second tree is bigger than the first tree deep, which means anything that conflicts with the next tree will be placed in the second tree, so we just set default_ancestor
to point to it.
Without further ado, let's take a look at a tree drawing algorithm proposed by Buchheim
with a time complexity of O(n)
:
Please see the next section :)
Summarize
In this article, I omitted some things because I felt it was more important for the final algorithm to try and present a logical progression, rather than reloading the article with pure code. If you want to see more details, or want to know the data structure of the trees used in the various code listings, you can go to https://github.com/llimllib/pymag-trees/ This repository downloads the code for each algorithm Source code, some basic tests, and the code used to generate the tree images for this article.
quote
1 K. Marriott, NP-Completeness of Minimal Width Unordered Tree Layout, Journal of Graph Algorithms and Applications, vol. 8, no. 3, pp. 295-312 (2004). http://www.emis.de/journals/JGAA/accepted/2004/MarriottStuckey2004.8.3.pdf
2 D. E. Knuth, Optimum binary search trees, Acta Informatica 1 (1971)
3 C. Wetherell, A. Shannon, Tidy Drawings of Trees, IEEE Transactions on Software Engineering. Volume 5, Issue 5
4 E. M. Reingold, J. S Tilford, Tidier Drawings of Trees, IEEE Transactions on Software Engineering. Volume 7, Issue 2
5 C. Buchheim, M. J Unger, and S. Leipert. Improving Walker's algorithm to run in linear time. In Proc. Graph Drawing (GD), 2002. http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.16.8757
Translator's personal performance
Detailed breakdown of the algorithm
Although the author has spent so much time to the final algorithm, but if it is released directly, there is a high probability that it is still incomprehensible, so the translator will try to break it .
The node classes are as follows, be sure to take a closer look at right()
and left()
methods:
// 树节点类
class DrawTree {
constructor(tree, parent = null, depth = 0, number = 1) {
// 节点名称
this.name = tree.name;
// 坐标
this.x = -1;
this.y = depth;
// 子节点
this.children = tree.children.map((child, index) => {
return new DrawTree(child, this, depth + 1, index + 1);
});
// 父节点
this.parent = parent;
// 线程节点,也就是指向下一个轮廓节点
this.thread = null;
// 根据左兄弟定位的x与根据子节点中间定位的x之差
this.mod = 0;
// 要么指向自身,要么指向所属树的根
this.ancestor = this;
// 记录分摊偏移量
this.change = this.shift = 0;
// 最左侧的兄弟节点
this._lmost_sibling = null;
// 这是它在兄弟节点中的位置索引 1...n
this.number = number;
}
// 关联了线程则返回线程节点,否则返回最右侧的子节点,也就是树的右轮廓的下一个节点
right() {
return (
this.thread ||
(this.children.length > 0
? this.children[this.children.length - 1]
: null)
);
}
// 关联了线程则返回线程节点,否则返回最左侧的子节点,也就是树的左轮廓的下一个节点
left() {
return (
this.thread || (this.children.length > 0 ? this.children[0] : null)
);
}
// 获取前一个兄弟节点
left_brother() {
let n = null;
if (this.parent) {
for (let i = 0; i < this.parent.children.length; i++) {
let node = this.parent.children[i];
if (node === this) {
return n;
} else {
n = node;
}
}
}
return n;
}
// 获取同一层级第一个兄弟节点,如果第一个是自身,那么返回null
get_lmost_sibling() {
if (
!this._lmost_sibling &&
this.parent &&
this !== this.parent.children[0]
) {
this._lmost_sibling = this.parent.children[0];
}
return this._lmost_sibling;
}
// 同一层级第一个兄弟节点
get leftmost_sibling() {
return this.get_lmost_sibling();
}
}
Entering the first recursion, the processing is as follows:
1. The current node is a leaf node and has no left sibling, and x is set to 0
2. The current node is a leaf node and has a left brother, and x is the x of the left brother plus the spacing, that is, positioning according to the left brother
3. The current node is not a leaf node and has no left sibling, x is the x of the first child node plus the x of the last child node divided by 2, that is, positioning according to the child node
4. The current node is not a leaf node and has a left brother, x is the x of the left brother plus the distance, and mod is set to the difference between the positioning of x relative to the child node
// 第一次递归
const firstwalk = (v, distance = 1) => {
if (v.children.length === 0) {
// 当前节点是叶子节点且存在左兄弟节点,则其x坐标等于其左兄弟的x坐标加上间距distance
if (v.leftmost_sibling) {
v.x = v.left_brother().x + distance;
} else {
// 当前节点是叶节点无左兄弟,那么x坐标为0
v.x = 0;
}
} else {
// 后序遍历,先递归子节点
v.children.forEach((child) => {
firstwalk(child);
});
// 子节点的中点
let midpoint =
(v.children[0].x + v.children[v.children.length - 1].x) / 2;
// 左兄弟
let w = v.left_brother();
if (w) {
// 有左兄弟节点,x坐标设为其左兄弟的x坐标加上间距distance
v.x = w.x + distance;
// 同时记录下偏移量(x坐标与子节点的中点之差)
v.mod = v.x - midpoint;
} else {
// 没有左兄弟节点,x坐标直接是子节点的中点
v.x = midpoint;
}
}
return v;
};
The second recursion adds the mod
value to x
, so that the parent node is still centered on the child node:
// 第二次遍历
const second_walk = (v, m = 0, depth = 0) => {
// 初始x值加上所有祖宗节点的mod值(不包括自身的mod)
v.x += m;
v.y = depth;
v.children.forEach((child) => {
second_walk(child, m + v.mod, depth + 1);
});
};
The whole process is two recursion:
const buchheim = (tree) => {
let dt = firstwalk(tree);
second_walk(dt);
return dt;
};
Node position after first recursion:
Node position after second recursion:
The subtrees with obvious conflicts can be seen to be G
and P
subtrees. The subtree P
needs to move a certain distance to the right. How to calculate this distance?
1. Enter the second layer of subtree
G
andP
, find the rightmost child node of subtreeG
in this layer, which isF
, and find the leftmost child node of subtreeP
in this layer, which isI
, Compare theirx
coordinates, the originalx
value plus the sum of themod
values of their ancestor nodes, and find that there is no intersection, so enter the next layer.2. Enter the third layer, and also find the rightmost child node of subtree
G
in this layer, which isE
, and the leftmost child node of subtreeP
in this layer, which isJ
. Compare theirx
and find that There is an intersection, this difference plus the node intervaldistance
is the distance that the subtreeP
needs to move to the right3. Repeat the above until the bottom layer.
So how to find the leftmost or rightmost node of each layer as quickly as possible? Of course, direct recursion is possible, but the time complexity is nonlinear, so the concept of thread mentioned above is introduced.
The G
node in the above figure is used as an example to introduce the connection process of the thread. After returning from its child node C
, because C
has no left brother, it will not be processed. Enter the F
node recursively, and then process the F
node after returning from the F
node, which exists. Left brother C
, since every tree has inside and outside, we set four pointers:
vInnerLeft
is the left sibling node of the current node, and vOuterLeft
is the leftmost sibling node of the current node. Of course, for the F
node, these two pointers both point to the C
node, and both vInnerRight
and vOuterRight
initially point to the current node.
Next we set the thread from the outside of the shallow tree to the inside
1. Because C
and F
nodes have child nodes, this layer still cannot determine which tree is deep and which is shallow, so it moves down one layer and updates four pointers at the same time. Here, the left()
or right()
method:
There are four pointers here, how to judge whether there is a next layer, because we want to check the node conflict is based on the comparison of the inner nodes of the two trees, so here we only need to check the two inner node pointers to determine whether there is a next layer One layer, we only need to go to the shallower tree to stop, and the deeper nodes of the other tree will not conflict, so we can judge whether both vInnerLeft.right()
and vInnerRight.left()
exist.
2. After moving down one layer, it is found that the leaf node of F
has been reached. Then we will judge and repeat our principle:
Set threads from the outside of the shallow tree to the inside of the deep tree
The shallow tree is the F
subtree, and the deep tree is the C
subtree, then set it from the outside of F
to the inside of C
, that is, connect the E
node and the A
node through a thread.
The specific judgment rules are:
2.1. If
vInnerLeft.right()
node (that is, the next node of the right contour of the tree where theB
node is located, it exists here, it is theA
node) exists, andvOuterRight.right()
node (that is, the next node of the right contour of the tree where theE
node is located) , does not exist here) does not exist, then set the threadthread
attribute on thevOuterRight
node to point to thevInnerLeft.right()
node, which just meets this condition, soE.thread
points to theA
node.2.2. Otherwise, if
vOuterLeft.left()
node (that is, the next node of the left contour of the tree where theB
node is located, which exists here, is theA
node) does not exist, andvInnerRight.left()
node (that is, the next node of the left contour of the tree where theD
node is located) , which does not exist here) exists, then set the threadthread
attribute on thevOuterLeft
node to point to thevInnerRight.left()
node, obviously the conditions are not met here.
For all other nodes, use this method to judge, and finally the thread node connection on this tree is:
Because we are traversing the tree in post-order, the lower-level node threads are connected earlier. For example, when processing the O
node, the I
and J
nodes will be connected, then when the P
node is processed later, although it also goes to the I
node, However, because the I
node has thread nodes, it is not a "leaf node" to a certain extent, so I
will no longer be connected to other nodes.
// 第一次递归
const firstwalk = (v, distance = 1) => {
if (v.children.length === 0) {
// ...
} else {
v.children.forEach((child) => {
firstwalk(child);
apportion(child);// ++
});
// ...
}
// ...
}
const apportion = (v) => {
let leftBrother = v.left_brother();
// 存在左兄弟才处理
if (leftBrother) {
// 四个节点指针
let vInnerRight = v;// 右子树左轮廓
let vOuterRight = v;// 右子树右轮廓
let vInnerLeft = leftBrother;// 当前节点的左兄弟节点,左子树右轮廓
let vOuterLeft = v.leftmost_sibling;// 当前节点的最左侧的兄弟节点,左子树左轮廓
// 一直遍历到叶子节点
while(vInnerLeft.right() && vInnerRight.left()) {
// 更新指针
vInnerLeft = vInnerLeft.right()
vInnerRight = vInnerRight.left()
vOuterLeft = vOuterLeft.left()
vOuterRight = vOuterRight.right()
}
// 将线程从浅的树的外侧设置到深的树的内侧
if (vInnerLeft.right() && !vOuterRight.right()) {
vOuterRight.thread = vInnerLeft.right();
} else {
if (vInnerRight.left() && !vOuterLeft.left()) {
vOuterLeft.thread = vInnerRight.left();
}
}
}
};
After the thread nodes are connected, then you can judge whether the two trees intersect according to the outline. Also, because we are traversing in post-order, when judging whether there is a conflict in a subtree, the node thread below it must have been connected, and you can directly use.
The logic of judging according to the outline is also placed in the apportion
method:
// 第一次递归
const firstwalk = (v, distance = 1) => {
if (v.children.length === 0) {
// ...
} else {
v.children.forEach((child) => {
firstwalk(child);
apportion(child, distance);// distance++
});
// ...
}
// ...
}
const apportion = (v, distance) => {
let leftBrother = v.left_brother();
if (leftBrother) {
// ...
// 从当前节点依次往下走,判断是否和左侧的子树发生冲突
while(vInnerLeft.right() && vInnerRight.left()) {
// ...
// 左侧节点减右侧节点
let shift = vInnerLeft.x + distance - vInnerRight.x
if (shift > 0) {
// 大于0说明存在交叉,那么右侧的树要向右移动
move_subtree(v, shift)
}
}
// ...
}
}
// 移动子树
const move_subtree = (v, shift) => {
v.x += shift// 自身移动
v.mod += shift// 后代节点移动
}
Taking node P
as an example, the process is as follows:
vInnerLeft.right()
exists ( H.right()=F
), vInnerRight.left()
exists ( P.left()=I
), so move down one level:
Comparing the difference between the x
coordinates of the F
and I
nodes, it can be found that they do not conflict, so continue to the next layer:
This comparison will find that E
and J
nodes conflict, so the subtree P
needs to move a certain distance to the right as a whole.
Of course, the above code is problematic, because the real final x
coordinate of a node is to add the mod
value of all its ancestors, so we add four variables to accumulate mod
value:
const apportion = (v, distance) => {
if (leftBrother) {
// 四个节点指针
// ...
// 累加mod值,它们的父节点是同一个,所以往上它们要加的mod值也是一样的,那么在后面shift值计算时vInnerLeft.x + 父节点.mod - (vInnerRight.x + 父节点.mod),父节点.mod可以直接消掉,所以不加上面的祖先节点的mod也没关系
let sInnerRight = vInnerRight.mod;
let sOuterRight = vOuterRight.mod;
let sInnerLeft = vInnerLeft.mod;
let sOuterLeft = vOuterLeft.mod;
// 从当前节点依次往下走,判断是否和左侧的子树发生冲突
while (vInnerLeft.right() && vInnerRight.left()) {
// ...
// 左侧节点减右侧节点,需要累加上mod值
let shift = vInnerLeft.x + sInnerLeft + distance - (vInnerRight.x + sInnerRight);
if (shift > 0) {
// ...
// v.mod,也就是节点P.mod增加了shift,sInnerRight、sOuterRight当然也要同步增加
sInnerRight += shift;
sOuterRight += shift;
}
// 累加当前层节点mod
sInnerRight += vInnerRight.mod;
sOuterRight += vOuterRight.mod;
sInnerLeft += vInnerLeft.mod;
sOuterLeft += vOuterLeft.mod;
}
// ...
}
};
The effect is as follows:
But this is still problematic, why? For example, for node E
, it adds the mod
value of nodes F
and H
, but the problem is that the H
node is not the ancestor of the E
node, they just pass through a thread virtual line The connection is only generated. The actual value to be added should be the mod
value of the nodes F
and G
. What should we do with this mod
Then for node A
, the real value of mod
to be accumulated should be:
B.mod + C.mod + G.mod = 1 + 2 + 3 = 6
But because of the thread connection, the actual accumulated value of mod
becomes:
E.mod + F.mod + H.mod = 0 + 4 + 0 = 4
2
, it would be beautiful to set a special mod
value on the thread nodes E
and H
to make up for the difference. Anyway, because there are no child nodes under them, no matter what you set for them None of the mod
values have any effect. So how is this special mod
value calculated? It's very simple. For example, when the F
node is processed for the first time, it has the left node C
, so the thread connection judgment of the nodes below them is carried out. Because they all have children, they move down one layer. At this time, the F
subtree is over. , C
subtree is not, at this time satisfies vInnerLeft.right() && !vOuterRight.right()
conditions will E
connected to A
, for C
and F
, they are the same ancestor node, the ancestor node mod
values do not control, then for A
node Say, the value of mod
that it really wants to accumulate is mod
, and the value of B.mod + C.mod
that it will add according to the thread B.mod + C.mod - F.mod
sInnerLeft - sOuterRight
E.mod + F.mod
E.mod
:
// 将线程从浅的树的外侧设置到深的树的内侧
if (vInnerLeft.right() && !vOuterRight.right()) {
vOuterRight.thread = vInnerLeft.right();
// 修正因为线程影响导致mod累加出错的问题,深的树减浅的树
vOuterRight.mod += sInnerLeft - sOuterRight// ++
} else {
if (vInnerRight.left() && !vOuterLeft.left()) {
vOuterLeft.thread = vInnerRight.left();
vOuterLeft.mod += sInnerRight - sOuterLeft// ++
}
}
At this point the effect is as follows:
There is no conflict here, but the position of H
should be centered. Obviously, it needs to move to the right. How much? The subtree P
moves shift
distance to the right, so this distance needs to be divided into three equal parts: G
, H
, P
On the distance between nodes, assuming that the number of subtrees between two conflicting subtrees is n
, then it is shift / (n + 1)
, and the subtree H
can be moved to the right by this distance.
In other words, we need to find out which two subtrees are in conflict before we can correct the tree between them. As can be seen in the above figure, the conflicting nodes are E
and J
. For the J
node, we must know that it belongs to the current top-level subtree. The tree is P
, then as long as we can find the tree to which the E
node belongs, we can see at a glance that it is the G
node, but the code has no eyes, so we can directly find it through upward recursion, but we can't do it for linear time complexity. .
We set a ancestor
attribute for each node, which initially points to itself. For the E
node, although it belongs to the vOuterRight
node in the conflict judgment, it belongs to the vInnerLeft
node in the tree to which it belongs, so in the thread connection stage, we By the way, you can set the ancestor
of the vOuterRight
node of each layer to point to the current top-level node v
, but sometimes this point may not meet P
ancestor
N
For the E
node, its ancestor
points to its parent node F
, and what we need is G
, so we set another variable default_ancestor
, when a node's ancestor
points to not meet our requirements, use default_ancestor
node, default_ancestor
initially points to the first child node of a node, and then updates the pointer when returning from each child node. If the previous child node is not as deep as the latter child node, then default_ancestor
is updated to point to the latter child node, because If there is a subtree on the right that conflicts with the left, it must be the one with the deeper one.
const firstwalk = (v, distance = 1) => {
if (v.children.length === 0) {
// ...
} else {
let default_ancestor = v.children[0]// ++初始指向第一个子节点
v.children.forEach((child) => {
firstwalk(child);
default_ancestor = apportion(child, distance, default_ancestor);// 递归完每一个子节点都更新default_ancestor
});
}
}
const apportion = (v, distance, default_ancestor) => {
let leftBrother = v.left_brother();
if (leftBrother) {
// ...
while (vInnerLeft.right() && vInnerRight.left()) {
// ...
// 节点v下面的每一层右轮廓节点都关联v
vOuterRight.ancestor = v;// ++
// ...
}
// ...
if (vInnerLeft.right() && !vOuterRight.right()) {
// ...
} else {
// ...
default_ancestor = v// ++,前面的节点没有当前节点深,那么default_ancestor指向当前节点
}
}
return default_ancestor;// ++
}
Then we can find the root node to which the conflicting node in the left tree belongs:
const apportion = (v, distance, default_ancestor) => {
let leftBrother = v.left_brother();
if (leftBrother) {
// ...
while (vInnerLeft.right() && vInnerRight.left()) {
// ...
let shift = vInnerLeft.x + sInnerLeft + distance - (vInnerRight.x + sInnerRight);
if (shift > 0) {
// 找出vInnerLeft节点所属的根节点
let _ancestor = ancestor(vInnerLeft, v, default_ancestor)// ++
move_subtree(v, shift);
// ...
}
// ...
}
// ...
}
return default_ancestor;// ++
}
// 找出节点所属的根节点
const ancestor = (vInnerLeft, v, default_ancestor) => {
// 如果vInnerLeft节点的ancestor指向的节点是v节点的兄弟,那么符合要求
if (v.parent.children.includes(vInnerLeft.ancestor)) {
return vInnerLeft.ancestor;
} else {
// 否则使用default_ancestor指向的节点
return default_ancestor
}
}
After finding out which two trees are in conflict, we can find the subtree between the two trees, and then assign shift
to them, but we still can't traverse them directly to make corrections, yes, or to maintain Linear time complexity, so you can only save the amortized data to the root nodes of the two conflicting trees first, and then wait for all their sibling nodes to complete recursively and then set it once.
const firstwalk = (v, distance = 1) => {
if (v.children.length === 0) {
// ...
} else {
let default_ancestor = v.children[0]
v.children.forEach((child) => {
firstwalk(child);
default_ancestor = apportion(child, distance, default_ancestor);
});
// 将shift分摊添加到中间节点的x及mod值上
execute_shifts(v)// ++
// ...
}
}
const apportion = (v, distance, default_ancestor) => {
let leftBrother = v.left_brother();
if (leftBrother) {
// ...
while (vInnerLeft.right() && vInnerRight.left()) {
// ...
if (shift > 0) {
let _ancestor = ancestor(vInnerLeft, v, default_ancestor)
move_subtree(_ancestor, v, shift);// ++
// ...
}
// ...
}
// ...
}
return default_ancestor;// ++
}
const execute_shifts = (v) => {
let change = 0
let shift = 0
// 从后往前遍历子节点
for(let i = v.children.length - 1; i >= 0; i--) {
let node = v.children[i]
node.x += shift
node.mod += shift
change += node.change// change一般为负值
shift += node.shift + change// 越往左,节点添加的shift值越小
}
}
const move_subtree = (leftV, v, shift) => {
let subTrees = v.number - leftV.number// 索引相减,得到节点之间被分隔的数量
let average = shift / subTrees// 平分偏移量
v.shift += shift// 完整的shift值添加到v节点的shift属性上
v.change -= average// v左边的节点从右往左要添加的偏移量是递减的,所以是加上负的average
leftV.change += average// v.change减了average,为了不影响leftV左侧的节点,这里需要恢复
// ...
};
What this process, the next assumption FIG Example view P
node final calculated shift = 3
, then P.number - G.number = 4 - 1 = 3
, assessed value of the intermediate node 3 / 3 = 1
, node G
to P
node between the distance to be equal, then, H
need to move to the right 1
, H2
to move 1 + 1
, so that their coordinates are 1,3,5,7
, arithmetic sequence, the spacing is equal, if there are more nodes, and so on, because the node to the right moves its own 1
, and is also replaced by the previous n
nodes Pushing n * 1
to the right, we save these two values on nodes G
and P
:
Then execute the execute_shifts
method to traverse the child nodes of 0623c25116f8eb from back to Q
:
1.
change=0
,shift=0
, first update last nodeP2
:P2.x
andP2.mod
plusshift
, namely plus0
, updatechange
:change + P2.change = 0 + 0 = 0
, updateshift
:shift + P2.shift + change = 0 + 0 + 0 = 0
2. Update
P
nodes:P.x
andP.mod
plusshift
, that is, add0
, updatechange
:change + P.change = 0 + (-1) = -1
,shift + P.shift + change = 0 + 3 + (-1) = 2
shift
3. Update
H2
nodes:H2.x
andH2.mod
plusshift
, that is, add2
, updatechange
:change + H2.change = -1 + 0 = -1
,shift + H2.shift + change = 2 + 0 + (-1) = 1
shift
4. Update
H
node:H.x
andH.mod
plusshift
, that is, add1
, updatechange
:change + H.change = -1 + 0 = -1
,shift + H.shift + change = 1 + 0 + (-1) = 0
shift
5. Update
G
nodes:G.x
andG.mod
plusshift
, that is, add0
, updatechange
:change + G.change = -1 + 1 = 0
, updateshift
:shift + G.shift + change = 0 + 0 + 0 = 0
6. Update
G0
nodes:G0.x
andG0.mod
plusshift
, that is, add0
, updatechange
:change + G0.change = 0 + 0 = 0
, updateshift
:shift + G0.shift + change = 0 + 0 + 0 = 0
The above is the translator's understanding of the aftermath, and the final effect:
x
and y
:
Implement a mind map
The above algorithm cannot be directly applied to mind maps, because the size of each node of the tree considered above is the same, and the width and height of each node of the mind map may be different, which needs to be based on the above algorithm. Make some modifications on the above, because this article is already very long, so I won't go into details. The online example https://wanglin2.github.io/tree_layout_demo/ , the complete code is in https://github.com/wanglin2/ tree_layout .
Reference link
1. native javascript implements tree layout algorithm
2. tree interface drawing algorithm (2) Simple and clear First-Second
3. Tree Interface Drawing Algorithm (3) Grinding
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。