二维表格转树形结构算法

二维表格转树形结构算法

原始需求是前端同学在 VUE 开发中使用 ElementUI 的树形组件时,组件需要一次性将整个树形组件的数据全部加载,这就要求后端将数据一次性返回并且必须是树形数据结构。我是理解不了 ElementUI 组件的逻辑,我自己的树形组件可以按需加载,即点击树中某一个节点时才加载这个节点下的所有数据。反抗是没有用的,老实实现需求就是了。

查询数据表的 SQL 语句如下:

SELECT id AS project_id, parent_project_id, project_name FROM qross_projects;

这个表通过字段parent_project_id实现了树形目录中的上下级关系,从而可以实现无限级的树形数据结构。顶层目录的parent_project_id值为0

要生成的树形目录数据结构应该是这样的(实际上远比这个要复杂得多):

[
    {
        "id": 1
        "project_name": "项目 A"
        "parent_project_id": 0,
        "children": [
            {
                "id": 3
                "project_name": "项目 A-1"
                "parent_project_id": 1,
                "children": []
            },
            {
                "id": 4
                "project_name": "项目 A-2"
                "parent_project_id": 1,
                "children": [
                    {
                        "id": 5
                        "project_name": "项目 A-2-1"
                        "parent_project_id": 4,
                        "children": []
                    },
                    {
                        "id": 6
                        "project_name": "项目 A-2-2"
                        "parent_project_id": 4,
                        "children": []
                    }
                ]
            },
        ]
    },
    {
        "id": 2
        "project_name": "项目 B"
        "parent_project_id": 0,
        "children": []
    }
]

使用children属性来保存子节点的所有信息,没有子节点时那么children则为空数组。

开始想如何实现,第一种方法是广度优先搜索,一层一层遍历节点和子节点,直到最后一层的所有节点都没有子节点;第二种方法是深度优先搜索,查找每个节点的第一个子节点是否有子节点是否有数据,如果有则继续向下查找,如果没有,则跳到下一个同胞节点向下查找,直到所有节点都没有子节点为止。记得上人工智能课时老师还留了这道题当作业,好吧,这两个方法都不可取,因为每一次查找都要访问一次数据库,即使不访问数据库,在内存中遍历也是一件不小的工程。我当时的作业每一种算法都有上百行代码,这显然作为一个有强迫症的程序员是不能容忍的。

开始思考第三种算法,一次性从数据库获取数据,且要求在内存中的查找数据的次数尽可能少,即时间复杂度尽可能低。最后尝试使用 引用类型的特性 实现,下面是的代码(Scala):

    /**
     * 按照指定的列做为分层依据将表格转成一个树形结构
     * @param   parentColumn    父级列的列名
     * @param   startPoint      父组列起始层的值,支持整数和字符串
     * @param   newColumn       新列的 id
     */
    def turnToTree(primaryColumn: String, parentColumn: String, startPoint: String, newColumn: String): java.util.List[java.util.Map[String, Any]] = {
        //id -> data
        val relations = new mutable.HashMap[String, java.util.Map[String, Any]]()
        //[data]
        val result: java.util.List[util.Map[String, Any]] = new util.ArrayList[util.Map[String, Any]]()
        //第一次未找到的项
        val stash = new mutable.ArrayBuffer[java.util.Map[String, Any]]()

        this.rows.foreach(row => {
            val parent = row.getString(parentColumn)
            val map = row.toJavaMap
            map.put(newColumn, new util.ArrayList[Any]())

            relations += row.getString(primaryColumn) -> map
            if (parent == startPoint) {
                //顶级
                result.add(map)
            }
            else if (relations.contains(parent)) {
                //子项
                relations(parent).get(newColumn).asInstanceOf[java.util.ArrayList[Any]].add(map)
            }
            else {
                //如果找不到,有可能其父级还在后面,先暂存
                stash += map
            }
        })

        //再次遍历
        stash.foreach(map => {
            val parent = map.get(parentColumn).toString
            if (relations.contains(parent)) {
                relations(parent).get(newColumn).asInstanceOf[java.util.ArrayList[Any]].add(map)
            }
            else {
                //实在找不到,就变成顶级
                result.add(map)
            }
        })

        stash.clear()
        relations.clear()
        this.clear()

        result
    }

以上代码的解释如下:

  1. 函数参数primaryColumn表示主键的名字,本例中为project_idparentColumn表示父级列的列名,本例中为parent_project_idstartPoint为查询起始点,本例中为0,用这个值来确认是否为顶层项;newColumn为生成新列的名称,用于保存所有子节点数据,本例中为children
  2. 这是类 DataTable 的其中的一个方法,DataTable 可以理解从数据库获取的一个二维表格,代码中的this即表示当前表格,this.rows表示当前表格的所有行。
  3. 使用relations保存每一项的实体系,是一个 HashMap 结构。Key 是节点主键project_id的值,Value 则是实体,包含节点的所有属性。
  4. 使用result保存最终要返回的结果,是一个对象数组。
  5. 需要两次遍历,第一次遍历,可能无法确定上下级关系,需要第一次遍历。第一次遍历时无法确认上下级关系的实体保存在stash中。
  6. 第一次遍历,初步确认下一级关系,判断是顶层还是子项,map变量即每一个实体。如果是顶层那么就添加到result中,如果是子项则在relations中找到父级实体并添加为这个实体的子项。由于引用类型的特性,resultrelations中引用的实体是同一个,如果更新了relations中的实体,那么result中的保存的值也跟着更新了。
  7. 第二次遍历是因为数据顺序的问题,有可能主键值小的实体是主键值大的实体的子项。最好的办法是跟 SQL 语句配合,先在数据库中进行一次排序,然后再在程序中加工。例如 SQL 语句可以修改为 SELECT id AS project_id, parent_project_id, project_name FROM qross_projects ORDER BY parent_project_id ASC, id ASC;
  8. 最后返回值result则包含了所有顶层的实体,顶层实体中包含了自己的所有子项。由于引用类型的特性,虽然三个集合中项都指向相同的实体,但清空relationsstash并不影响reulst的结果。

本例是 PQLSharp 表达式 TO TREE 的底层实现。

原文地址:http://www.qross.cn/blog/20210710

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值