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

一、背景

原项目是纯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源码,如下

let node_width = 140;
let node_height = 60;
let node_spaceH = 50;
let node_spaceV = 20;

function cacluPos() {
    this.node_x = this.x;
    this.node_y = this.y + (this.height - this.node_height) / 2;
    // 递归求当前结点的子结点的位置
    if (this.children && !this.folded) {
        let y = this.y;
        this.children.forEach(child => {
            // 子结点的横坐标为当前结点横坐标 + 当前结点本身的宽度 + 横间距
            child.x = this.x + this.node_width + node_spaceH;
            // 子结点的纵坐标为前一个子结点的纵坐标 + 纵间距
            child.y = y;
            y += child.height + node_spaceV;
            // 求该结点本身的位置
            child.cacluPos();
        });
    }
}
function cacluSize() {
    if (!this.children || this.folded) {
        this.width = this.node_width;
        this.height = this.node_height;
        return;
    }
    // 当前结点的宽度,等于当前结点本身的宽度 + 横间距 + 最宽的子结点的宽度
    // 当前结点的高度,等于所有子结点的高度 + 纵间距
    let childWidth = 0;
    this.height = 0;
    this.children.forEach(child => {
        // 先求子结点的尺寸(递归)
        child.cacluSize();
        // 找到最宽的子结点
        if (childWidth < child.width) {
            childWidth = child.width;
        }
        // 各子结点的高累加
        this.height += child.height;
    });
    // 计算当前结点的宽度
    this.width = node_width + node_spaceH + childWidth;
    // 当前结点的高度,记得加上各子结点的纵轴方向上的间距
    this.height += (this.children.length - 1) * node_spaceV;
}

function traversalTree(tree, findNode) {
    let list = [tree];
    while (list.length > 0) {
        // 取出首结点
        let node = list[0];
        list = list.splice(1);
        // 找到一个结点
        findNode(node);
        // 将该结点的子结点加入列表
        if (node.children && !node.folded) {
            list.push(...node.children);
        }
    }
}

function initTree(tree) {
    // 遍历树的所有结点
    traversalTree(tree, (node) => {
        // 初始化该结点
        node.folded = false;
        node.node_width = node_width;
        node.node_height = node_height;
        node.cacluPos = cacluPos;
        node.cacluSize = cacluSize;
        // 删除无效的子结点
        if (node.children) {
            if (!(node.children instanceof Array) && node.children.length < 1) {
                delete node.children;
            }
        }
    });
}
function printTree(tree) {
    // 遍历树的所有结点
    traversalTree(tree, (node) => {
        // 打印该结点位置
        console.log(node.name + ": " + node.node_x + ", " + node.node_y + ", " + node.width + ", " + node.height);
    });
}

// 布局树的所有结点
function layoutTree(tree) {
    // 初始化树的所有结点
    initTree(tree);
    // 计算各结点宽度和高度
    tree.cacluSize();
    // 计算各结点的位置
    tree.x = 0;
    tree.y = 0;
    tree.cacluPos();
    // 打印所有结点的位置
    printTree(tree);
}

export default {
    layoutTree: layoutTree,
}

六、结点点击

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

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

七、总结

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

所以,要多写代码。

你可能感兴趣的:(JavaScript,算法,算法)