Cách hiệu quả / thanh lịch nhất để phân tích một bàn phẳng vào cây là gì?


517

Giả sử bạn có một bảng phẳng lưu trữ phân cấp cây theo thứ tự:

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

Đây là một sơ đồ, nơi chúng ta có [id] Name. Nút gốc 0 là hư cấu.

                       [0] ROOT
                          / \ 
              [1] Nút 1 [3] Nút 2
              / \ \
    [2] Nút 1.1 [6] Nút 1.2 [5] Nút 2.1
          /          
 [4] Nút 1.1.1

Cách tiếp cận tối giản nào bạn sẽ sử dụng để xuất ra HTML (hoặc văn bản, cho vấn đề đó) như một cây thụt lề được sắp xếp chính xác?

Giả sử xa hơn, bạn chỉ có các cấu trúc dữ liệu cơ bản (mảng và hàm băm), không có đối tượng ưa thích với tham chiếu cha / con, không ORM, không khung, chỉ có hai bàn tay của bạn. Bảng được biểu diễn dưới dạng tập kết quả, có thể được truy cập ngẫu nhiên.

Mã giả hoặc tiếng Anh đơn giản là được, đây hoàn toàn là một câu hỏi mang thai.

Câu hỏi về phần thưởng: Có cách nào cơ bản tốt hơn để lưu trữ cấu trúc cây như thế này trong RDBMS không?


EDIT VÀ THÊM

Để trả lời câu hỏi của một người bình luận ( Mark Bessey 's): Một nút gốc là không cần thiết, bởi vì nó sẽ không bao giờ được hiển thị. ParentId = 0 là quy ước để thể hiện "đây là cấp cao nhất". Cột Thứ tự xác định cách các nút có cùng cha mẹ sẽ được sắp xếp.

"Tập kết quả" mà tôi đã nói có thể được hình dung như một mảng các hashtag (để giữ nguyên thuật ngữ đó). Ví dụ của tôi có nghĩa là đã ở đó. Một số câu trả lời đi xa hơn và xây dựng nó trước, nhưng không sao.

Cây có thể sâu tùy ý. Mỗi nút có thể có N con. Mặc dù vậy, tôi không thực sự có một cây "hàng triệu mục".

Đừng nhầm lẫn lựa chọn đặt tên nút của tôi ('Nút 1.1.1') cho một cái gì đó để dựa vào. Các nút cũng có thể được gọi là 'Frank' hoặc 'Bob', không có cấu trúc đặt tên nào được ngụ ý, điều này chỉ đơn thuần là làm cho nó có thể đọc được.

Tôi đã đăng giải pháp của riêng tôi để các bạn có thể kéo nó ra từng mảnh.


2
"Không có đối tượng ưa thích với tài liệu tham khảo phụ huynh / trẻ em" - tại sao không? Tạo một đối tượng Node cơ bản với phương thức .addChild (), .getParent () cho phép bạn mô hình hóa mối quan hệ nút khá tốt.
matt b

2
Đây có phải là cây thông thường (n con trong đó n có thể> 2) cây hoặc cây nhị phân (nút có thể có 0, 1 hoặc 2 con) không?
BKimmel

Vì bạn có thể thực hiện cấu trúc dữ liệu nút thích hợp với hàm băm, nên không có hạn chế thực sự ở đây, chỉ cần làm việc nhiều hơn.
Svante

... Và đó chính xác là những gì bạn đã làm.
Svante

Câu trả lời:


451

Bây giờ MySQL 8.0 hỗ trợ các truy vấn đệ quy , chúng ta có thể nói rằng tất cả các cơ sở dữ liệu SQL phổ biến đều hỗ trợ các truy vấn đệ quy theo cú pháp chuẩn.

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;

Tôi đã thử nghiệm các truy vấn đệ quy trong MySQL 8.0 trong bài trình bày Truy vấn truy vấn đệ quy năm 2017.

Dưới đây là câu trả lời ban đầu của tôi từ năm 2008:


Có một số cách để lưu trữ dữ liệu có cấu trúc cây trong cơ sở dữ liệu quan hệ. Những gì bạn thể hiện trong ví dụ của bạn sử dụng hai phương pháp:

  • Danh sách điều chỉnh (cột "cha mẹ") và
  • Đường dẫn liệt kê (các số chấm trong cột tên của bạn).

Một giải pháp khác được gọi là Bộ lồng nhau và nó cũng có thể được lưu trữ trong cùng một bảng. Đọc " Cây và phân cấp trong SQL cho Smarties " của Joe Celko để biết thêm thông tin về các thiết kế này.

Tôi thường thích một thiết kế có tên là Bảng đóng (hay còn gọi là "Quan hệ phụ trợ") để lưu trữ dữ liệu có cấu trúc cây. Nó đòi hỏi một bảng khác, nhưng sau đó truy vấn cây khá dễ dàng.

Tôi trình bày Bảng đóng cửa trong phần trình bày của tôi Các mô hình cho dữ liệu phân cấp với SQL và PHP và trong cuốn sách của tôi Antipotype: Tránh các cạm bẫy của lập trình cơ sở dữ liệu .

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)
);

