Was ist die effizienteste / eleganteste Art, einen flachen Tisch in einen Baum zu zerlegen?


517

Angenommen, Sie haben eine flache Tabelle, in der eine geordnete Baumhierarchie gespeichert ist:

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

Hier ist ein Diagramm, wo wir haben [id] Name. Wurzelknoten 0 ist fiktiv.

                       [0] Wurzel
                          / \. 
              [1] Knoten 1 [3] Knoten 2
              / \ \
    [2] Knoten 1.1 [6] Knoten 1.2 [5] Knoten 2.1
          /.          
 [4] Knoten 1.1.1

Welchen minimalistischen Ansatz würden Sie verwenden, um dies als korrekt geordneten, korrekt eingerückten Baum in HTML (oder Text) auszugeben?

Angenommen, Sie haben nur grundlegende Datenstrukturen (Arrays und Hashmaps), keine ausgefallenen Objekte mit übergeordneten / untergeordneten Referenzen, kein ORM, kein Framework, nur Ihre beiden Hände. Die Tabelle wird als Ergebnismenge dargestellt, auf die zufällig zugegriffen werden kann.

Pseudocode oder einfaches Englisch ist in Ordnung, dies ist eine rein konzeptionelle Frage.

Bonusfrage: Gibt es eine grundlegend bessere Möglichkeit, eine solche Baumstruktur in einem RDBMS zu speichern?


BEARBEITUNGEN UND ERGÄNZUNGEN

So beantworten Sie die Frage eines Kommentators ( Mark Bessey ): Ein Stammknoten ist nicht erforderlich, da er ohnehin nie angezeigt wird. ParentId = 0 ist die Konvention, um "diese sind die oberste Ebene" auszudrücken. In der Spalte Reihenfolge wird definiert, wie Knoten mit demselben übergeordneten Element sortiert werden.

Die "Ergebnismenge", von der ich gesprochen habe, kann als eine Reihe von Hashmaps dargestellt werden (um in dieser Terminologie zu bleiben). Denn mein Beispiel sollte schon da sein. Einige Antworten gehen die Extrameile und konstruieren es zuerst, aber das ist okay.

Der Baum kann beliebig tief sein. Jeder Knoten kann N untergeordnete Knoten haben. Ich hatte jedoch nicht genau einen Baum mit "Millionen von Einträgen" im Sinn.

Verwechseln Sie meine Wahl der Knotenbezeichnung ('Node 1.1.1') nicht mit etwas, auf das Sie sich verlassen können. Die Knoten könnten ebenso gut als "Frank" oder "Bob" bezeichnet werden, es ist keine Namensstruktur impliziert, dies diente lediglich dazu, sie lesbar zu machen.

Ich habe meine eigene Lösung veröffentlicht, damit ihr sie in Stücke reißen könnt.


2
"Keine ausgefallenen Objekte mit Eltern- / Kinderreferenzen" - warum nicht? Wenn Sie ein grundlegendes Knotenobjekt mit den Methoden .addChild () und .getParent () erstellen, können Sie die Knotenbeziehung ziemlich gut modellieren.
Matt B

2
Ist es ein regulärer Baum (n Kinder, wobei n> 2 sein kann) oder ein Binärbaum (Knoten kann 0, 1 oder 2 Kinder haben)?
BKimmel

Da Sie eine ordnungsgemäße Knotendatenstruktur mit einer Hashmap implementieren können, gibt es hier keine wirkliche Einschränkung, nur mehr Arbeit.
Svante

... und genau das hast du getan.
Svante

Antworten:


451

Nachdem MySQL 8.0 rekursive Abfragen unterstützt , können wir sagen, dass alle gängigen SQL-Datenbanken rekursive Abfragen in Standardsyntax unterstützen.

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;

Ich habe rekursive Abfragen in MySQL 8.0 in meiner Präsentation Recursive Query Throwdown im Jahr 2017 getestet .

Unten ist meine ursprüngliche Antwort von 2008:


Es gibt verschiedene Möglichkeiten, baumstrukturierte Daten in einer relationalen Datenbank zu speichern. Was Sie in Ihrem Beispiel zeigen, verwendet zwei Methoden:

  • Adjazenzliste (die "übergeordnete" Spalte) und
  • Pfadaufzählung (die gepunkteten Zahlen in Ihrer Namensspalte).

