• 树的排布、展开与折叠算法


    一、背景

    原项目是纯C端程序,未来要移植成BS架构。其中一个功能是界面上展示树,基本要求是:

    (1)按层次展示树各个结点

    (2)根结点以及各个子树结点可以点击,点击后切换展开状态与折叠状态

    (3)叶子结点不能相互遮挡

    (4)折叠的子树结点也不能被遮挡

    二、经验

    很多年前,大约是2009年,用C#实现过这样的逻辑。记得当时还有个优化要求,即叶子结点可以被遮挡,但是折叠的子树结点不能被遮挡。

    当时不喜欢用递归,基本实现逻辑是:

    1、遍历该树,找到所有的叶子结点

    (1)含折叠的子树结点,不含其叶子结点

    (2)按顺序排列这些结点,要保证一个子树结点在另一个子树结点前面,则前者所有叶子结点都在后者所有叶子结点的前面。

    2、对这些叶子排序以确定Y值,X值是根据结点深度计算的

    3、遍历这些叶子结点,找到其父结点,再根据该父结点的所有子结点确定其Y值

    4、继续,最终要确定根结点的Y值

    三、解决方案

    要在B端实现这个功能,需要用js。

    非递归的算法简单,简单在算法容易理解,基本流程定下来就可以实现了,但是过程很复杂。而递归算法简单在实现过程,但是最难的是算法思路。

    递归算法基本思路如下:

    1、任意一个结点,X值由深度确定,Y值由上一个兄弟结点的位置和该结点的范围计算得到

    2、叶子结点和折叠的子树结点的范围是它本身

    3、子树结点的范围,宽度是本身的宽度加间距,再加最宽的子结点宽度;高度是其所有子结点高度加各子结点间的间距

    四、算法描述

    有了十多年代码经验,用递归还是比较轻松简单的,反而不喜欢用非递归,实现过程太麻烦了。

    算法基本过程如下:

    1、初始化树

    遍历所有结点,设置每一个结点本身的大小(位置还不能确定)、绑定计算位置的方法cacluPos和计算范围的方法cacluSize(每一个结点都可以单独计算其位置和大小)、删除无效子结点

    2、计算根结点的宽度和高度

    cacluSize方法实现:

    (1)递归结束条件:当前结点是叶子结点或该结点被折叠,范围是其本身

    (2)遍历子结点,找到最宽的子结点的宽度:

    第一步,计算各个子结点的范围

    第二步,找到最宽的子结点,记录该宽度

    第三步,累加这些子结点的高度

    (3)当前结点的宽度,等于该结点本身的宽度加横向间距,再加最宽的子结点的宽度值

    (4)当前结点的高度,等于累加到的子结点的高度,再加这些子结点的纵向间距

    3、设计根结点范围起点为(0,0),计算根结点的位置

    cacluPos方法实现:

    (1)当前结点本身的位置

    X值是其范围的X值,Y值是范围的Y值+(范围的高度-本身的高度)/2

    (2)当前结点是子树结点,且没被折叠,则遍历当前结点的子结点:

    第一步,子结点的范围X值是当前结点的X值,加当前结点的宽度,再加横向间距

    第二步,第一个子结点的范围Y值是当前结点的范围Y值;后续子结点的范围Y值,是前一子结点的范围Y值加范围高度,再加纵向间距

    第三步,计算子结点本身的位置

    五、源码分享

    直接贴上js源码,如下

    1. let node_width = 140;
    2. let node_height = 60;
    3. let node_spaceH = 50;
    4. let node_spaceV = 20;
    5. function cacluPos() {
    6. this.node_x = this.x;
    7. this.node_y = this.y + (this.height - this.node_height) / 2;
    8. // 递归求当前结点的子结点的位置
    9. if (this.children && !this.folded) {
    10. let y = this.y;
    11. this.children.forEach(child => {
    12. // 子结点的横坐标为当前结点横坐标 + 当前结点本身的宽度 + 横间距
    13. child.x = this.x + this.node_width + node_spaceH;
    14. // 子结点的纵坐标为前一个子结点的纵坐标 + 纵间距
    15. child.y = y;
    16. y += child.height + node_spaceV;
    17. // 求该结点本身的位置
    18. child.cacluPos();
    19. });
    20. }
    21. }
    22. function cacluSize() {
    23. if (!this.children || this.folded) {
    24. this.width = this.node_width;
    25. this.height = this.node_height;
    26. return;
    27. }
    28. // 当前结点的宽度,等于当前结点本身的宽度 + 横间距 + 最宽的子结点的宽度
    29. // 当前结点的高度,等于所有子结点的高度 + 纵间距
    30. let childWidth = 0;
    31. this.height = 0;
    32. this.children.forEach(child => {
    33. // 先求子结点的尺寸(递归)
    34. child.cacluSize();
    35. // 找到最宽的子结点
    36. if (childWidth < child.width) {
    37. childWidth = child.width;
    38. }
    39. // 各子结点的高累加
    40. this.height += child.height;
    41. });
    42. // 计算当前结点的宽度
    43. this.width = node_width + node_spaceH + childWidth;
    44. // 当前结点的高度,记得加上各子结点的纵轴方向上的间距
    45. this.height += (this.children.length - 1) * node_spaceV;
    46. }
    47. function traversalTree(tree, findNode) {
    48. let list = [tree];
    49. while (list.length > 0) {
    50. // 取出首结点
    51. let node = list[0];
    52. list = list.splice(1);
    53. // 找到一个结点
    54. findNode(node);
    55. // 将该结点的子结点加入列表
    56. if (node.children && !node.folded) {
    57. list.push(...node.children);
    58. }
    59. }
    60. }
    61. function initTree(tree) {
    62. // 遍历树的所有结点
    63. traversalTree(tree, (node) => {
    64. // 初始化该结点
    65. node.folded = false;
    66. node.node_width = node_width;
    67. node.node_height = node_height;
    68. node.cacluPos = cacluPos;
    69. node.cacluSize = cacluSize;
    70. // 删除无效的子结点
    71. if (node.children) {
    72. if (!(node.children instanceof Array) && node.children.length < 1) {
    73. delete node.children;
    74. }
    75. }
    76. });
    77. }
    78. function printTree(tree) {
    79. // 遍历树的所有结点
    80. traversalTree(tree, (node) => {
    81. // 打印该结点位置
    82. console.log(node.name + ": " + node.node_x + ", " + node.node_y + ", " + node.width + ", " + node.height);
    83. });
    84. }
    85. // 布局树的所有结点
    86. function layoutTree(tree) {
    87. // 初始化树的所有结点
    88. initTree(tree);
    89. // 计算各结点宽度和高度
    90. tree.cacluSize();
    91. // 计算各结点的位置
    92. tree.x = 0;
    93. tree.y = 0;
    94. tree.cacluPos();
    95. // 打印所有结点的位置
    96. printTree(tree);
    97. }
    98. export default {
    99. layoutTree: layoutTree,
    100. }

    六、结点点击

    关于结点点击的代码比较简单,就不分享了,无非是遍历结点,看看点击位置在哪个结点本身的范围内,依然可以用递归。

    广度优先遍历,先看点击位置在哪个结点范围内,再进一步精确到具体的结点,比直接比对所有结点肯定要好。

    七、总结

    该算法其实不难,但是也需要相当的代码经验才能独立开发实现。如果上网搜代码,怕是很难搜到比较满意的,也不可能刚刚满足自己的项目需求。

    所以,要多写代码。

  • 相关阅读:
    【软考软件评测师】第三章节 白盒测试测试方法
    数据集快速生成方法集合
    luogu-P1462 通往奥格瑞玛的道路 && ybt-修建道路【最短路+二分】
    win11安装jdk
    升级Kubernetes集群的Docker版本(亲测)
    ffmpeg 开发第一例
    后端接口性能优化分析
    快鲸SCRM如何助力企业高效运营私域流量?
    oracle---一表向另一表循环插入数据过程中、发现异常并抛进日志表、直至数据传输完成
    【java开发技术积累篇】之springboot项目优美的文件上传方式
  • 原文地址:https://blog.csdn.net/yjh4866/article/details/126685737