Lưu trữ tất cả các đường dẫn trong Bảng đóng, nơi có tổ tiên trực tiếp từ nút này sang nút khác. Bao gồm một hàng cho mỗi nút để tham chiếu chính nó. Ví dụ: sử dụng tập dữ liệu bạn đã hiển thị trong câu hỏi của mình:

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);

Bây giờ bạn có thể lấy một cây bắt đầu từ nút 1 như thế này:

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

Đầu ra (trong máy khách MySQL) trông như sau:

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

Nói cách khác, các nút 3 và 5 bị loại trừ, vì chúng là một phần của hệ thống phân cấp riêng biệt, không giảm dần từ nút 1.


Re: nhận xét từ e-satis về trẻ em ngay lập tức (hoặc cha mẹ ngay lập tức). Bạn có thể thêm một path_lengthcột "" vào ClosureTableđể dễ dàng truy vấn cụ thể hơn cho một đứa trẻ hoặc cha mẹ ngay lập tức (hoặc bất kỳ khoảng cách nào khác).

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);

Sau đó, bạn có thể thêm một thuật ngữ trong tìm kiếm của bạn để truy vấn con ngay lập tức của một nút nhất định. Đây là những hậu duệ có path_length1.

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 | 
+----+

Nhận xét lại từ @ashraf: "Làm thế nào về việc sắp xếp toàn bộ cây [theo tên]?"

Đây là một truy vấn mẫu để trả về tất cả các nút là hậu duệ của nút 1, nối chúng với FlatTable có chứa các thuộc tính nút khác như namevà sắp xếp theo tên.

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

Nhận xét lại từ @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         |
+------------+-------------+

Một người dùng đề nghị chỉnh sửa ngày hôm nay. Người điều hành SO đã phê duyệt bản chỉnh sửa, nhưng tôi đang đảo ngược nó.

Bản chỉnh sửa đề xuất rằng ORDER BY trong truy vấn cuối cùng ở trên ORDER BY b.path_length, f.name, có lẽ là để đảm bảo thứ tự khớp với cấu trúc phân cấp. Nhưng điều này không hoạt động, bởi vì nó sẽ đặt hàng "Nút 1.1.1" sau "Nút 1.2".

Nếu bạn muốn thứ tự khớp với hệ thống phân cấp một cách hợp lý, điều đó là có thể, nhưng không chỉ đơn giản bằng cách sắp xếp theo độ dài đường dẫn. Ví dụ: xem câu trả lời của tôi về cơ sở dữ liệu phân cấp của Bảng đóng cửa MySQL - Cách lấy thông tin theo đúng thứ tự .


6
Điều này rất thanh lịch, cảm ơn bạn. Điểm thưởng được trao. ;-) Tôi thấy một nhược điểm nhỏ - vì nó lưu trữ mối quan hệ con một cách rõ ràng ngầm định, bạn cần thực hiện CẬP NHẬT cẩn thận cho một sự thay đổi nhỏ trong cấu trúc cây.
Tomalak

16
Đúng, mọi phương thức lưu trữ cấu trúc cây trong cơ sở dữ liệu đều yêu cầu một số công việc, khi tạo hoặc cập nhật cây hoặc khi truy vấn cây và cây con. Chọn thiết kế dựa trên những gì bạn muốn đơn giản hơn: viết hoặc đọc.
Bill Karwin

