Qual è il modo più efficiente / elegante per analizzare un tavolo piatto in un albero?


517

Supponiamo di avere una tabella piatta che memorizza una gerarchia ad albero ordinata:

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

Ecco un diagramma, dove abbiamo [id] Name. Il nodo radice 0 è fittizio.

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

Quale approccio minimalista useresti per inviarlo a HTML (o testo, per quella materia) come un albero correttamente ordinato, correttamente rientrato?

Supponi inoltre di avere solo strutture di dati di base (array e hashmap), nessun oggetto di fantasia con riferimenti padre / figlio, nessun ORM, nessun framework, solo le tue due mani. La tabella è rappresentata come un set di risultati, a cui è possibile accedere in modo casuale.

Lo pseudo codice o l'inglese semplice va bene, questa è puramente una domanda concettuale.

Domanda bonus: esiste un modo fondamentalmente migliore per memorizzare una struttura ad albero come questa in un RDBMS?


MODIFICHE E AGGIUNTE

Per rispondere alla domanda di un commentatore ( Mark Bessey ): un nodo radice non è necessario, perché non verrà mai visualizzato comunque. ParentId = 0 è la convenzione per esprimere "questi sono di livello superiore". La colonna Ordine definisce come verranno ordinati i nodi con lo stesso genitore.

Il "set di risultati" di cui ho parlato può essere rappresentato come una serie di hashmap (per rimanere in quella terminologia). Per il mio esempio doveva essere già lì. Alcune risposte fanno il possibile e lo costruiscono per prime, ma va bene.

L'albero può essere arbitrariamente profondo. Ogni nodo può avere N figli. Tuttavia, non avevo esattamente in mente un albero da "milioni di voci".

Non confondere la mia scelta del nome del nodo ('Nodo 1.1.1') con qualcosa su cui fare affidamento. I nodi potrebbero ugualmente essere chiamati "Frank" o "Bob", non è implicita alcuna struttura di denominazione, questo era semplicemente per renderlo leggibile.

Ho pubblicato la mia soluzione in modo che voi ragazzi la possiate fare a pezzi.


2
"nessun oggetto di fantasia con riferimenti genitori / figli" - perché no? La creazione di un oggetto Node di base con i metodi .addChild (), .getParent () consente di modellare piuttosto bene la relazione del nodo.
opaco b

2
È un albero normale (n figli dove n può essere> 2) o albero binario (il nodo può avere 0, 1 o 2 figli)?
BKimmel,

Dal momento che è possibile implementare una corretta struttura dei dati del nodo con una hashmap, qui non ci sono restrizioni reali, solo più lavoro.
Svante,

... ed è esattamente quello che hai fatto.
Svante,

Risposte:


451

Ora che MySQL 8.0 supporta query ricorsive , possiamo dire che tutti i database SQL più diffusi supportano query ricorsive in sintassi standard.

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;

Ho testato le query ricorsive in MySQL 8.0 nella mia presentazione Lancio di query ricorsive nel 2017.

Di seguito è la mia risposta originale del 2008:


Esistono diversi modi per archiviare i dati strutturati ad albero in un database relazionale. Ciò che mostri nel tuo esempio utilizza due metodi:

  • Elenco di adiacenza (la colonna "padre") e
  • Enumerazione percorso (i numeri punteggiati nella colonna del tuo nome).

Un'altra soluzione si chiama Set nidificati e può essere archiviata anche nella stessa tabella. Leggi " Alberi e gerarchie in SQL per Smarties " di Joe Celko per molte più informazioni su questi progetti.

Di solito preferisco un design chiamato Tabella di chiusura (noto anche come "relazione di adiacenza") per la memorizzazione di dati strutturati ad albero. Richiede un'altra tabella, ma eseguire query sugli alberi è piuttosto semplice.

Tratto Tabella di chiusura nella mia presentazione Modelli per dati gerarchici con SQL e PHP e nel mio libro SQL Antipatterns: evitare le insidie ​​della programmazione di database .

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

