550

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

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)的问题:根节点不是必需的,因为它永远不会被显示。ParentId = 0 是表达“这些是顶级”的约定。Order 列定义了如何对具有相同父节点的节点进行排序。

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

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

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

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

4

14 回答 14

482

既然MySQL 8.0 支持递归查询,我们可以说所有流行的 SQL 数据库都支持标准语法的递归查询。

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 的“ Smarties 中的树和层次结构”。

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

我在我的演示文稿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 下降。


回复:来自 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);

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

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 的所有后代节点,将它们连接到包含其他节点属性的 FlatTable,例如name,并按名称排序。

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 hierarchy database - How to pull information out in the correct order 的回答。

于 2008-10-10T17:58:07.263 回答
62

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

对于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
| |________________________________| |________________________________| |
|____________________________________________________________________________________|

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

为了方便起见,该level列有点非规范化,并且该tree_id列允许您重新启动每个顶级节点的lftand编号,这减少了受插入、移动和删除影响的列数,因为and列必须是当这些操作发生时进行相应调整,以创建或缩小差距。当我试图围绕每个操作所需的查询进行思考时,我做了一些开发笔记。rghtlftrght

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

更多关于 MPTT 的信息:

于 2008-10-11T12:31:22.337 回答
26

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

为了读取树结构,您可以使用递归公用表表达式(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 的文档或阅读我的两篇关于该主题的文章:

于 2014-03-13T11:19:21.293 回答
18

从 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'
于 2008-10-10T20:06:21.153 回答
10

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

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

mysql> desc 节点;
+-------------+-------------+------+-----+------ --+----------------+
| 领域 | 类型 | 空 | 钥匙 | 默认 | 额外 |
+-------------+-------------+------+-----+------ --+----------------+
| 编号 | 整数(11) | 否 | 优先级 | 空 | 自动增量 |
| 姓名 | varchar(255) | 是 | | 空 | |
| 左兄弟 | 整数(11) | 否 | | 0 | |
+-------------+-------------+------+-----+------ --+----------------+
3 行一组(0.00 秒)

mysql> desc 邻接;
+------------+---------+------+-----+---------+--- -------------+
| 领域 | 类型 | 空 | 钥匙 | 默认 | 额外 |
+------------+---------+------+-----+---------+--- -------------+
| 关系 ID | 整数(11) | 否 | 优先级 | 空 | 自动增量 |
| 家长 | 整数(11) | 否 | | 空 | |
| 孩子| 整数(11) | 否 | | 空 | |
| 路径长度 | 整数(11) | 否 | | 空 | |
+------------+---------+------+-----+---------+--- -------------+
4 行一组(0.00 秒)

更多细节和 SQL 代码在我的博客上

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

于 2010-12-22T04:31:40.227 回答
7

好吧,如果有选择,我会使用对象。我会为每个记录创建一个对象,其中每个对象都有一个children集合,并将它们全部存储在一个关联数组(/哈希表)中,其中 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 路线。

于 2008-10-10T17:36:21.690 回答
6

确实有很好的解决方案可以利用 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值移出范围,所以在移动分支时也需要禁用外键约束。移动分支基本上需要四个查询:

  • 将分支移出范围。
  • 关闭它留下的差距。(剩余的树现在已标准化)。
  • 打开它会去的缝隙。
  • 将分支移动到新位置。

调整树查询

树中间隙的打开/关闭是创建/更新/删除方法使用的重要子功能,因此我将其包含在此处。

我们需要两个参数 - 一个表示我们是缩小还是扩大规模的标志,以及节点的 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;
于 2017-03-14T08:43:51.550 回答
5

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

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

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

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;
    }
}
于 2008-10-10T18:25:29.937 回答
4

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

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)
于 2008-10-10T16:59:37.677 回答
3

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

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

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

于 2008-10-10T17:24:34.973 回答
1

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

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

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

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

于 2008-10-10T17:02:40.103 回答
1

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

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

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

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

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

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

于 2008-10-10T18:42:36.940 回答
1

如果元素按树顺序排列,如您的示例所示,您可以使用类似于以下 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 访问节点。

于 2008-10-10T21:45:05.167 回答
0

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

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

于 2012-11-26T15:49:12.657 回答