2
@buffer, có một cơ hội để tạo ra sự không nhất quán khi bạn tạo tất cả các hàng cho một hệ thống phân cấp. Danh sách điều chỉnh ( parent_id) chỉ có một hàng để thể hiện mỗi mối quan hệ cha-con, nhưng Bảng đóng có nhiều.
Bill Karwin

1
@BillKarwin Một điều nữa, là các Bảng đóng cửa phù hợp với biểu đồ có nhiều đường dẫn đến bất kỳ nút nào (ví dụ: hệ thống phân cấp trong đó bất kỳ nút lá hoặc không lá nào có thể thuộc về nhiều hơn một cha mẹ)
người dùng

2
@Reza, để nếu bạn thêm một nút con mới, bạn có thể truy vấn tất cả các hậu duệ của (1) và đó là tổ tiên của đứa trẻ mới.
Bill Karwin

58

Nếu bạn sử dụng các bộ lồng nhau (đôi khi được gọi là Chuyển đổi cây theo thứ tự trước đã sửa đổi), bạn có thể trích xuất toàn bộ cấu trúc cây hoặc bất kỳ cây con nào trong nó theo thứ tự cây với một truy vấn duy nhất, với chi phí chèn cao hơn, vì bạn cần quản lý các cột mô tả một đường dẫn theo thứ tự thông qua cấu trúc cây.

Đối với django-mptt , tôi đã sử dụng một cấu trúc như thế này:

id cha_id cây_id cấp độ lft rght
- --------- ------- ----- --- ----
 1 null 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

Mô tả một cây trông như thế này ( idđại diện cho từng mục):

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

Hoặc, như một sơ đồ tập hợp lồng nhau, làm cho nó rõ ràng hơn về cách thức lftrghtcác giá trị hoạt động:

 Giới thiệu
| Rễ 1 |
| Giới thiệu sản phẩm của bạn
| | Con 1.1 | | Con 1,2 | |
| | ___________ | | ___________ | |
| | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | |
1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14
| | | |
| __________________________________________________________________________ |

Như bạn có thể thấy, để có được toàn bộ cây con cho một nút nhất định, theo thứ tự cây, bạn chỉ cần chọn tất cả các hàng có lftrghtgiá trị giữa nó lftrghtgiá trị. Thật đơn giản để lấy cây của tổ tiên cho một nút cho trước.

Các levelcột là một chút của denormalisation để thuận tiện hơn bất cứ điều gì vàtree_id cột cho phép bạn khởi động lại lftrghtđánh số cho mỗi nút cấp cao nhất, làm giảm số lượng các cột bị ảnh hưởng bởi chèn, di chuyển và xóa bỏ, như lftrghtcột phải điều chỉnh phù hợp khi các hoạt động này diễn ra để tạo hoặc đóng các khoảng trống. Tôi đã thực hiện một số ghi chú phát triển tại thời điểm tôi đang cố gắng xoay quanh các truy vấn cần thiết cho mỗi thao tác.

Về mặt thực sự làm việc với dữ liệu này để hiển thị cây, tôi đã tạo một tree_item_iteratorhàm tiện ích, với mỗi nút, sẽ cung cấp cho bạn đủ thông tin để tạo bất kỳ loại màn hình nào bạn muốn.

Thông tin thêm về MPTT:


9
Tôi ước chúng ta sẽ ngừng sử dụng các chữ viết tắt như lftrghtcho các tên cột, ý tôi là chúng ta không phải gõ bao nhiêu ký tự? một?!
orustammanapov

21

Đó là một câu hỏi khá cũ, nhưng vì nó có nhiều quan điểm, tôi nghĩ rằng nó đáng để trình bày một giải pháp thay thế, và theo tôi là giải pháp rất thanh lịch.

Để đọc cấu trúc cây, bạn có thể sử dụng Biểu thức bảng chung đệ quy (CTE). Nó cung cấp khả năng tìm nạp toàn bộ cấu trúc cây cùng một lúc, có thông tin về mức độ của nút, nút cha và thứ tự trong phạm vi con của nút cha.