Memorizza tutti i percorsi nella Tabella di chiusura, dove esiste un antenato diretto da un nodo a un altro. Includere una riga per ogni nodo per fare riferimento a se stesso. Ad esempio, utilizzando il set di dati che hai mostrato nella tua domanda:

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

Ora puoi ottenere un albero a partire dal nodo 1 in questo modo:

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

L'output (nel client MySQL) è simile al seguente:

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

In altre parole, i nodi 3 e 5 sono esclusi, perché fanno parte di una gerarchia separata, non discendente dal nodo 1.


Ri: commento di e-satis su figli immediati (o genitore immediato). È possibile aggiungere una path_lengthcolonna " " a ClosureTableper semplificare l'interrogazione specifica per un figlio o un genitore immediato (o qualsiasi altra distanza).

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

Quindi è possibile aggiungere un termine nella ricerca per interrogare i figli immediati di un determinato nodo. Questi sono i discendenti di cui 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 | 
+----+

Riguardo al commento di @ashraf: "Che ne dici di ordinare l'intero albero [per nome]?"

Ecco una query di esempio per restituire tutti i nodi che sono discendenti del nodo 1, unirli alla FlatTable che contiene altri attributi di nodo come namee ordinare in base al nome.

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

Per commentare da @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         |
+------------+-------------+

Un utente ha suggerito una modifica oggi. I moderatori SO hanno approvato la modifica, ma la sto invertendo.

La modifica ha suggerito che ORDER BY nell'ultima query sopra dovrebbe essere ORDER BY b.path_length, f.name, presumibilmente per assicurarsi che l'ordinamento corrisponda alla gerarchia. Ma questo non funziona, perché ordinerebbe "Nodo 1.1.1" dopo "Nodo 1.2".

Se si desidera che l'ordinamento corrisponda alla gerarchia in modo ragionevole, ciò è possibile, ma non semplicemente ordinando in base alla lunghezza del percorso. Ad esempio, vedere la mia risposta al database gerarchico della tabella di chiusura di MySQL - Come estrarre le informazioni nell'ordine corretto .


6
Questo è molto elegante, grazie. Punto bonus assegnato. ;-) Vedo però un piccolo inconveniente: poiché memorizza la relazione figlio in modo esplicito e implicito, è necessario fare un attento AGGIORNAMENTO anche per un piccolo spostamento nella struttura ad albero.
Tomalak,

16
È vero, ogni metodo di archiviazione delle strutture ad albero in un database richiede un po 'di lavoro, sia durante la creazione o l'aggiornamento dell'albero, sia durante l'interrogazione di alberi e sottostrutture. Scegli il design in base al quale desideri essere più semplice: scrivere o leggere.
Bill Karwin,

2
@buffer, c'è la possibilità di creare incoerenze mentre crei tutte le righe per una gerarchia. Elenco adiacenza ( parent_id) ha solo una riga per esprimere ogni relazione genitore-figlio, ma la Tabella di chiusura ne ha molte.
Bill Karwin,

1
@BillKarwin Un'altra cosa, sono le Tabelle di chiusura adatte per un grafico con più percorsi verso un dato nodo (ad esempio una gerarchia di categorie in cui qualsiasi nodo foglia o non foglia può appartenere a più di un genitore)
utente

2
@Reza, in modo che se si aggiunge un nuovo nodo figlio, è possibile eseguire una query per tutti i discendenti di (1) e questi sono gli antenati del nuovo figlio.
Bill Karwin,

58

Se si utilizzano insiemi nidificati (a volte definiti Traversal albero pre-ordine modificato) è possibile estrarre l'intera struttura ad albero o qualsiasi sottostruttura al suo interno in ordine di albero con una singola query, a costo di inserti più costosi, in quanto è necessario gestire le colonne che descrivono un percorso in ordine attraverso la struttura ad albero.

Per django-mptt , ho usato una struttura come questa:

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

Che descrive un albero che assomiglia a questo (con la idrappresentazione di ogni elemento):

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

Oppure, come un diagramma set nidificato che rende più ovvio il funzionamento dei valori lfte rght:

 __________________________________________________________________________