Eine andere Lösung heißt Verschachtelte Mengen und kann auch in derselben Tabelle gespeichert werden. Weitere Informationen zu diesen Designs finden Sie unter " Bäume und Hierarchien in SQL für Smarties " von Joe Celko.

Normalerweise bevorzuge ich ein Design namens Closure Table (auch bekannt als "Adjacency Relation") zum Speichern von Daten mit Baumstruktur. Es erfordert eine andere Tabelle, aber das Abfragen von Bäumen ist ziemlich einfach.

Ich beschreibe die Closure Table in meinen Präsentationsmodellen für hierarchische Daten mit SQL und PHP und in meinem Buch SQL Antipatterns: Vermeiden der Fallstricke der Datenbankprogrammierung .

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

Speichern Sie alle Pfade in der Closure Table, in der es eine direkte Abstammung von einem Knoten zum anderen gibt. Fügen Sie für jeden Knoten eine Zeile ein, um auf sich selbst zu verweisen. Verwenden Sie beispielsweise den Datensatz, den Sie in Ihrer Frage angezeigt haben:

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

Jetzt können Sie einen Baum ab Knoten 1 wie folgt erhalten:

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

Die Ausgabe (im MySQL-Client) sieht folgendermaßen aus:

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

Mit anderen Worten, die Knoten 3 und 5 werden ausgeschlossen, da sie Teil einer separaten Hierarchie sind und nicht von Knoten 1 abstammen.


Betreff: Kommentar von e-satis zu unmittelbaren Kindern (oder unmittelbaren Eltern). Sie können der path_lengthSpalte " " eine Spalte hinzufügen ClosureTable, um die Abfrage eines unmittelbaren Kindes oder Elternteils (oder einer anderen Entfernung) zu vereinfachen.

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

Anschließend können Sie Ihrer Suche einen Begriff hinzufügen, um die unmittelbaren untergeordneten Elemente eines bestimmten Knotens abzufragen. Dies sind Nachkommen, deren path_length1 ist.

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

Kommentar von @ashraf: "Wie wäre es, den ganzen Baum [nach Namen] zu sortieren?"

Hier ist eine Beispielabfrage, um alle Knoten, die Nachkommen von Knoten 1 sind, zurückzugeben, sie mit der FlatTable zu verknüpfen, die andere Knotenattribute enthält, z. B. nameund nach dem Namen zu sortieren.

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

Kommentar von @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         |
+------------+-------------+

Ein Benutzer hat heute eine Bearbeitung vorgeschlagen. SO-Moderatoren haben die Bearbeitung genehmigt, aber ich mache sie rückgängig.

Die Bearbeitung schlug vor, dass ORDER BY in der letzten Abfrage oben sein sollte ORDER BY b.path_length, f.name, vermutlich um sicherzustellen, dass die Reihenfolge mit der Hierarchie übereinstimmt. Dies funktioniert jedoch nicht, da "Knoten 1.1.1" nach "Knoten 1.2" bestellt wird.

Wenn Sie möchten, dass die Reihenfolge auf sinnvolle Weise mit der Hierarchie übereinstimmt, ist dies möglich, jedoch nicht einfach durch die Reihenfolge nach Pfadlänge. Siehe zum Beispiel meine Antwort auf die hierarchische Datenbank der MySQL Closure Table - So ziehen Sie Informationen in der richtigen Reihenfolge ab .


6
Das ist sehr elegant, danke. Bonuspunkt vergeben. ;-) Ich sehe jedoch einen kleinen Nachteil: Da die untergeordnete Beziehung explizit und implizit gespeichert wird, müssen Sie selbst für eine kleine Verschiebung der Baumstruktur viel sorgfältiges UPDATE durchführen.
Tomalak

16
Richtig, jede Methode zum Speichern von Baumstrukturen in einer Datenbank erfordert einige Arbeit, entweder beim Erstellen oder Aktualisieren des Baums oder beim Abfragen von Bäumen und Teilbäumen. Wählen Sie das Design, basierend auf dem Sie einfacher sein möchten: Schreiben oder Lesen.
Bill Karwin