Hãy để tôi chỉ cho bạn cách làm việc này trong PostgreSQL 9.1.

  1. Tạo cấu trúc

    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. Viết một truy vấn

    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;

    Đây là kết quả:

     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)

    Các nút cây được sắp xếp theo một mức độ sâu. Trong đầu ra cuối cùng, chúng tôi sẽ trình bày chúng trong các dòng tiếp theo.

    Đối với mỗi cấp độ, chúng được sắp xếp theo Parent_id và node_order trong cha mẹ. Điều này cho chúng ta biết làm thế nào để trình bày chúng trong nút liên kết đầu ra cho cha mẹ theo thứ tự này.

    Có cấu trúc như vậy sẽ không khó để tạo ra một bản trình bày thực sự hay trong HTML.

    CTE đệ quy có sẵn trong PostgreSQL, IBM DB2, MS SQL Server và Oracle .

    Nếu bạn muốn đọc thêm về các truy vấn SQL đệ quy, bạn có thể kiểm tra tài liệu của DBMS yêu thích của bạn hoặc đọc hai bài viết của tôi về chủ đề này:


18

Kể từ Oracle 9i, bạn có thể sử dụng 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)

Kể từ SQL Server 2005, bạn có thể sử dụng biểu thức bảng chung đệ quy (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]

Cả hai sẽ cho kết quả sau.

Tên
'Nút 1'
'Nút 1.1'
'Nút 1.1.1'
'Nút 1,2'
'Nút 2'
'Nút 2.1'

cte có thể được sử dụng trong cả sqlserver và oracle @Eric Weilnau
Nisar

9

Câu trả lời của Bill khá hay, câu trả lời này bổ sung một số điều khiến tôi mong muốn SO được hỗ trợ trả lời theo chuỗi.

Dù sao, tôi muốn hỗ trợ cấu trúc cây và thuộc tính Đặt hàng. Tôi đã bao gồm một thuộc tính duy nhất trong mỗi Nút được gọi là leftSiblingthực hiện điều tương tự Ordertrong câu hỏi ban đầu (duy trì thứ tự từ trái sang phải).

các nút mysc> desc;
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| Lĩnh vực | Loại | Không | Chìa khóa | Mặc định | Thêm |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| id | int (11) | KHÔNG | PRI | NULL | auto_increment |
| tên | varchar (255) | CÓ | | NULL | |
| trái Anh chị em | int (11) | KHÔNG | | 0 | |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
3 hàng trong bộ (0,00 giây)

mysql> desc kề;
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| Lĩnh vực | Loại | Không | Chìa khóa | Mặc định | Thêm |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| quan hệ | int (11) | KHÔNG | PRI | NULL | auto_increment |
| cha mẹ | int (11) | KHÔNG | | NULL | |
| con | int (11) | KHÔNG | | NULL | |
| đường dẫn | int (11) | KHÔNG | | NULL | |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
4 hàng trong bộ (0,00 giây)

Chi tiết hơn và mã SQL trên blog của tôi .

Cảm ơn Bill câu trả lời của bạn rất hữu ích trong việc bắt đầu!


7

Được lựa chọn tốt, tôi sẽ sử dụng các đối tượng. Tôi sẽ tạo một đối tượng cho mỗi bản ghi trong đó mỗi đối tượng có một childrenbộ sưu tập và lưu trữ tất cả chúng trong một mảng PGS (/ hashtable) trong đó Id là khóa. Và blitz thông qua bộ sưu tập một lần, thêm trẻ em vào các lĩnh vực trẻ em có liên quan. Đơn giản.

Nhưng vì bạn không vui bằng cách hạn chế sử dụng một số OOP tốt, có lẽ tôi sẽ lặp lại dựa trên:

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

PrintLine(0, 0)

Chỉnh sửa: điều này tương tự như một vài mục khác, nhưng tôi nghĩ nó sạch hơn một chút. Một điều tôi sẽ thêm: điều này cực kỳ chuyên sâu về SQL. Thật khó chịu . Nếu bạn có sự lựa chọn, hãy đi theo con đường OOP.


Đó là ý của tôi với "không có khung" - bạn đang sử dụng LINQ, phải không? Về đoạn đầu tiên của bạn: Tập kết quả đã có sẵn, tại sao sao chép tất cả thông tin vào cấu trúc đối tượng mới trước? (Tôi không đủ rõ ràng về thực tế đó, xin lỗi)
Tomalak

Tomalak - không có mã là mã giả. Tất nhiên, bạn phải chia mọi thứ thành các lựa chọn và lặp đúng ... và một cú pháp thực sự! Tại sao OOP? Bởi vì bạn có thể phản ánh chính xác cấu trúc. Nó giữ cho mọi thứ tốt đẹp và nó chỉ xảy ra hiệu quả hơn (chỉ có một lựa chọn)
Oli