| Radice 1 |
| ________________________________ ________________________________ |
| | Bambino 1.1 | | Bambino 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
| | ________________________________ | | ________________________________ | |
| __________________________________________________________________________ |

Come puoi vedere, per ottenere l'intera sottostruttura per un dato nodo, in ordine di albero, devi semplicemente selezionare tutte le righe che hanno lfte rghtvalori tra i suoi lfte rghtvalori. È anche semplice recuperare l'albero degli antenati per un dato nodo.

La levelcolonna è un po 'di denormalizzazione per comodità più che altro e la tree_idcolonna consente di riavviare la numerazione lfte rghtper ogni nodo di livello superiore, il che riduce il numero di colonne interessate da inserimenti, spostamenti ed eliminazioni, come devono essere le colonne lfte rghtadattato di conseguenza quando si verificano queste operazioni al fine di creare o colmare lacune. Ho preso alcune note di sviluppo nel momento in cui stavo cercando di avvolgere la mia testa attorno alle domande richieste per ogni operazione.

In termini di lavoro effettivo con questi dati per visualizzare un albero, ho creato una tree_item_iteratorfunzione di utilità che, per ciascun nodo, dovrebbe fornire informazioni sufficienti per generare il tipo di visualizzazione desiderato.

Ulteriori informazioni su MPTT:


9
Vorrei che smetteremmo di usare abbreviazioni come lfte rghtper i nomi delle colonne, intendo quanti caratteri non abbiamo dovuto digitare? uno?!
orustammanapov,

21

È una domanda piuttosto vecchia, ma dato che ha molti punti di vista, penso che valga la pena presentare una soluzione alternativa e, a mio avviso, molto elegante.

Per leggere una struttura ad albero è possibile utilizzare espressioni di tabella comuni (CTE) ricorsive . Dà la possibilità di recuperare l'intera struttura ad albero in una volta, avere le informazioni sul livello del nodo, il suo nodo genitore e l'ordine all'interno dei figli del nodo genitore.

Lascia che ti mostri come funzionerebbe in PostgreSQL 9.1.

  1. Crea una struttura

    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. Scrivi una domanda

    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;
    

    Ecco i risultati:

     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)
    

    I nodi dell'albero sono ordinati per livello di profondità. Nell'output finale li presenteremmo nelle righe successive.

    Per ogni livello, sono ordinati per parent_id e node_order all'interno del parent. Questo ci dice come presentarli nel nodo output - link al genitore in questo ordine.

    Avere una struttura del genere non sarebbe difficile realizzare una presentazione davvero piacevole in HTML.

    I CTE ricorsivi sono disponibili in PostgreSQL, IBM DB2, MS SQL Server e Oracle .

    Se desideri saperne di più sulle query ricorsive SQL, puoi controllare la documentazione del tuo DBMS preferito o leggere i miei due articoli che trattano questo argomento:


18

A partire da Oracle 9i, è possibile utilizzare 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)

A partire da SQL Server 2005, è possibile utilizzare un'espressione di tabella comune (CTE) ricorsiva.

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]

Entrambi produrranno i seguenti risultati.

Nome
"Nodo 1"
"Nodo 1.1"
"Nodo 1.1.1"
"Nodo 1.2"
"Nodo 2"
"Nodo 2.1"

cte può essere usato sia in sqlserver che in oracle @Eric Weilnau
Nisar

9

La risposta di Bill è piuttosto dannatamente buona, questa risposta aggiunge alcune cose che mi fanno desiderare risposte filettate supportate da SO.