2
@buffer, es besteht die Möglichkeit, Inkonsistenzen zu erstellen, wenn Sie alle Zeilen für eine Hierarchie erstellen. Adjacency List ( parent_id) hat nur eine Zeile, um jede Eltern-Kind-Beziehung auszudrücken, aber Closure Table hat viele.
Bill Karwin

1
@ BillKarwin Eine weitere Sache sind Closure Tables, die für ein Diagramm mit mehreren Pfaden zu einem bestimmten Knoten geeignet sind (z. B. eine Kategoriehierarchie, in der ein Blatt- oder Nichtblattknoten zu mehr als einem übergeordneten Knoten gehören kann)
Benutzer

2
@Reza, sodass Sie beim Hinzufügen eines neuen untergeordneten Knotens alle Nachkommen von (1) abfragen können und dies die Vorfahren des neuen untergeordneten Knotens sind.
Bill Karwin

58

Wenn Sie verschachtelte Mengen verwenden (manchmal auch als modifizierte Baumdurchquerung vor der Bestellung bezeichnet), können Sie die gesamte Baumstruktur oder einen darin enthaltenen Teilbaum in Baumreihenfolge mit einer einzigen Abfrage extrahieren, wobei die Kosten für Einfügungen nach Bedarf höher sind Verwalten Sie Spalten, die einen Pfad in der richtigen Reihenfolge durch Ihre Baumstruktur beschreiben.

Für django-mptt habe ich eine Struktur wie diese verwendet:

id parent_id tree_id level 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

Welches beschreibt einen Baum, der so aussieht (mit idDarstellung jedes Elements):

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

Oder als verschachteltes Mengen-Diagramm, das die Funktionsweise der lftund rght-Werte deutlicher macht :

 __________________________________________________________________________
| Wurzel 1 |
| ________________________________ ________________________________ |
| | Kind 1.1 | | Kind 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
| | ________________________________ | | ________________________________ | |
| __________________________________________________________________________ |

Wie Sie sehen können, die gesamte Unterstruktur für einen bestimmten Knoten im Baum erhalten, können Sie einfach alle Zeilen wählen haben , die haben lftund rghtihre Werte zwischen lftund rghtWerte. Es ist auch einfach, den Stammbaum für einen bestimmten Knoten abzurufen.

Die levelSpalte ist vor allem der Einfachheit halber ein wenig denormalisiert, und in der tree_idSpalte können Sie die lftund die rghtNummerierung für jeden Knoten der obersten Ebene neu starten , wodurch die Anzahl der Spalten, die von Einfügungen, Verschiebungen und Löschungen betroffen sind, so verringert wird, wie es die Spalten lftund rghtsein müssen entsprechend angepasst, wenn diese Operationen stattfinden, um Lücken zu schaffen oder zu schließen. Ich habe einige Entwicklungsnotizen gemacht, als ich versuchte, mich mit den für jede Operation erforderlichen Abfragen zu beschäftigen.

Um tatsächlich mit diesen Daten zu arbeiten, um einen Baum anzuzeigen, habe ich eine tree_item_iteratorDienstprogrammfunktion erstellt, die für jeden Knoten ausreichend Informationen liefert, um die gewünschte Art von Anzeige zu generieren.

Weitere Informationen zu MPTT:


9
Ich wünschte, wir würden keine Abkürzungen wie lftund rghtfür Spaltennamen mehr verwenden. Ich meine, wie viele Zeichen mussten wir nicht eingeben? einer?!
orustammanapov

21

Es ist eine ziemlich alte Frage, aber da sie viele Ansichten hat, denke ich, dass es sich lohnt, eine alternative und meiner Meinung nach sehr elegante Lösung vorzustellen.

Um eine Baumstruktur zu lesen, können Sie rekursive Common Table Expressions (CTEs) verwenden. Es bietet die Möglichkeit, die gesamte Baumstruktur auf einmal abzurufen, Informationen über die Ebene des Knotens, seinen übergeordneten Knoten und die Reihenfolge innerhalb der untergeordneten Knoten des übergeordneten Knotens zu erhalten.