Tôi cũng không có lựa chọn lặp đi lặp lại trong tâm trí. Về OOP: Mark Bessey đã nói trong câu trả lời của mình: "Bạn có thể mô phỏng bất kỳ cấu trúc dữ liệu nào khác bằng một hashmap, vì vậy đó không phải là một hạn chế khủng khiếp." Giải pháp của bạn là chính xác, nhưng tôi nghĩ rằng có một số cải tiến trước khi có OOP.
Tomalak

5

Điều này đã được viết một cách nhanh chóng, và không đẹp cũng không hiệu quả (cộng với nó tự động rất nhiều, chuyển đổi giữa intIntegergây phiền nhiễu!), Nhưng nó hoạt động.

Nó có thể phá vỡ các quy tắc kể từ khi tôi tạo các đối tượng của riêng mình nhưng hey tôi đang làm điều này như một sự chuyển hướng từ công việc thực tế :)

Điều này cũng giả định rằng kết quả / bảng hoàn toàn được đọc thành một loại cấu trúc nào đó trước khi bạn bắt đầu xây dựng Nút, đây sẽ không phải là giải pháp tốt nhất nếu bạn có hàng trăm ngàn hàng.

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;
    }
}

Tôi luôn cảm thấy khó khăn khi lọc phần dành riêng cho thuật toán từ phần dành riêng cho việc triển khai khi được trình bày với rất nhiều mã nguồn. Đó là lý do tại sao tôi yêu cầu một giải pháp không dành riêng cho ngôn ngữ ngay từ đầu. Nhưng nó làm công việc, vì vậy cảm ơn thời gian của bạn!
Tomalak

Tôi hiểu ý của bạn bây giờ, nếu không rõ ràng thuật toán chính là trong NodeBuilder.build () - tôi có thể đã làm tốt hơn việc tóm tắt điều này.
matt b

5

Có những giải pháp thực sự tốt khai thác đại diện btree nội bộ của các chỉ số sql. Điều này dựa trên một số nghiên cứu tuyệt vời được thực hiện vào khoảng năm 1998.

Dưới đây là bảng ví dụ (trong 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
)

Các trường duy nhất cần thiết cho biểu diễn cây là:

  • tw: Chỉ mục đặt hàng trước từ trái sang phải DFS, trong đó root = 1.
  • pa: Tham chiếu (sử dụng tw) cho nút cha, root có null.
  • sz: Kích thước của nhánh của nút bao gồm chính nó.
  • nc: được sử dụng làm đường cú pháp. nó là tw + nc và đại diện cho tw của "đứa con tiếp theo" của nút.

Dưới đây là ví dụ về dân số 24 nút, được sắp xếp theo 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 |
+-----+---------+----+------+------+------+

Mỗi kết quả cây có thể được thực hiện không đệ quy. Chẳng hạn, để có được danh sách tổ tiên của nút tại tw = '22 '

Tổ tiên

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 |
+-----+---------+----+------+------+------+

Anh chị em và trẻ em là tầm thường - chỉ cần sử dụng thứ tự trường pa theo tw.

Hậu duệ

Ví dụ: tập hợp (nhánh) của các nút được bắt nguồn từ 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 |
+-----+---------+----+------+------+------+

Ghi chú bổ sung

Phương pháp này cực kỳ hữu ích khi có số lần đọc lớn hơn nhiều so với việc chèn hoặc cập nhật.

Bởi vì việc chèn, di chuyển hoặc cập nhật một nút trong cây đòi hỏi phải điều chỉnh cây, nên cần phải khóa bảng trước khi bắt đầu hành động.

Chi phí chèn / xóa cao vì giá trị tw index và sz (kích thước nhánh) sẽ cần được cập nhật trên tất cả các nút sau điểm chèn và cho tất cả các tổ tiên tương ứng.

Di chuyển nhánh liên quan đến việc di chuyển giá trị tw của nhánh ra khỏi phạm vi, do đó cũng cần phải vô hiệu hóa các ràng buộc khóa ngoài khi di chuyển một nhánh. Về cơ bản, có bốn truy vấn cần thiết để di chuyển một nhánh:

  • Di chuyển chi nhánh ra khỏi phạm vi.
  • Đóng khoảng trống mà nó để lại. (cây còn lại hiện đang được chuẩn hóa).
  • Mở khoảng cách nơi nó sẽ đi đến.
  • Di chuyển chi nhánh vào vị trí mới.

