目录
再探FirstWalk
上章介绍了算法的大体流程,这章讨论firstwork的具体行为,以及firstwalk中的重量级函数apportion怎样去修正节点的偏移。
原文使用了15个节点的树状图用来测试,本项目继续沿用,节点大小为20,节点间间距为40。根据上章介绍的定位方法计算prelim和modifier,下面直接翻译原来的测试用例:
A.prelim = 0; //无左兄弟无子节点
A.modifier = 0;
B.prelim = 0;
B.modifier = 0;
C.prelim = 0 + 20 + 40 = 60; //有左兄弟无子节点,B.prelim+节点宽度+间距
C.modifier = 0;
D.prelim = 0 + 20 + 40 = 60;//有左兄弟有子节点,A.prelim+节点宽度+间距,计算修正字段modifier
D.modifier = 60 - (0 + 60) / 2;//D.prelim - (B.prelim + C.prelim)/ 2,左节点定位位置减去子节点定位位置 (apportion未生效)
这样的计算可以持续到节点M,在计算到N时apportion生效, 这里直接讨论N节点计算时发生的事情,之前节点运算结果如下:
A | B | C | D | E | F | G | H | I | J | K | L | M | |
prelim | 0 | 0 | 60 | 60 | 30 | 90 | 0 | 0 | 60 | 120 | 180 | 240 | 60 |
modifier | 0 | 0 | 0 | 30 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -60 |
Apportion
apportion函数在firstwalk中,当前节点既有左兄弟又有子节点时发生的一次位置调整,具体逻辑是当子节点定位x坐标大于左兄弟定位x坐标时,当前节点prelim和modifier均累加偏移量。
下面根据节点N描述如何计算偏移量:
1. 初次计算N.prelim = 90 + 20 + 60 = 150(F.prelim + 节点宽度 + 间距),N.modifier = 150 - (0 + 60)/ 2 = 120 (N.prelim - (G.prelim + M.prelim)/ 2);
2. 下移一个深度,N树的最左节点为G,G的左邻居为D。D根据父节点定位0 + 120 = 120(D.prelim + N.modifier),根据左邻居定位 60 + 0 + 20 + 40 = 120 (D.prelim + E.modifier + 节点宽度 + 间距),相差值为0,没有偏移量。
3. 下移一个深度,N树的最左节点为H,H的左邻居为C。H根据父节点定位0 + -60 + 120 = 60(H.prelim + M.modifier + N.modifier),根据左兄弟定位60 + 30 + 0 + 20 + 40 = 150(C.prelim + D.modifier + E.modifier + 节点宽度 + 间距),150 - 60 = 90,因此N树的偏移量为90
PS: 偏移量计算应遍历到最底层叶节点;父节点定位时modifier累加到当前节点即可,再往上modifier都是一样的,这里算法跟secondwalk类似;偏移量小于0时忽略,左移会造成节点重叠。
得到N的偏移量后,我们将N节点prelim右移90,modifier也加90,表示它的子节点需要移动这么多;对于其左兄弟们,E是C的祖宗节点,用于计算相对位置,中间的F分摊E移动的一半。(若中间有n个节点,则每个分摊1/(n + 1)的移动位置)
以上则是apportion的实现逻辑,D节点和M节点均调用过该函数,没有计算出有效偏移量。firstwalk执行结束后,计算结果如下:
A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | |
prelim | 0 | 0 | 60 | 60 | 30 | 135 | 0 | 0 | 60 | 120 | 180 | 240 | 60 | 240 | 135 |
modifier | 0 | 0 | 0 | 30 | 0 | 45 | 0 | 0 | 0 | 0 | 0 | 0 | -60 | 210 | 0 |
apportion具体代码如下:
void apportion(QString name)
{
Node curNode = nodeList.value(name);
Node leftMost = nodeList.value(curNode.firstChild); //当前节点下移一行,取最左节点
Node neighbor = nodeList.value(leftMost.leftNeighbor); //下移后左节点的左兄弟,进行比较
int CompareDepth = 1;
float leftModSum, rightModSum;
//计算当前行理论最佳位置
while (!leftMost.name.isEmpty() && !neighbor.name.isEmpty()) {
//两个节点都存在,可进行比较时,累加其父节点的modifier(即理想位置)
leftModSum = 0.0;
rightModSum = 0.0;
Node ancestorLeftmost = leftMost;
Node ancestorNeighbor = neighbor;
//从子节点往上遍历到当前深度
for(int i = 0; i < CompareDepth; i++)
{
ancestorLeftmost = nodeList.value(ancestorLeftmost.parent);
ancestorNeighbor = nodeList.value(ancestorNeighbor.parent);
if(ancestorLeftmost.name.isEmpty() || ancestorNeighbor.name.isEmpty())
continue;
rightModSum += ancestorLeftmost.modifier;
leftModSum += ancestorNeighbor.modifier;
}
float moveDistance = (neighbor.prelim + leftModSum + space + nodeSize) - (leftMost.prelim + rightModSum);
//计算后应向右偏移,当前列所有节点分摊右移
if(moveDistance > 0.0)
{
Node tempPtr = curNode;
int leftSibling = 0;
//往左数到子节点邻居的父节点,
while (!tempPtr.name.isEmpty() && tempPtr.name != ancestorNeighbor.name) {
leftSibling = leftSibling + 1;
tempPtr = nodeList.value(tempPtr.leftSibling);
}
//从当前节点数到邻居父节点,对所有节点移位
if(!tempPtr.name.isEmpty())
{
int portion = moveDistance / leftSibling;
tempPtr = curNode;
while (tempPtr.name != ancestorNeighbor.name) {//此处判定为不等,与原文相反(或者我对原文语言理解错误)
tempPtr.prelim = tempPtr.prelim + moveDistance;
tempPtr.modifier = tempPtr.modifier + moveDistance;
moveDistance = moveDistance - portion;
nodeList.insert(tempPtr.name, tempPtr);
tempPtr = nodeList.value(tempPtr.leftSibling);
}
}
else
return;
}
CompareDepth += 1;
if(leftMost.firstChild.isEmpty())
leftMost = getLeftMost(curNode, 0, CompareDepth);//当前深度,查找第一个有子节点的,取最左子节点
else
leftMost = nodeList.value(leftMost.firstChild);
//相对原文新增操作,每次深度下降刷新左邻居节点
neighbor = nodeList.value(leftMost.leftNeighbor);
}
}
后记
关于树形图的绘图算法就介绍到这里,改版后的源代码已上传至https://github.com/Iyme/nodePosition,CSDN路径为https://download.csdn.net/download/zfy920323/10678561,源码中还有关于qml的绘图部分逻辑,就不详细介绍了。
在具体实现过程中,译者调试多次才将算法具体实现,主要对逻辑理解不够深入,以及使用C++语言时,函数跳转后数据缓存没有及时刷新,导致输出结果异常。
最后再次向该书作者致敬,优秀的算法可以成为经典并一直保留,国内相关研究还有很多可以学习的地方,希望程序员以后装逼的不是82年的雪碧,而是82年的老算法。