【问题标题】:What is the most efficient/elegant way to parse a flat table into a tree?将平面表解析为树的最有效/优雅的方法是什么?
【发布时间】:2010-09-16 13:45:30
【问题描述】:

假设您有一个存储有序树层次结构的平面表:

Id   Name         ParentId   Order
 1   'Node 1'            0      10
 2   'Node 1.1'          1      10
 3   'Node 2'            0      20
 4   'Node 1.1.1'        2      10
 5   'Node 2.1'          3      10
 6   'Node 1.2'          1      20

这是一个图表,我们有[id] Name。根节点 0 是虚构的。

[0] 根 / \ [1] 节点 1 [3] 节点 2 / \ \ [2] 节点 1.1 [6] 节点 1.2 [5] 节点 2.1 / [4] 节点 1.1.1

你会使用什么简约的方法将它作为正确排序、正确缩进的树输出到 HTML(或文本)?

进一步假设你只有基本的数据结构(数组和哈希图),没有带有父/子引用的花哨对象,没有 ORM,没有框架,只有你的两只手。该表表示为一个结果集,可以随机访问。

伪代码或纯英文都可以,这纯属概念问题。

额外问题:有没有更好的方法在 RDBMS 中存储这样的树结构?


编辑和添加

回答一位评论者 (Mark Bessey's) 的问题:根节点不是必需的,因为无论如何它都不会显示。 ParentId = 0 是表达“这些是顶级”的约定。 Order 列定义了具有相同父节点的节点将如何排序。

我所说的“结果集”可以被描绘成一个哈希图数组(保留在那个术语中)。因为我的例子本来就应该在那里。有些答案会加倍努力并先构建它,但没关系。

树可以任意深。每个节点可以有 N 个孩子。不过,我并没有完全考虑“数百万条目”树。

不要将我选择的节点命名(“Node 1.1.1”)误认为是可以依赖的东西。这些节点同样可以称为“Frank”或“Bob”,没有暗示命名结构,这只是为了使其可读。

我已经发布了我自己的解决方案,所以你们可以将其分解。

【问题讨论】:

  • “没有带有父/子引用的花哨对象” - 为什么不呢?使用 .addChild()、.getParent() 方法创建一个基本的 Node 对象可以让您很好地建模节点关系。
  • 它是常规(n 个子节点,其中 n 可以 > 2)树还是二叉树(节点可以有 0、1 或 2 个子节点)?
  • 既然你可以用hashmap实现一个合适的节点数据结构,这里没有真正的限制,只是更多的工作。
  • ...这正是你所做的。
  • @dreftymac,从技术上讲,传递闭包表是非规范化的。避免数据异常比传统的邻接表设计更难。但作为非规范化设计的典型特征,它使某些类型的查询更快。

标签: sql algorithm recursion tree hierarchical-data


【解决方案1】:

现在MySQL 8.0 supports recursive queries,我们可以用标准语法说all popular SQL databases support recursive queries

WITH RECURSIVE MyTree AS (
    SELECT * FROM MyTable WHERE ParentId IS NULL
    UNION ALL
    SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;

我在 2017 年的演讲 Recursive Query Throwdown 中测试了 MySQL 8.0 中的递归查询。

以下是我 2008 年的原始答案:


有几种方法可以在关系数据库中存储树形结构的数据。您在示例中显示的内容使用了两种方法:

  • 邻接列表(“父”列)和
  • 路径枚举(您的姓名列中的虚线数字)。

另一种解决方案称为嵌套集,它也可以存储在同一个表中。阅读 Joe Celko 的“Trees and Hierarchies in SQL for Smarties”,了解有关这些设计的更多信息。

我通常更喜欢一种名为 Closure Table(又名“邻接关系”)的设计来存储树形结构的数据。它需要另一个表,但是查询树很容易。

我在我的演讲 Models for Hierarchical Data with SQL and PHP 和我的书 SQL Antipatterns: Avoiding the Pitfalls of Database Programming 中介绍了 Closure Table。

CREATE TABLE ClosureTable (
  ancestor_id   INT NOT NULL REFERENCES FlatTable(id),
  descendant_id INT NOT NULL REFERENCES FlatTable(id),
  PRIMARY KEY (ancestor_id, descendant_id)
);

将所有路径存储在闭包表中,其中存在从一个节点到另一个节点的直接祖先。为每个节点包含一行以引用自身。例如,使用您在问题中显示的数据集:

INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
  (1,1), (1,2), (1,4), (1,6),
  (2,2), (2,4),
  (3,3), (3,5),
  (4,4),
  (5,5),
  (6,6);

现在你可以像这样从节点 1 开始得到一棵树:

SELECT f.* 
FROM FlatTable f 
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;

输出(在 MySQL 客户端中)如下所示:

+----+
| id |
+----+
|  1 | 
|  2 | 
|  4 | 
|  6 | 
+----+

换句话说,节点 3 和 5 被排除在外,因为它们是单独层次结构的一部分,而不是从节点 1 下降。


Re:来自 e-satis 关于直系子女(或直系父母)的评论。您可以将“path_length”列添加到ClosureTable,以便更轻松地专门查询直系子女或父母(或任何其他距离)。

INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
  (1,1,0), (1,2,1), (1,4,2), (1,6,1),
  (2,2,0), (2,4,1),
  (3,3,0), (3,5,1),
  (4,4,0),
  (5,5,0),
  (6,6,0);

然后,您可以在搜索中添加一个词来查询给定节点的直接子节点。这些是path_length 为 1 的后代。

SELECT f.* 
FROM FlatTable f 
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
  AND path_length = 1;

+----+
| id |
+----+
|  2 | 
|  6 | 
+----+

来自@ashraf 的回复:“如何[按名称] 对整棵树进行排序?”

这是一个示例查询,用于返回节点 1 的所有后代节点,将它们连接到包含其他节点属性(例如 name)的 FlatTable,并按名称排序。

SELECT f.name
FROM FlatTable f 
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;

来自@Nate 的回复:

SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f 
JOIN ClosureTable a ON (f.id = a.descendant_id) 
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id) 
WHERE a.ancestor_id = 1 
GROUP BY a.descendant_id 
ORDER BY f.name

+------------+-------------+
| name       | breadcrumbs |
+------------+-------------+
| Node 1     | 1           |
| Node 1.1   | 1,2         |
| Node 1.1.1 | 1,2,4       |
| Node 1.2   | 1,6         |
+------------+-------------+

今天有用户提出了修改建议。 SO 版主批准了编辑,但我正在撤消它。

编辑建议上面最后一个查询中的 ORDER BY 应该是ORDER BY b.path_length, f.name,大概是为了确保排序与层次结构匹配。但这不起作用,因为它会在“Node 1.2”之后订购“Node 1.1.1”。

如果您希望排序以合理的方式匹配层次结构,这是可能的,但不能简单地通过路径长度排序。例如,查看我对MySQL Closure Table hierarchical database - How to pull information out in the correct order 的回复。

【讨论】:

  • 这很优雅,谢谢。奖励积分。 ;-) 不过我看到了一个小缺点 - 因为它显式地存储子关系,所以即使是树结构的微小变化,您也需要进行大量仔细的更新。
  • 没错,在数据库中存储树结构的每种方法都需要一些工作,无论是在创建或更新树时,还是在查询树和子树时。选择您希望更简单的设计:写作或阅读。
  • @buffer,当您为层次结构创建所有行时,可能会产生不一致。 Adjacency List (parent_id) 只有一行来表达每个父子关系,而 Closure Table 有很多。
  • @BillKarwin 还有一件事,闭包表适用于具有到任何给定节点的多条路径的图(例如,任何叶或非叶节点可能属于多个父节点的类别层次结构)
  • @Reza,这样如果你添加一个新的子节点,你可以查询 (1) 的所有后代,这些都是新孩子的祖先。