Điều chỉnh truy vấn cây

Việc mở / đóng các khoảng trống trong cây là một chức năng phụ quan trọng được sử dụng bởi các phương thức tạo / cập nhật / xóa, vì vậy tôi đưa nó vào đây.

Chúng ta cần hai tham số - một cờ biểu thị cho dù chúng ta thu nhỏ hay tăng kích thước và chỉ số tw của nút. Vì vậy, ví dụ tw = 18 (có kích thước nhánh là 5). Giả sử rằng chúng tôi thu nhỏ (loại bỏ tw) - điều này có nghĩa là chúng tôi đang sử dụng '-' thay vì '+' trong các bản cập nhật của ví dụ sau.

Trước tiên chúng ta sử dụng hàm tổ tiên (thay đổi một chút) để cập nhật giá trị 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));

Sau đó, chúng ta cần điều chỉnh tw cho những người có tw cao hơn nhánh cần loại bỏ.

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;

Sau đó, chúng ta cần điều chỉnh cha mẹ cho những người có tw của pa cao hơn nhánh cần loại bỏ.

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;

3

Giả sử rằng bạn biết rằng các phần tử gốc bằng 0, đây là mã giả để xuất thành văn bản:

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)

3

Bạn có thể mô phỏng bất kỳ cấu trúc dữ liệu nào khác bằng hàm băm, vì vậy đó không phải là một hạn chế khủng khiếp. Quét từ trên xuống dưới, bạn tạo một hashmap cho mỗi hàng của cơ sở dữ liệu, với một mục nhập cho mỗi cột. Thêm từng hashtag này vào hashmap "master", được khóa trên id. Nếu bất kỳ nút nào có "cha mẹ" mà bạn chưa thấy, hãy tạo một mục giữ chỗ cho nó trong hashmap chính và điền vào đó khi bạn thấy nút thực tế.

Để in nó ra, hãy thực hiện một bước sâu đơn giản thông qua dữ liệu, theo dõi mức độ thụt lề trên đường đi. Bạn có thể thực hiện việc này dễ dàng hơn bằng cách giữ một mục nhập "trẻ em" cho mỗi hàng và điền vào đó khi bạn quét dữ liệu.

Về việc có cách nào tốt hơn để lưu trữ cây trong cơ sở dữ liệu hay không, điều đó phụ thuộc vào cách bạn sẽ sử dụng dữ liệu. Tôi đã thấy các hệ thống có độ sâu tối đa đã biết sử dụng một bảng khác nhau cho mỗi cấp trong cấu trúc phân cấp. Điều đó rất có ý nghĩa nếu tất cả các cấp độ trong cây không hoàn toàn tương đương (các loại cấp cao nhất khác với các loại lá).


1

Nếu các bản đồ hoặc mảng băm lồng nhau có thể được tạo, thì tôi có thể chỉ cần đi xuống bảng từ đầu và thêm từng mục vào mảng lồng nhau. Tôi phải theo dõi từng dòng đến nút gốc để biết mức nào trong mảng được lồng vào. Tôi có thể sử dụng ghi nhớ để không cần phải tìm kiếm cùng một cha mẹ nhiều lần.

Chỉnh sửa: Tôi sẽ đọc toàn bộ bảng thành một mảng trước, vì vậy nó sẽ không truy vấn DB nhiều lần. Tất nhiên điều này sẽ không thực tế nếu bàn của bạn rất lớn.

Sau khi cấu trúc được xây dựng, trước tiên tôi phải thực hiện một chiều sâu xuyên qua nó và in ra HTML.

Không có cách cơ bản nào tốt hơn để lưu trữ thông tin này bằng một bảng (mặc dù tôi có thể sai;) và rất thích xem giải pháp tốt hơn). Tuy nhiên, nếu bạn tạo một lược đồ để sử dụng các bảng db được tạo động, thì bạn đã mở ra một thế giới hoàn toàn mới với sự hy sinh của sự đơn giản và nguy cơ địa ngục SQL;).


1
Tôi không muốn thay đổi bố cục DB chỉ vì cần một cấp độ nút phụ mới. :-)
Tomalak

1