Comunque volevo supportare la struttura ad albero e la proprietà Order. Ho incluso una singola proprietà in ciascun nodo chiamato leftSiblingche fa la stessa cosa che Orderdovrebbe fare nella domanda originale (mantenere l'ordine da sinistra a destra).

mysql> desc nodes;
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| Campo | Digita | Null | Chiave | Predefinito | Extra |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| id | int (11) | NO | PRI | NULL | auto_increment |
| nome | varchar (255) | SÌ | | NULL | |
| leftSibling | int (11) | NO | | 0 | |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
3 righe in set (0,00 sec)

mysql> desc adiacenze;
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| Campo | Digita | Null | Chiave | Predefinito | Extra |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| relazioneId | int (11) | NO | PRI | NULL | auto_increment |
| genitore | int (11) | NO | | NULL | |
| bambino | int (11) | NO | | NULL | |
| pathLen | int (11) | NO | | NULL | |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
4 righe in set (0,00 sec)

Maggiori dettagli e codice SQL sul mio blog .

Grazie Bill la tua risposta è stata utile per iniziare!


7

Ben data la scelta, userei gli oggetti. Creerei un oggetto per ogni record in cui ogni oggetto ha una childrenraccolta e li memorizzerei tutti in un array assoc (/ hashtable) in cui l'ID è la chiave. E sfoglia una volta la collezione, aggiungendo i bambini ai campi relativi ai bambini. Semplice.

Ma poiché non ti diverti limitando l'uso di un po 'di OOP buono, probabilmente ripeterei in base a:

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

PrintLine(0, 0)

Modifica: è simile a un paio di altre voci, ma penso che sia leggermente più pulito. Una cosa che aggiungerò: è estremamente ad alta intensità di SQL. È brutto . Se hai la scelta, segui il percorso OOP.


Questo è ciò che intendevo con "nessun framework": stai usando LINQ, vero? Per quanto riguarda il tuo primo paragrafo: il set di risultati è già lì, perché copiare prima tutte le informazioni in una nuova struttura di oggetti? (Non ero abbastanza chiaro su questo fatto, scusa)
Tomalak,

Tomalak: no, il codice è pseudo-codice. Ovviamente dovresti suddividere le cose in selezioni e iteratori appropriati ... e una vera sintassi! Perché OOP? Perché puoi rispecchiare esattamente la struttura. Mantiene le cose belle e sembra essere più efficiente (solo una selezione)
Oli,

Non avevo in mente selezioni ripetute. Riguardo a OOP: Mark Bessey ha detto nella sua risposta: "Puoi emulare qualsiasi altra struttura di dati con una hashmap, quindi non è un limite terribile". La tua soluzione è corretta, ma penso che ci sia un certo margine di miglioramento anche senza OOP.
Tomalak,

5

Questo è stato scritto rapidamente e non è né carino né efficiente (in più si autobox molto, convertendo tra inted Integerè fastidioso!), Ma funziona.

Probabilmente infrange le regole poiché sto creando i miei oggetti, ma ehi, lo sto facendo come diversivo dal lavoro reale :)

Ciò presuppone anche che il set di risultati / tabella sia completamente letto in una sorta di struttura prima di iniziare a costruire nodi, che non sarebbe la soluzione migliore se si dispone di centinaia di migliaia di righe.

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

Trovo sempre difficile filtrare la parte specifica dell'algoritmo dalla parte specifica dell'implementazione quando viene presentato con molto codice sorgente. Ecco perché ho chiesto una soluzione che non fosse specifica per la lingua in primo luogo. Ma fa il lavoro, quindi grazie per il tuo tempo!
Tomalak,

Capisco cosa intendi ora, se non è ovvio che l'algoritmo principale si trova in NodeBuilder.build () - probabilmente avrei potuto fare un lavoro migliore nel riassumere questo.
matt b,

5

Esistono soluzioni davvero valide che sfruttano la rappresentazione interna di btree degli indici sql. Questo si basa su alcune grandi ricerche fatte intorno al 1998.

Ecco una tabella di esempio (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
)

Gli unici campi necessari per la rappresentazione ad albero sono:

  • tw: l'indice di preordine DFS da sinistra a destra, dove root = 1.
  • pa: il riferimento (usando tw) al nodo padre, root ha null.
  • sz: la dimensione del ramo del nodo incluso se stesso.
  • nc: è usato come zucchero sintattico. è tw + nc e rappresenta il tw del "figlio successivo" del nodo.

Ecco un esempio di popolazione di 24 nodi, ordinata per 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 |
+-----+---------+----+------+------+------+