Lassen Sie mich Ihnen zeigen, wie dies in PostgreSQL 9.1 funktionieren würde.

  1. Erstellen Sie eine Struktur

    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. Schreiben Sie eine Abfrage

    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;

    Hier sind die Ergebnisse:

     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)

    Die Baumknoten sind nach einer Tiefe geordnet. In der endgültigen Ausgabe würden wir sie in den folgenden Zeilen präsentieren.

    Für jede Ebene werden sie nach parent_id und node_order innerhalb des übergeordneten Elements sortiert. Hier erfahren Sie, wie Sie sie im Ausgangsverknüpfungsknoten in dieser Reihenfolge dem übergeordneten Knoten präsentieren.

    Mit einer solchen Struktur wäre es nicht schwierig, eine wirklich schöne Präsentation in HTML zu erstellen.

    Rekursive CTEs sind in PostgreSQL, IBM DB2, MS SQL Server und Oracle verfügbar .

    Wenn Sie mehr über rekursive SQL-Abfragen erfahren möchten, können Sie entweder die Dokumentation Ihres bevorzugten DBMS überprüfen oder meine beiden Artikel zu diesem Thema lesen:


18

Ab Oracle 9i können Sie CONNECT BY verwenden.

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)

Ab SQL Server 2005 können Sie einen rekursiven allgemeinen Tabellenausdruck (CTE) verwenden.

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]

Beide geben die folgenden Ergebnisse aus.

Name
'Knoten 1'
'Knoten 1.1'
'Knoten 1.1.1'
'Knoten 1.2'
'Knoten 2'
'Knoten 2.1'

cte kann sowohl in sqlserver als auch in oracle @Eric Weilnau
Nisar

9

Bills Antwort ist verdammt gut, diese Antwort fügt einige Dinge hinzu, die mich dazu bringen, SO unterstützte Thread-Antworten zu wünschen.

Auf jeden Fall wollte ich die Baumstruktur und die Order-Eigenschaft unterstützen. Ich habe in jeden aufgerufenen Knoten eine einzelne Eigenschaft eingefügt leftSibling, die dasselbe tut Order, was in der ursprünglichen Frage vorgesehen ist (Reihenfolge von links nach rechts beibehalten).

mysql> desc node;
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| Feld | Geben Sie | ein Null | Schlüssel | Standard | Extra |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| id | int (11) | NEIN | PRI | NULL | auto_increment |
| Name | varchar (255) | JA | | NULL | |
| leftSibling | int (11) | NEIN | | 0 | |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
3 Reihen im Satz (0,00 Sek.)

mysql> desc Nachbarschaften;
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| Feld | Geben Sie | ein Null | Schlüssel | Standard | Extra |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| RelationId | int (11) | NEIN | PRI | NULL | auto_increment |
| Eltern | int (11) | NEIN | | NULL | |
| Kind | int (11) | NEIN | | NULL | |
| pathLen | int (11) | NEIN | | NULL | |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
4 Reihen im Satz (0,00 Sek.)

Weitere Details und SQL-Code in meinem Blog .

Vielen Dank, Bill. Ihre Antwort war hilfreich für den Einstieg!


7

Wenn ich die Wahl hätte, würde ich Objekte verwenden. Ich würde ein Objekt für jeden Datensatz erstellen, in dem jedes Objekt eine childrenSammlung hat, und sie alle in einem Assoc-Array (/ hashtable) speichern, in dem die ID der Schlüssel ist. Und blitzen Sie einmal durch die Sammlung und fügen Sie die Kinder zu den relevanten Kinderfeldern hinzu.Einfach.

Aber weil Sie keinen Spaß daran haben, die Verwendung eines guten OOP einzuschränken, würde ich wahrscheinlich basierend auf Folgendem iterieren:

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

PrintLine(0, 0)

Bearbeiten: Dies ähnelt einigen anderen Einträgen, aber ich denke, es ist etwas sauberer. Eines möchte ich hinzufügen: Dies ist extrem SQL-intensiv. Es ist böse . Wenn Sie die Wahl haben, gehen Sie die OOP-Route.


Das habe ich mit "keine Frameworks" gemeint - Sie verwenden LINQ, nicht wahr? Zu Ihrem ersten Absatz: Die Ergebnismenge ist bereits vorhanden. Warum sollten Sie zuerst alle Informationen in eine neue Objektstruktur kopieren? (Ich war mir darüber nicht klar genug, sorry)
Tomalak