【解决方案2】:

如果您使用嵌套集(有时称为修改前序树遍历),您可以使用单个查询按树顺序提取整个树结构或其中的任何子树,但插入成本更高,因为您需要管理描述通过树结构的有序路径的列。

对于django-mptt,我使用了这样的结构:

id parent_id tree_id level lft rght -- --------- ------- ----- --- ---- 1 无 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12

它描述了一个看起来像这样的树(id 代表每个项目):

1 +-- 2 | +-- 3 | +-- 4 | +-- 5 +-- 6 +-- 7

或者,作为一个嵌套的集合图,它使lftrght 值的工作方式更加明显:

__________________________________________________________________________ |根 1 | | ________________________________ ________________________________ | | |儿童 1.1 | |儿童 1.2 | | | | ___________ ___________ | | ___________ ___________ | | | | | 1.1.1 | | 1.1.2 | | | | C 1.2.1 | | 1.2.2 | | | 1 2 3_______________4 5___________6 7 8 9___________10 11______________12 13 14 | |________________________________| |________________________________| | |____________________________________________________________________________________|

如您所见,要按树顺序获取给定节点的整个子树,您只需选择所有具有lftrght 值在其lftrght 值之间的行。检索给定节点的祖先树也很简单。

为了方便起见,level 列有点非规范化,tree_id 列允许您重新启动每个顶级节点的 lftrght 编号,这减少了列数受到插入、移动和删除的影响,因为在进行这些操作以创建或关闭间隙时,必须相应地调整 lftrght 列。当我试图围绕每个操作所需的查询时,我做了一些development notes