Ogni risultato dell'albero può essere fatto in modo non ricorsivo. Ad esempio, per ottenere un elenco di antenati del nodo in tw = '22 '

antenati

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

Fratelli e figli sono banali: basta usare l'ordinamento dei campi pa per tw.

discendenti

Ad esempio il set (ramo) di nodi che sono radicati in 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 |
+-----+---------+----+------+------+------+

Note aggiuntive

Questa metodologia è estremamente utile per i casi in cui esiste un numero di letture molto maggiore rispetto a inserti o aggiornamenti.

Poiché l'inserimento, lo spostamento o l'aggiornamento di un nodo nella struttura richiedono la modifica della struttura, è necessario bloccare la tabella prima di iniziare l'azione.

Il costo di inserimento / eliminazione è elevato perché i valori dell'indice tw e sz (dimensione del ramo) dovranno essere aggiornati su tutti i nodi dopo il punto di inserimento e rispettivamente per tutti gli antenati.

Lo spostamento del ramo comporta lo spostamento del valore tw del ramo fuori dall'intervallo, quindi è anche necessario disabilitare i vincoli di chiave esterna quando si sposta un ramo. Esistono essenzialmente quattro query per spostare un ramo:

  • Sposta il ramo fuori portata.
  • Colmare il divario che ha lasciato. (l'albero rimanente è ora normalizzato).
  • Apri lo spazio dove andrà.
  • Sposta il ramo nella sua nuova posizione.

Regola query albero

L'apertura / chiusura degli spazi vuoti nell'albero è un'importante sottofunzione utilizzata dai metodi di creazione / aggiornamento / eliminazione, quindi la includo qui.

Abbiamo bisogno di due parametri: un flag che rappresenti o meno il ridimensionamento o l'upsize e l'indice tw del nodo. Quindi, ad esempio tw = 18 (che ha una dimensione del ramo di 5). Supponiamo che stiamo ridimensionando (rimuovendo tw) - questo significa che stiamo usando '-' invece di '+' negli aggiornamenti del seguente esempio.

Per prima cosa utilizziamo una funzione antenata (leggermente modificata) per aggiornare il valore 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));

Quindi dobbiamo regolare il tw per quelli il cui tw è più alto del ramo da rimuovere.

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;

Quindi dobbiamo regolare il genitore per quelli il cui pa è più alto del ramo da rimuovere.

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

Supponendo che tu sappia che gli elementi radice sono zero, ecco lo pseudocodice per l'output in testo:

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

Puoi emulare qualsiasi altra struttura di dati con una hashmap, quindi non è una limitazione terribile. Scansionando dall'alto verso il basso, si crea una hashmap per ogni riga del database, con una voce per ogni colonna. Aggiungi ognuna di queste hashap a una hashmap "master", digitata sull'id. Se un nodo ha un "genitore" che non hai ancora visto, crea una voce segnaposto per esso nella hashmap principale e compila quando vedi il nodo effettivo.

Per stamparlo, fai un semplice passaggio in profondità attraverso i dati, tenendo traccia del livello di rientro lungo il percorso. Puoi renderlo più semplice mantenendo una voce "figlio" per ogni riga e popolandola durante la scansione dei dati.

Se esiste un modo "migliore" per archiviare un albero in un database, ciò dipende dal modo in cui userete i dati. Ho visto sistemi con una profondità massima nota che utilizzavano una tabella diversa per ogni livello nella gerarchia. Ciò ha molto senso se i livelli dell'albero non sono del tutto equivalenti (le categorie di livello superiore sono diverse dalle foglie).


1

Se è possibile creare mappe o matrici di hash nidificate, allora posso semplicemente andare giù dalla tabella dall'inizio e aggiungere ogni elemento alla matrice nidificata. Devo tracciare ogni riga sul nodo principale per sapere in quale livello dell'array nidificato inserire. Posso utilizzare la memoization in modo da non dover cercare più volte lo stesso genitore.

Modifica: leggerei prima l'intera tabella in un array, quindi non interrogherà ripetutamente il DB. Naturalmente questo non sarà pratico se il tuo tavolo è molto grande.