Tomalak - nein, der Code ist Pseudocode. Natürlich müssten Sie die Dinge in richtige Auswahlen und Iteratoren aufteilen ... und eine echte Syntax! Warum OOP? Weil Sie die Struktur genau spiegeln können. Es hält die Dinge schön und es ist einfach effizienter (nur eine Auswahl)
Oli

Ich hatte auch keine wiederholten Auswahlen im Sinn. In Bezug auf OOP: Mark Bessey sagte in seiner Antwort: "Sie können jede andere Datenstruktur mit einer Hashmap emulieren, das ist also keine schreckliche Einschränkung." Ihre Lösung ist richtig, aber ich denke, auch ohne OOP gibt es Raum für Verbesserungen.
Tomalak

5

Dies wurde schnell geschrieben und ist weder hübsch noch effizient (außerdem gibt es viele Autoboxen, konvertiert zwischen intund Integerist ärgerlich!), Aber es funktioniert.

Es verstößt wahrscheinlich gegen die Regeln, da ich meine eigenen Objekte erstelle, aber hey, ich mache das als Ablenkung von der realen Arbeit :)

Dies setzt auch voraus, dass das resultSet / die Tabelle vollständig in eine Struktur eingelesen wird, bevor Sie mit dem Erstellen von Knoten beginnen. Dies wäre nicht die beste Lösung, wenn Sie Hunderttausende von Zeilen haben.

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

Es fällt mir immer schwer, den algorithmischen Teil aus dem implementierungsspezifischen Teil herauszufiltern, wenn viel Quellcode angezeigt wird. Deshalb habe ich nach einer Lösung gefragt, die überhaupt nicht sprachspezifisch war. Aber es macht den Job, also danke für deine Zeit!
Tomalak

Ich verstehe, was Sie jetzt meinen, wenn es nicht offensichtlich ist, dass der Hauptalgorithmus in NodeBuilder.build () ist - ich hätte es wahrscheinlich besser zusammenfassen können.
Matt B

5

Es gibt wirklich gute Lösungen, die die interne btree-Darstellung von SQL-Indizes ausnutzen. Dies basiert auf einigen großartigen Forschungen, die um 1998 durchgeführt wurden.

Hier ist eine Beispieltabelle (in 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
)

Die einzigen Felder, die für die Baumdarstellung erforderlich sind, sind:

  • tw: Der DFS-Vorbestellungsindex von links nach rechts, wobei root = 1 ist.
  • pa: Der Verweis (mit tw) auf den übergeordneten Knoten root hat null.
  • sz: Die Größe des Zweigs des Knotens einschließlich sich selbst.
  • nc: wird als syntaktischer Zucker verwendet. es ist tw + nc und repräsentiert das tw des "nächsten Kindes" des Knotens.

Hier ist ein Beispiel für eine 24-Knoten-Population, geordnet nach 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 |
+-----+---------+----+------+------+------+

Jedes Baumergebnis kann nicht rekursiv erstellt werden. Zum Beispiel, um eine Liste der Vorfahren des Knotens bei tw = '22 'zu erhalten.

Vorfahren

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

Geschwister und Kinder sind trivial - verwenden Sie einfach die Pa-Feldreihenfolge nach Tw.

Nachkommenschaft

Zum Beispiel die Menge (Verzweigung) von Knoten, die bei tw = 17 verwurzelt sind.

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

Zusätzliche Bemerkungen

Diese Methode ist äußerst nützlich, wenn eine weitaus größere Anzahl von Lesevorgängen vorliegt als Einfügungen oder Aktualisierungen.

Da zum Einfügen, Verschieben oder Aktualisieren eines Knotens in den Baum eine Anpassung des Baums erforderlich ist, muss die Tabelle gesperrt werden, bevor mit der Aktion begonnen wird.

Die Einfüge- / Löschkosten sind hoch, da die Werte für tw-Index und sz (Zweiggröße) auf allen Knoten nach dem Einfügepunkt bzw. für alle Vorfahren aktualisiert werden müssen.

Beim Verschieben von Zweigen wird der tw-Wert des Zweigs außerhalb des Bereichs verschoben. Daher müssen beim Verschieben eines Zweigs auch Fremdschlüsseleinschränkungen deaktiviert werden. Es sind im Wesentlichen vier Abfragen erforderlich, um einen Zweig zu verschieben:

  • Bewegen Sie den Zweig außerhalb des Bereichs.
  • Schließen Sie die Lücke, die es hinterlassen hat. (Der verbleibende Baum ist jetzt normalisiert).
  • Öffne die Lücke, in die es gehen wird.
  • Bewegen Sie den Zweig in seine neue Position.