就实际使用这些数据来显示树而言,我创建了一个tree_item_iterator 实用函数,对于每个节点,它应该为您提供足够的信息来生成您想要的任何类型的显示。

更多关于 MPTT 的信息:

【讨论】:

  • 我希望我们停止使用像 lftrght 这样的缩写来表示列名,我的意思是我们不必输入多少字符?一个?!
  • 因为'left'和'right'是SQL中的保留字
【解决方案3】:

这是一个相当古老的问题,但由于它有很多观点,我认为值得提出一个替代方案,在我看来非常优雅的解决方案。

为了读取树结构,您可以使用递归公用表表达式 (CTE)。它提供了一次获取整个树结构的可能性,具有有关节点级别、其父节点以及父节点子节点中的顺序的信息。

让我向您展示这将如何在 PostgreSQL 9.1 中工作。

  1. 创建结构

    CREATE TABLE tree (
        id int  NOT NULL,
        name varchar(32)  NOT NULL,
        parent_id int  NULL,
        node_order int  NOT NULL,
        CONSTRAINT tree_pk PRIMARY KEY (id),
        CONSTRAINT tree_tree_fk FOREIGN KEY (parent_id) 
          REFERENCES tree (id) NOT DEFERRABLE
    );
    
    
    insert into tree values
      (0, 'ROOT', NULL, 0),
      (1, 'Node 1', 0, 10),
      (2, 'Node 1.1', 1, 10),
      (3, 'Node 2', 0, 20),
      (4, 'Node 1.1.1', 2, 10),
      (5, 'Node 2.1', 3, 10),
      (6, 'Node 1.2', 1, 20);
    
  2. 写一个查询

    WITH RECURSIVE 
    tree_search (id, name, level, parent_id, node_order) AS (
      SELECT 
        id, 
        name,
        0,
        parent_id, 
        1 
      FROM tree
      WHERE parent_id is NULL
    
      UNION ALL 
      SELECT 
        t.id, 
        t.name,
        ts.level + 1, 
        ts.id, 
        t.node_order 
      FROM tree t, tree_search ts 
      WHERE t.parent_id = ts.id 
    ) 
    SELECT * FROM tree_search 
    WHERE level > 0 
    ORDER BY level, parent_id, node_order;
    

    结果如下:

     id |    name    | level | parent_id | node_order 
    ----+------------+-------+-----------+------------
      1 | Node 1     |     1 |         0 |         10
      3 | Node 2     |     1 |         0 |         20
      2 | Node 1.1   |     2 |         1 |         10
      6 | Node 1.2   |     2 |         1 |         20
      5 | Node 2.1   |     2 |         3 |         10
      4 | Node 1.1.1 |     3 |         2 |         10
    (6 rows)
    

    树节点按深度级别排序。在最终输出中,我们将在后续行中呈现它们。

    对于每个级别,它们在父级中按 parent_id 和 node_order 排序。这告诉我们如何在输出 - 链接节点中按此顺序将它们呈现给父节点。

    有了这样的结构,用 HTML 制作一个非常漂亮的演示文稿并不难。

    递归 CTE 可用于 PostgreSQL、IBM DB2、MS SQL Server 和 Oracle

    如果您想了解更多关于递归 SQL 查询的信息,可以查看您最喜欢的 DBMS 的文档,或者阅读我的两篇关于该主题的文章:

【讨论】:

    【解决方案4】:

    从 Oracle 9i 开始,您可以使用 CONNECT BY。

    SELECT LPAD(' ', (LEVEL - 1) * 4) || "Name" AS "Name"
    FROM (SELECT * FROM TMP_NODE ORDER BY "Order")
    CONNECT BY PRIOR "Id" = "ParentId"
    START WITH "Id" IN (SELECT "Id" FROM TMP_NODE WHERE "ParentId" = 0)
    

    从 SQL Server 2005 开始,您可以使用递归公用表表达式 (CTE)。

    WITH [NodeList] (
      [Id]
      , [ParentId]
      , [Level]
      , [Order]
    ) AS (
      SELECT [Node].[Id]
        , [Node].[ParentId]
        , 0 AS [Level]
        , CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
      FROM [Node]
      WHERE [Node].[ParentId] = 0
      UNION ALL
      SELECT [Node].[Id]
        , [Node].[ParentId]
        , [NodeList].[Level] + 1 AS [Level]
        , [NodeList].[Order] + '|'
          + CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
      FROM [Node]
        INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[ParentId]
    ) SELECT REPLICATE(' ', [NodeList].[Level] * 4) + [Node].[Name] AS [Name]
    FROM [Node]
      INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[Id]
    ORDER BY [NodeList].[Order]
    

    两者都会输出以下结果。

    姓名 '节点 1' '节点 1.1' '节点 1.1.1' '节点 1.2' '节点 2' '节点 2.1'

    【讨论】:

    • cte 可以在 sqlserver 和 oracle 中使用 @Eric Weilnau
    【解决方案5】:

    Bill 的答案非常好,这个答案增加了一些东西,这让我希望 SO 支持线程答案。

    无论如何,我想支持树形结构和 Order 属性。我在每个节点中包含了一个名为leftSibling 的属性,它与Order 在原始问题中的作用相同(保持从左到右的顺序)。

    mysql> desc 节点; +-------------+-------------+------+-----+------ --+----------------+ |领域 |类型 |空 |钥匙 |默认 |额外 | +-------------+-------------+------+-----+------ --+----------------+ |编号 |整数(11) |否 |优先级 |空 |自动增量 | |姓名 | varchar(255) |是 | |空 | | |左兄弟 |整数(11) |否 | | 0 | | +-------------+-------------+------+-----+------ --+----------------+ 3 行一组(0.00 秒) mysql> desc 邻接; +------------+---------+------+-----+---------+--- -------------+ |领域 |类型 |空 |钥匙 |默认 |额外 | +------------+---------+------+-----+---------+--- -------------+ |关系 ID |整数(11) |否 |优先级 |空 |自动增量 | |家长 |整数(11) |否 | |空 | | |孩子|整数(11) |否 | |空 | | |路径长度 |整数(11) |否 | |空 | | +------------+---------+------+-----+---------+--- -------------+ 4 行一组(0.00 秒)

    More detail and SQL code on my blog.

    感谢比尔,您的回答对入门很有帮助!

    【讨论】:

      【解决方案6】:

      如果有选择,我会使用对象。我将为每个记录创建一个对象,其中每个对象都有一个 children 集合,并将它们全部存储在一个 assoc 数组(/hashtable)中,其中 Id 是键。并通过集合闪电战一次,将孩子添加到相关的孩子字段中。 简单。

      但是由于限制使用一些好的 OOP 并不好玩,我可能会根据以下内容进行迭代:

      function PrintLine(int pID, int level)
          foreach record where ParentID == pID
              print level*tabs + record-data
              PrintLine(record.ID, level + 1)
      
      PrintLine(0, 0)
      

      编辑:这与其他几个条目相似,但我认为它稍微干净一些。我要补充一件事:这是 SQL 密集型的。这是讨厌如果可以选择,请走 OOP 路线。

      【讨论】:

      • 这就是我所说的“无框架”——你使用的是 LINQ,不是吗?关于您的第一段:结果集已经存在,为什么要先将所有信息复制到新的对象结构中? (我对这个事实不够清楚,抱歉)
      • Tomalak - 不,代码是伪代码。当然,您必须将事情分解为适当的选择和迭代器......以及真正的语法!为什么选择面向对象?因为您可以准确地反映结构。它让事情变得更好,而且它恰好更有效率(只有一个选择)
      • 我也没有重复选择的想法。关于 OOP:Mark Bessey 在他的回答中说:“你可以用 hashmap 模拟任何其他数据结构,所以这不是一个可怕的限制。”。您的解决方案是正确的,但我认为即使没有 OOP,也有一些改进的余地。
      【解决方案7】:

      确实有很好的解决方案可以利用 sql 索引的内部 btree 表示。这是基于 1998 年左右所做的一些伟大研究。

      这是一个示例表(在 mysql 中)。

      CREATE TABLE `node` (
        `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
        `name` varchar(255) NOT NULL,
        `tw` int(10) unsigned NOT NULL,
        `pa` int(10) unsigned DEFAULT NULL,
        `sz` int(10) unsigned DEFAULT NULL,
        `nc` int(11) GENERATED ALWAYS AS (tw+sz) STORED,
        PRIMARY KEY (`id`),
        KEY `node_tw_index` (`tw`),
        KEY `node_pa_index` (`pa`),
        KEY `node_nc_index` (`nc`),
        CONSTRAINT `node_pa_fk` FOREIGN KEY (`pa`) REFERENCES `node` (`tw`) ON DELETE CASCADE
      )
      

      树表示所需的唯一字段是:

      • tw:从左到右的 DFS 预购索引,其中 root = 1。
      • pa:对父节点的引用(使用 tw),root 为 null。
      • sz:节点分支的大小,包括它自己。
      • nc:用作语法糖。它是 tw+sz,代表节点的“下一个孩子”的 tw。

      这是一个 24 个节点的示例,按 tw 排序:

      +-----+---------+----+------+------+------+
      | id  | name    | tw | pa   | sz   | nc   |
      +-----+---------+----+------+------+------+
      |   1 | Root    |  1 | NULL |   24 |   25 |
      |   2 | A       |  2 |    1 |   14 |   16 |
      |   3 | AA      |  3 |    2 |    1 |    4 |
      |   4 | AB      |  4 |    2 |    7 |   11 |
      |   5 | ABA     |  5 |    4 |    1 |    6 |
      |   6 | ABB     |  6 |    4 |    3 |    9 |
      |   7 | ABBA    |  7 |    6 |    1 |    8 |
      |   8 | ABBB    |  8 |    6 |    1 |    9 |
      |   9 | ABC     |  9 |    4 |    2 |   11 |
      |  10 | ABCD    | 10 |    9 |    1 |   11 |
      |  11 | AC      | 11 |    2 |    4 |   15 |
      |  12 | ACA     | 12 |   11 |    2 |   14 |
      |  13 | ACAA    | 13 |   12 |    1 |   14 |
      |  14 | ACB     | 14 |   11 |    1 |   15 |
      |  15 | AD      | 15 |    2 |    1 |   16 |
      |  16 | B       | 16 |    1 |    1 |   17 |
      |  17 | C       | 17 |    1 |    6 |   23 |
      | 359 | C0      | 18 |   17 |    5 |   23 |
      | 360 | C1      | 19 |   18 |    4 |   23 |
      | 361 | C2(res) | 20 |   19 |    3 |   23 |
      | 362 | C3      | 21 |   20 |    2 |   23 |
      | 363 | C4      | 22 |   21 |    1 |   23 |
      |  18 | D       | 23 |    1 |    1 |   24 |
      |  19 | E       | 24 |    1 |    1 |   25 |
      +-----+---------+----+------+------+------+
      

      每个树的结果都可以非递归地完成。 例如,在 tw='22' 处获取节点的祖先列表

      祖先

      select anc.* from node me,node anc 
      where me.tw=22 and anc.nc >= me.tw and anc.tw <= me.tw 
      order by anc.tw;
      +-----+---------+----+------+------+------+
      | id  | name    | tw | pa   | sz   | nc   |
      +-----+---------+----+------+------+------+
      |   1 | Root    |  1 | NULL |   24 |   25 |
      |  17 | C       | 17 |    1 |    6 |   23 |
      | 359 | C0      | 18 |   17 |    5 |   23 |
      | 360 | C1      | 19 |   18 |    4 |   23 |
      | 361 | C2(res) | 20 |   19 |    3 |   23 |
      | 362 | C3      | 21 |   20 |    2 |   23 |
      | 363 | C4      | 22 |   21 |    1 |   23 |
      +-----+---------+----+------+------+------+
      

      兄弟姐妹和孩子是微不足道的 - 只需使用 tw 的 pa 字段排序。

      后裔

      例如,以 tw = 17 为根的节点集(分支)。

      select des.* from node me,node des 
      where me.tw=17 and des.tw < me.nc and des.tw >= me.tw 
      order by des.tw;
      +-----+---------+----+------+------+------+
      | id  | name    | tw | pa   | sz   | nc   |
      +-----+---------+----+------+------+------+
      |  17 | C       | 17 |    1 |    6 |   23 |
      | 359 | C0      | 18 |   17 |    5 |   23 |
      | 360 | C1      | 19 |   18 |    4 |   23 |
      | 361 | C2(res) | 20 |   19 |    3 |   23 |
      | 362 | C3      | 21 |   20 |    2 |   23 |
      | 363 | C4      | 22 |   21 |    1 |   23 |
      +-----+---------+----+------+------+------+
      

      附加说明

      当读取次数远多于插入或更新次数时,此方法非常有用。

      由于树中节点的插入、移动或更新需要对树进行调整,所以在开始操作之前需要锁定表。

      插入/删除成本很高,因为 tw 索引和 sz(分支大小)值将需要在插入点之后的所有节点上更新,并分别为所有祖先更新。

      分支移动涉及到将分支的tw值移出范围,所以在移动分支时也需要禁用外键约束。移动分支基本上需要四个查询:

      • 将分支移出范围。
      • 关闭它留下的间隙。 (剩余的树现在已标准化)。
      • 打开将要到达的间隙。
      • 将分支移到新位置。

      调整树查询

      树中间隙的打开/关闭是create/update/delete方法使用的一个重要子功能,所以我在这里包含它。

      我们需要两个参数 - 一个代表我们是缩小还是扩大规模的标志,以及节点的 tw 索引。因此,例如 tw=18(分支大小为 5)。假设我们正在缩小规模(删除 tw) - 这意味着我们在以下示例的更新中使用“-”而不是“+”。

      我们首先使用(稍作改动的)祖先函数来更新 sz 值。

      update node me, node anc set anc.sz = anc.sz - me.sz from 
      node me, node anc where me.tw=18 
      and ((anc.nc >= me.tw and anc.tw < me.pa) or (anc.tw=me.pa));
      

      那么我们需要为那些tw高于要移除的分支调整tw。

      update node me, node anc set anc.tw = anc.tw - me.sz from 
      node me, node anc where me.tw=18 and anc.tw >= me.tw;
      

      然后我们需要为那些pa的tw高于要移除的分支的人调整parent。

      update node me, node anc set anc.pa = anc.pa - me.sz from 
      node me, node anc where me.tw=18 and anc.pa >= me.tw;
      

      【讨论】:

        【解决方案8】:

        这写得很快,既不漂亮也不高效(加上它自动装箱很多,在intInteger 之间转换很烦人!),但它确实有效。

        这可能违反了规则,因为我正在创建自己的对象,但是嘿,我这样做是为了转移实际工作:)

        这还假设在您开始构建节点之前将结果集/表完全读入某种结构,如果您有数十万行,这不是最佳解决方案。

        public class Node {
        
            private Node parent = null;
        
            private List<Node> children;
        
            private String name;
        
            private int id = -1;
        
            public Node(Node parent, int id, String name) {
                this.parent = parent;
                this.children = new ArrayList<Node>();
                this.name = name;
                this.id = id;
            }
        
            public int getId() {
                return this.id;
            }
        
            public String getName() {
                return this.name;
            }
        
            public void addChild(Node child) {
                children.add(child);
            }
        
            public List<Node> getChildren() {
                return children;
            }
        
            public boolean isRoot() {
                return (this.parent == null);
            }
        
            @Override
            public String toString() {
                return "id=" + id + ", name=" + name + ", parent=" + parent;
            }
        }
        
        public class NodeBuilder {
        
            public static Node build(List<Map<String, String>> input) {
        
                // maps id of a node to it's Node object
                Map<Integer, Node> nodeMap = new HashMap<Integer, Node>();
        
                // maps id of a node to the id of it's parent
                Map<Integer, Integer> childParentMap = new HashMap<Integer, Integer>();
        
                // create special 'root' Node with id=0
                Node root = new Node(null, 0, "root");
                nodeMap.put(root.getId(), root);
        
                // iterate thru the input
                for (Map<String, String> map : input) {
        
                    // expect each Map to have keys for "id", "name", "parent" ... a
                    // real implementation would read from a SQL object or resultset
                    int id = Integer.parseInt(map.get("id"));
                    String name = map.get("name");
                    int parent = Integer.parseInt(map.get("parent"));
        
                    Node node = new Node(null, id, name);
                    nodeMap.put(id, node);
        
                    childParentMap.put(id, parent);
                }
        
                // now that each Node is created, setup the child-parent relationships
                for (Map.Entry<Integer, Integer> entry : childParentMap.entrySet()) {
                    int nodeId = entry.getKey();
                    int parentId = entry.getValue();
        
                    Node child = nodeMap.get(nodeId);
                    Node parent = nodeMap.get(parentId);
                    parent.addChild(child);
                }
        
                return root;
            }
        }
        
        public class NodePrinter {
        
            static void printRootNode(Node root) {
                printNodes(root, 0);
            }
        
            static void printNodes(Node node, int indentLevel) {
        
                printNode(node, indentLevel);
                // recurse
                for (Node child : node.getChildren()) {
                    printNodes(child, indentLevel + 1);
                }
            }
        
            static void printNode(Node node, int indentLevel) {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < indentLevel; i++) {
                    sb.append("\t");
                }
                sb.append(node);
        
                System.out.println(sb.toString());
            }
        
            public static void main(String[] args) {
        
                // setup dummy data
                List<Map<String, String>> resultSet = new ArrayList<Map<String, String>>();
                resultSet.add(newMap("1", "Node 1", "0"));
                resultSet.add(newMap("2", "Node 1.1", "1"));
                resultSet.add(newMap("3", "Node 2", "0"));
                resultSet.add(newMap("4", "Node 1.1.1", "2"));
                resultSet.add(newMap("5", "Node 2.1", "3"));
                resultSet.add(newMap("6", "Node 1.2", "1"));
        
                Node root = NodeBuilder.build(resultSet);
                printRootNode(root);
        
            }
        
            //convenience method for creating our dummy data
            private static Map<String, String> newMap(String id, String name, String parentId) {
                Map<String, String> row = new HashMap<String, String>();
                row.put("id", id);
                row.put("name", name);
                row.put("parent", parentId);
                return row;
            }
        }
        

        【讨论】:

        • 当看到大量源代码时,我总是发现很难从特定于实现的部分中筛选出特定于算法的部分。这就是为什么我首先要求一个不是特定语言的解决方案。但它确实有效,所以感谢您的时间!
        • 我现在明白你的意思了,如果主要算法在 NodeBuilder.build() 中不是很明显的话——我可能会做得更好。
        【解决方案9】:

        假设你知道根元素为零,下面是输出到文本的伪代码:

        function PrintLevel (int curr, int level)
            //print the indents
            for (i=1; i<=level; i++)
                print a tab
            print curr \n;
            for each child in the table with a parent of curr
                PrintLevel (child, level+1)
        
        
        for each elementID where the parentid is zero
            PrintLevel(elementID, 0)
        

        【讨论】:

          【解决方案10】:

          您可以使用 hashmap 模拟任何其他数据结构,所以这不是一个可怕的限制。从上到下扫描,您为数据库的每一行创建一个哈希图,每列都有一个条目。将这些哈希映射中的每一个添加到一个“主”哈希映射中,以 id 为键。如果任何节点有一个您还没有看到的“父节点”,请在主 hashmap 中为其创建一个占位符条目,并在您看到实际节点时填写它。

          要将其打印出来,请对数据进行简单的深度优先遍历,并在此过程中跟踪缩进级别。您可以通过为每一行保留一个“子”条目并在扫描数据时填充它来简化此操作。

          至于是否有“更好”的方式将树存储在数据库中,这取决于您将如何使用数据。我见过具有已知最大深度的系统,它为层次结构中的每个级别使用不同的表。如果树中的级别毕竟不是完全相等的(顶级类别与叶子不同),这很有意义。

          【讨论】:

            【解决方案11】:

            如果可以创建嵌套哈希映射或数组,那么我可以简单地从头开始向下遍历表并将每个项目添加到嵌套数组中。我必须将每一行跟踪到根节点,以便知道要插入嵌套数组中的哪个级别。我可以使用记忆,这样我就不需要一遍又一遍地查找同一个父级。

            编辑:我会先将整个表读入一个数组,所以它不会重复查询数据库。当然,如果您的表非常大,这将不实用。

            结构建好后,我必须先做一个深度遍历,打印出HTML。

            没有更好的基本方法来使用一张表存储这些信息(虽然我可能是错的;),并且希望看到更好的解决方案)。但是,如果您创建一个方案来使用动态创建的数据库表,那么您会以牺牲简单性和 SQL 地狱的风险打开一个全新的世界;)。

            【讨论】:

            • 我不希望仅仅因为需要新级别的子节点而更改数据库布局。 :-)
            【解决方案12】:

            如果元素按树形顺序排列,如您的示例所示,您可以使用类似于以下 Python 示例的内容:

            delimiter = '.'
            stack = []
            for item in items:
              while stack and not item.startswith(stack[-1]+delimiter):
                print "</div>"
                stack.pop()
              print "<div>"
              print item
              stack.append(item)
            

            它的作用是维护一个代表树中当前位置的堆栈。对于表中的每个元素,它会弹出堆栈元素(关闭匹配的 div),直到找到当前项的父项。然后它输出该节点的开始并将其推入堆栈。

            如果你想使用缩进而不是嵌套元素来输出树,你可以简单地跳过打印语句来打印 div,并在每个项目之前打印一些等于堆栈大小的倍数的空格。例如,在 Python 中:

            print "  " * len(stack)
            

            您还可以轻松地使用此方法构造一组嵌套列表或字典。

            编辑:我从您的说明中看到,这些名称并非旨在成为节点路径。这表明了另一种方法:

            idx = {}
            idx[0] = []
            for node in results:
              child_list = []
              idx[node.Id] = child_list
              idx[node.ParentId].append((node, child_list))
            

            这构造了一个元组数组树(!)。 idx[0] 表示树的根。数组中的每个元素都是一个 2 元组,由节点本身及其所有子节点的列表组成。构造完成后,您可以保留 idx[0] 并丢弃 idx,除非您想通过其 ID 访问节点。

            【讨论】:

              【解决方案13】:

              要扩展 Bill 的 SQL 解决方案,您基本上可以使用平面数组来做同样的事情。此外,如果您的字符串都具有相同的长度并且您的最大子节点数是已知的(例如在二叉树中),您可以使用单个字符串(字符数组)来完成。如果您有任意数量的孩子,这会使事情变得有点复杂......我将不得不检查我的旧笔记以了解可以做什么。

              然后,牺牲一点内存,特别是如果你的树是稀疏和/或不平衡的,你可以通过一些索引数学,通过存储你的树来随机访问所有字符串,宽度在数组中像这样(对于二叉树):

              String[] nodeArray = [L0root, L1child1, L1child2, L2Child1, L2Child2, L2Child3, L2Child4] ...
              

              你知道你的字符串长度,你知道的

              我现在正在工作,所以不能花太多时间在上面,但我可以感兴趣地获取一些代码来完成这项工作。

              我们用它来搜索由 DNA 密码子组成的二叉树,一个过程构建了树,然后我们将它展平以搜索文本模式,当找到时,虽然索引数学(从上面反转)我们得到了节点。 .. 非常快速、高效、坚固,我们的树很少有空节点,但我们可以快速搜索千兆字节的数据。

              【讨论】:

                【解决方案14】:

                考虑将 nosql 工具(如 neo4j)用于层次结构。 例如,像linkedin这样的网络应用程序使用couchbase(另一种nosql解决方案)

                但 nosql 仅用于数据集市级别的查询,而不是存储/维护事务

                【讨论】:

                • 阅读了 SQL 和“非表”结构的复杂性和性能后,这也是我的第一个想法,nosql。当然,导出等问题有很多。另外,OP只提到了表格。那好吧。很明显,我不是数据库专家。
                猜你喜欢
                • 1970-01-01
                • 2011-12-04
                • 2013-03-08
                • 2012-07-09
                • 1970-01-01
                • 2011-04-01
                • 1970-01-01
                相关资源
                最近更新 更多