Dopo che la struttura è stata costruita, prima devo attraversare una profondità e stampare l'HTML.

Non esiste un modo fondamentale migliore per archiviare queste informazioni usando una tabella (potrei sbagliarmi però;) e mi piacerebbe vedere una soluzione migliore). Tuttavia, se crei uno schema per impiegare tabelle di db create dinamicamente, allora hai aperto un mondo completamente nuovo a sacrificare la semplicità e il rischio dell'inferno di SQL;).


1
Preferirei non modificare il layout del DB solo perché è necessario un nuovo livello di sottonodi. :-)
Tomalak,

1

Se gli elementi sono in ordine ad albero, come mostrato nel tuo esempio, puoi usare qualcosa come il seguente esempio di 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)

Ciò che fa è mantenere uno stack che rappresenta la posizione corrente nella struttura. Per ogni elemento nella tabella, apre gli elementi dello stack (chiudendo i div corrispondenti) fino a quando non trova il genitore dell'elemento corrente. Quindi genera l'inizio di quel nodo e lo spinge nello stack.

Se si desidera produrre l'albero utilizzando rientri anziché elementi nidificati, è possibile semplicemente saltare le istruzioni di stampa per stampare i div e stampare un numero di spazi pari a un multiplo della dimensione della pila prima di ogni elemento. Ad esempio, in Python:

print "  " * len(stack)

È inoltre possibile utilizzare facilmente questo metodo per costruire una serie di elenchi o dizionari nidificati.

Modifica: vedo dal tuo chiarimento che i nomi non erano intesi come percorsi dei nodi. Ciò suggerisce un approccio alternativo:

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

Questo costruisce un albero di matrici di tuple (!). idx [0] rappresenta la radice o le radici dell'albero. Ogni elemento in un array è una 2 tupla costituita dal nodo stesso e da un elenco di tutti i suoi figli. Una volta creato, puoi conservare idx [0] e scartare idx, a meno che tu non voglia accedere ai nodi con il loro ID.


1

Per estendere la soluzione SQL di Bill puoi sostanzialmente fare lo stesso usando un array piatto. Inoltre, se le tue stringhe hanno tutte la stessa lunghezza e il tuo numero massimo di figli è noto (diciamo in un albero binario) puoi farlo usando una singola stringa (array di caratteri). Se hai un numero arbitrario di bambini questo complica un po 'le cose ... Dovrei controllare i miei vecchi appunti per vedere cosa si può fare.

Quindi, sacrificando un po 'di memoria, specialmente se il tuo albero è scarso e / o sbilanciato, puoi, con un po' di indice matematico, accedere a tutte le stringhe in modo casuale memorizzando il tuo albero, larghezza prima dell'array in questo modo (per un binario albero):

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

sai la lunghezza della tua stringa, lo sai

Sono al lavoro ora, quindi non posso passare molto tempo su di esso, ma con interesse posso prendere un po 'di codice per farlo.

Lo usiamo per cercare negli alberi binari fatti di codoni del DNA, un processo ha costruito l'albero, quindi l'abbiamo appiattito per cercare schemi di testo e quando viene trovato, sebbene l'indice matematico (inverti dall'alto) riportiamo il nodo ... molto veloce ed efficiente, resistente il nostro albero raramente aveva nodi vuoti, ma potevamo scovare gigabyte di dati in un batter d'occhio.


0

Pensa all'utilizzo di strumenti nosql come neo4j per strutture gerarchiche. ad es. un'applicazione in rete come il linkedin utilizza couchbase (un'altra soluzione nosql)

Ma utilizzare nosql solo per query a livello di data mart e non per archiviare / gestire le transazioni


Dopo aver letto le complessità e il perf di SQL e strutture "non-table", questo è stato anche il mio primo pensiero, nosql. Naturalmente, ci sono così tanti problemi da esportare, ecc. Inoltre, l'OP ha menzionato solo le tabelle. Oh bene. Non sono un esperto di DB, come è ovvio.
Josef.B,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.