Passen Sie Baumabfragen an

Das Öffnen / Schließen von Lücken im Baum ist eine wichtige Unterfunktion, die von Methoden zum Erstellen / Aktualisieren / Löschen verwendet wird. Deshalb füge ich sie hier ein.

Wir benötigen zwei Parameter - ein Flag, das angibt, ob wir verkleinern oder vergrößern, und den Tw-Index des Knotens. Also zum Beispiel tw = 18 (mit einer Zweiggröße von 5). Nehmen wir an, wir verkleinern (entfernen tw) - dies bedeutet, dass wir in den Aktualisierungen des folgenden Beispiels '-' anstelle von '+' verwenden.

Wir verwenden zuerst eine (leicht veränderte) Ahnenfunktion, um den sz-Wert zu aktualisieren.

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

Dann müssen wir das Tw für diejenigen anpassen, deren Tw höher ist als der zu entfernende Zweig.

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;

Dann müssen wir das Elternteil für diejenigen anpassen, deren pa's tw höher ist als der zu entfernende Zweig.

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

Angenommen, Sie wissen, dass die Stammelemente Null sind, ist hier der Pseudocode, der in Text ausgegeben werden soll:

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

Sie können jede andere Datenstruktur mit einer Hashmap emulieren, das ist also keine schreckliche Einschränkung. Beim Scannen von oben nach unten erstellen Sie für jede Zeile der Datenbank eine Hashmap mit einem Eintrag für jede Spalte. Fügen Sie jede dieser Hashmaps zu einer "Master" -Hashmap hinzu, die auf der ID angegeben ist. Wenn ein Knoten ein "übergeordnetes Element" hat, das Sie noch nicht gesehen haben, erstellen Sie einen Platzhaltereintrag dafür in der Master-Hashmap und füllen Sie ihn aus, wenn Sie den tatsächlichen Knoten sehen.

Führen Sie zum Ausdrucken einen einfachen Tiefen-First-Durchlauf durch die Daten durch und verfolgen Sie dabei die Einrückungsstufe. Sie können dies vereinfachen, indem Sie für jede Zeile einen "untergeordneten" Eintrag beibehalten und ihn beim Scannen der Daten ausfüllen.

Ob es eine "bessere" Möglichkeit gibt, einen Baum in einer Datenbank zu speichern, hängt davon ab, wie Sie die Daten verwenden. Ich habe Systeme mit einer bekannten maximalen Tiefe gesehen, die für jede Ebene in der Hierarchie eine andere Tabelle verwendeten. Das ist sehr sinnvoll, wenn die Ebenen im Baum doch nicht ganz gleich sind (Kategorien der obersten Ebene unterscheiden sich von den Blättern).


1

Wenn verschachtelte Hash-Maps oder Arrays erstellt werden können, kann ich einfach die Tabelle von Anfang an durchgehen und jedes Element dem verschachtelten Array hinzufügen. Ich muss jede Zeile bis zum Wurzelknoten verfolgen, um zu wissen, in welche Ebene des verschachtelten Arrays sie eingefügt werden soll. Ich kann Memoization verwenden, damit ich nicht immer wieder denselben Elternteil nachschlagen muss.

Bearbeiten: Ich würde zuerst die gesamte Tabelle in ein Array einlesen, damit die Datenbank nicht wiederholt abgefragt wird. Dies ist natürlich nicht praktikabel, wenn Ihr Tisch sehr groß ist.

Nachdem die Struktur erstellt wurde, muss ich zuerst eine Tiefe durchlaufen und den HTML-Code ausdrucken.

Es gibt keinen besseren grundlegenden Weg, um diese Informationen in einer Tabelle zu speichern (ich könnte mich jedoch irren;) und würde gerne eine bessere Lösung sehen). Wenn Sie jedoch ein Schema erstellen, um dynamisch erstellte DB-Tabellen zu verwenden, haben Sie auf Kosten der Einfachheit und des Risikos der SQL-Hölle eine ganz neue Welt eröffnet;).