Nếu các phần tử theo thứ tự cây, như trong ví dụ của bạn, bạn có thể sử dụng một cái gì đó giống như ví dụ Python sau:

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)

Điều này làm là duy trì một ngăn xếp đại diện cho vị trí hiện tại trong cây. Đối với mỗi phần tử trong bảng, nó sẽ bật các phần tử ngăn xếp (đóng các div phù hợp) cho đến khi tìm thấy phần tử mẹ của mục hiện tại. Sau đó, nó xuất ra điểm bắt đầu của nút đó và đẩy nó vào ngăn xếp.

Nếu bạn muốn xuất cây bằng cách sử dụng thụt đầu dòng thay vì các phần tử lồng nhau, bạn có thể chỉ cần bỏ qua các câu lệnh in để in các div và in một số khoảng trắng bằng một số bội của kích thước của ngăn xếp trước mỗi mục. Ví dụ: trong Python:

print "  " * len(stack)

Bạn cũng có thể dễ dàng sử dụng phương pháp này để xây dựng một tập hợp các danh sách hoặc từ điển lồng nhau.

Chỉnh sửa: Tôi thấy từ sự làm rõ của bạn rằng các tên không có ý định là đường dẫn nút. Điều đó cho thấy một cách tiếp cận khác:

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

Điều này xây dựng một cây các mảng của bộ dữ liệu (!). idx [0] đại diện cho (các) gốc của cây. Mỗi phần tử trong một mảng là 2 tuple bao gồm chính nút đó và một danh sách tất cả các phần tử con của nó. Sau khi được xây dựng, bạn có thể giữ idx [0] và loại bỏ idx, trừ khi bạn muốn truy cập các nút bằng ID của chúng.


1

Để mở rộng giải pháp SQL của Bill, về cơ bản bạn có thể làm tương tự bằng cách sử dụng một mảng phẳng. Hơn nữa nếu tất cả các chuỗi của bạn có cùng chiều dài và số lượng con tối đa của bạn được biết đến (giả sử trong cây nhị phân), bạn có thể thực hiện bằng một chuỗi duy nhất (mảng ký tự). Nếu bạn có số lượng trẻ em tùy ý, điều này sẽ làm phức tạp mọi thứ một chút ... Tôi sẽ phải kiểm tra các ghi chú cũ của tôi để xem những gì có thể được thực hiện.

Sau đó, hy sinh một chút bộ nhớ, đặc biệt là nếu cây của bạn thưa thớt và / hoặc không có bóng, bạn có thể, với một chút toán học chỉ mục, truy cập ngẫu nhiên tất cả các chuỗi bằng cách lưu trữ cây của bạn, chiều rộng đầu tiên trong mảng như vậy (đối với nhị phân cây):

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

yo biết chiều dài chuỗi của bạn, bạn biết nó

Bây giờ tôi đang làm việc nên không thể dành nhiều thời gian cho nó nhưng với sự quan tâm tôi có thể lấy một chút mã để làm điều này.

Chúng tôi thường làm điều đó để tìm kiếm trong các cây nhị phân được tạo từ các codon DNA, một quá trình xây dựng cây, sau đó chúng tôi làm phẳng nó để tìm kiếm các mẫu văn bản và khi tìm thấy, mặc dù toán học chỉ mục (đảo ngược từ trên xuống) chúng tôi lấy lại nút ... rất nhanh và hiệu quả, khó khăn, cây của chúng ta hiếm khi có các nút trống, nhưng chúng ta có thể thu được hàng gigabyte dữ liệu trong nháy mắt.


0

Hãy suy nghĩ về việc sử dụng các công cụ nosql như neo4j cho các cấu trúc phân cấp. ví dụ: một ứng dụng được nối mạng như Linkedin sử dụng couchbase (một giải pháp nosql khác)

Nhưng chỉ sử dụng nosql cho các truy vấn mức dữ liệu-mart và không lưu trữ / duy trì giao dịch


Đã đọc về sự phức tạp và hoàn hảo của các cấu trúc SQL và "không phải bảng", đây cũng là suy nghĩ đầu tiên của tôi, nosql. Tất nhiên, có rất nhiều vấn đề để xuất khẩu, v.v. Ngoài ra, OP chỉ đề cập đến các bảng. Ồ tốt Tôi không phải là một chuyên gia DB, như là hiển nhiên.
Josef.B
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.