1
Ich möchte das DB-Layout lieber nicht ändern, nur weil eine neue Ebene von Unterknoten benötigt wird. :-)
Tomalak

1

Wenn sich die Elemente in der Baumreihenfolge befinden, wie in Ihrem Beispiel gezeigt, können Sie etwa das folgende Python-Beispiel verwenden:

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)

Dadurch wird ein Stapel verwaltet, der die aktuelle Position im Baum darstellt. Für jedes Element in der Tabelle werden Stapelelemente angezeigt (die übereinstimmenden Divs werden geschlossen), bis das übergeordnete Element des aktuellen Elements gefunden wird. Dann gibt es den Start dieses Knotens aus und schiebt ihn auf den Stapel.

Wenn Sie den Baum mit Einrückungen anstatt mit verschachtelten Elementen ausgeben möchten, können Sie einfach die print-Anweisungen überspringen, um die Divs zu drucken, und vor jedem Element eine Anzahl von Leerzeichen drucken, die einem Vielfachen der Größe des Stapels entsprechen. Zum Beispiel in Python:

print "  " * len(stack)

Sie können diese Methode auch problemlos verwenden, um eine Reihe verschachtelter Listen oder Wörterbücher zu erstellen.

Bearbeiten: Ich sehe aus Ihrer Klarstellung, dass die Namen nicht als Knotenpfade gedacht waren. Das deutet auf einen alternativen Ansatz hin:

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

Dies konstruiert einen Baum von Tupelarrays (!). idx [0] repräsentiert die Wurzel (n) des Baums. Jedes Element in einem Array ist ein 2-Tupel, das aus dem Knoten selbst und einer Liste aller untergeordneten Elemente besteht. Nach der Erstellung können Sie an idx [0] festhalten und idx verwerfen, es sei denn, Sie möchten über ihre ID auf Knoten zugreifen.


1

Um die SQL-Lösung von Bill zu erweitern, können Sie dasselbe grundsätzlich mit einem flachen Array tun. Wenn Ihre Zeichenfolgen alle dieselbe Länge haben und Ihre maximale Anzahl von untergeordneten Zeichen bekannt ist (z. B. in einem Binärbaum), können Sie dies mit einer einzelnen Zeichenfolge (Zeichenarray) tun. Wenn Sie eine beliebige Anzahl von Kindern haben, erschwert dies die Dinge ein wenig ... Ich müsste meine alten Notizen überprüfen, um zu sehen, was getan werden kann.

Wenn Sie dann ein wenig Speicher opfern, insbesondere wenn Ihr Baum spärlich und / oder nicht ausgeglichen ist, können Sie mit ein wenig Indexmathematik zufällig auf alle Zeichenfolgen zugreifen, indem Sie Ihren Baum mit der Breite zuerst im Array wie folgt speichern (für eine Binärdatei) Baum):

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

Du kennst deine Saitenlänge, du weißt es

Ich bin jetzt auf der Arbeit, kann also nicht viel Zeit damit verbringen, aber mit Interesse kann ich ein bisschen Code abrufen, um dies zu tun.

Wir verwenden es, um in binären Bäumen zu suchen, die aus DNA-Codons bestehen. Ein Prozess, der den Baum erstellt hat, wurde dann abgeflacht, um nach Textmustern zu suchen. Wenn wir gefunden werden, erhalten wir den Knoten zurück, obwohl Indexmathematik (Umkehrung von oben) ... sehr schnell und effizient, hart, unser Baum hatte selten leere Knoten, aber wir konnten im Handumdrehen Gigabyte an Daten suchen.


0

Denken Sie daran, nosql-Tools wie neo4j für hierarchische Strukturen zu verwenden. zB eine vernetzte Anwendung wie LinkedIn verwendet Couchbase (eine andere NOSQL-Lösung)

Verwenden Sie nosql jedoch nur für Abfragen auf Data-Mart-Ebene und nicht zum Speichern / Verwalten von Transaktionen


Nachdem ich die Komplexität und Perfektion von SQL- und "Nicht-Tabellen" -Strukturen gelesen hatte, war dies auch mein erster Gedanke, nosql. Natürlich gibt es so viele Probleme beim Exportieren usw. Außerdem erwähnte das OP nur Tabellen. Naja. Ich bin kein DB-Experte, wie es offensichtlich ist.
Josef.B
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.