Quelle est la façon la plus efficace / élégante d'analyser une table plate dans un arbre?

517

Supposons que vous ayez une table plate qui stocke une hiérarchie d'arbre ordonnée:

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

Voici un diagramme, où nous en avons [id] Name. Le nœud racine 0 est fictif.

                       [0] RACINE
                          / \ 
              [1] Nœud 1 [3] Nœud 2
              / \ \
    [2] Noeud 1.1 [6] Noeud 1.2 [5] Noeud 2.1
          /          
 [4] Noeud 1.1.1

Quelle approche minimaliste utiliseriez-vous pour produire cela en HTML (ou texte, d'ailleurs) sous la forme d'un arbre correctement ordonné et correctement en retrait?

Supposons en outre que vous n'avez que des structures de données de base (tableaux et hashmaps), pas d'objets fantaisistes avec des références parent / enfants, pas d'ORM, pas de framework, juste vos deux mains. Le tableau est représenté comme un ensemble de résultats, auquel on peut accéder de manière aléatoire.

Le pseudo-code ou l'anglais simple est correct, c'est une question purement conceptuelle.

Question bonus: Existe-t-il un moyen fondamentalement meilleur de stocker une structure arborescente comme celle-ci dans un SGBDR?


MODIFICATIONS ET ADDITIONS

Pour répondre à la question d'un intervenant ( Mark Bessey ): Un nœud racine n'est pas nécessaire, car il ne sera jamais affiché de toute façon. ParentId = 0 est la convention pour exprimer "ce sont des niveaux supérieurs". La colonne Ordre définit comment les nœuds avec le même parent vont être triés.

Le "jeu de résultats" dont j'ai parlé peut être décrit comme un tableau de hashmaps (pour rester dans cette terminologie). Car mon exemple était censé être déjà là. Certaines réponses vont plus loin et le construisent d'abord, mais ça va.

L'arbre peut être arbitrairement profond. Chaque nœud peut avoir N enfants. Cependant, je n'avais pas exactement une arborescence de "millions d'entrées" en tête.

Ne confondez pas mon choix de nom de nœud ('Node 1.1.1') avec quelque chose sur lequel compter. Les nœuds pourraient également être appelés «Frank» ou «Bob», aucune structure de dénomination n'est impliquée, c'était simplement pour la rendre lisible.

J'ai publié ma propre solution pour que vous puissiez la mettre en pièces.

Tomalak
la source
2
"pas d'objets fantaisistes avec des références parents / enfants" - pourquoi pas? La création d'un objet Node de base avec les méthodes .addChild (), .getParent () vous permet de modéliser assez bien la relation entre les nœuds.
mat b
2
Est-ce un arbre régulier (n enfants où n peut être> 2) ou un arbre binaire (le nœud peut avoir 0, 1 ou 2 enfants)?
BKimmel
Comme vous pouvez implémenter une structure de données de nœud appropriée avec une table de hachage, il n'y a pas de véritable restriction ici, juste plus de travail.
Svante
... et c'est exactement ce que vous avez fait.
Svante

Réponses:

451

Maintenant que MySQL 8.0 prend en charge les requêtes récursives , nous pouvons dire que toutes les bases de données SQL populaires prennent en charge les requêtes récursives dans la syntaxe 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;

J'ai testé les requêtes récursives dans MySQL 8.0 dans ma présentation Recursive Query Throwdown en 2017.

Voici ma réponse originale de 2008:


Il existe plusieurs façons de stocker des données arborescentes dans une base de données relationnelle. Ce que vous montrez dans votre exemple utilise deux méthodes:

  • Liste d'adjacence (la colonne "parent") et
  • Énumération du chemin (les nombres en pointillés dans votre colonne de nom).

Une autre solution est appelée ensembles imbriqués et peut également être stockée dans la même table. Lisez " Arbres et hiérarchies en SQL pour Smarties " par Joe Celko pour plus d'informations sur ces conceptions.

Je préfère généralement une conception appelée Closure Table (aka "Adjacency Relation") pour stocker des données arborescentes. Il nécessite une autre table, mais interroger les arbres est assez facile.

Je couvre la table de fermeture dans ma présentation Modèles de données hiérarchiques avec SQL et PHP et dans mon livre Antipatterns SQL: éviter les pièges de la programmation de base de données .

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

Stockez tous les chemins dans la table de fermeture, où il existe une ascendance directe d'un nœud à un autre. Inclure une ligne pour chaque nœud pour se référencer. Par exemple, en utilisant l'ensemble de données que vous avez montré dans votre question:

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

Vous pouvez maintenant obtenir un arbre commençant au nœud 1 comme ceci:

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

La sortie (dans le client MySQL) ressemble à ceci:

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

En d'autres termes, les nœuds 3 et 5 sont exclus, car ils font partie d'une hiérarchie distincte, ne descendant pas du nœud 1.


Re: commentaire d'e-satis sur les enfants immédiats (ou le parent immédiat). Vous pouvez ajouter une path_lengthcolonne " " pour ClosureTablefaciliter la recherche spécifique d'un enfant ou d'un parent immédiat (ou de toute autre distance).

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

Ensuite, vous pouvez ajouter un terme dans votre recherche pour interroger les enfants immédiats d'un nœud donné. Ce sont des descendants dont le 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 | 
+----+

Commentaire de @ashraf: "Que diriez-vous de trier tout l'arbre [par nom]?"

Voici un exemple de requête pour renvoyer tous les nœuds qui sont des descendants du nœud 1, les joindre à la FlatTable qui contient d'autres attributs de nœud tels que nameet trier par nom.

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

Re commentaire de @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 utilisateur a suggéré une modification aujourd'hui. Les modérateurs ont approuvé la modification, mais je l'inverse.

L'édition a suggéré que l'ORDRE PAR dans la dernière requête ci-dessus soit ORDER BY b.path_length, f.name, probablement pour s'assurer que la commande correspond à la hiérarchie. Mais cela ne fonctionne pas, car il ordonnerait "Node 1.1.1" après "Node 1.2".

Si vous voulez que le classement corresponde à la hiérarchie de manière sensée, c'est possible, mais pas simplement en triant par la longueur du chemin. Par exemple, voir ma réponse à la base de données hiérarchique MySQL Closure Table - Comment extraire les informations dans le bon ordre .

Bill Karwin
la source
6
C'est très élégant, merci. Point bonus attribué. ;-) Je vois cependant un petit inconvénient - car il stocke la relation enfant explicitement et implicitement, vous devez faire beaucoup de MISE À JOUR prudente, même pour un petit changement dans la structure de l'arbre.
Tomalak
16
Certes, chaque méthode de stockage des arborescences dans une base de données nécessite un certain travail, soit lors de la création ou de la mise à jour de l'arborescence, soit lors de l'interrogation des arbres et des sous-arbres. Choisissez le design sur lequel vous souhaitez être plus simple: écrire ou lire.
Bill Karwin,
2
@buffer, il est possible de créer des incohérences lorsque vous créez toutes les lignes d'une hiérarchie. La liste d'adjacence ( parent_id) n'a qu'une seule ligne pour exprimer chaque relation parent-enfant, mais la table de fermeture en a plusieurs.
Bill Karwin
1
@BillKarwin Une autre chose, les tables de fermeture conviennent à un graphique avec plusieurs chemins d'accès à un nœud donné (par exemple, une hiérarchie de catégories où n'importe quel nœud feuille ou non feuille peut appartenir à plusieurs parents)
utilisateur
2
@Reza, de sorte que si vous ajoutez un nouveau nœud enfant, vous pouvez interroger tous les descendants de (1) et ce sont les ancêtres du nouvel enfant.
Bill Karwin
58

Si vous utilisez des ensembles imbriqués (parfois appelés Traversée d'arbre de précommande modifiée), vous pouvez extraire la totalité de la structure de l'arborescence ou tout sous-arbre dans celle-ci dans un ordre d'arborescence avec une seule requête, au prix d'insertions plus coûteuses, car vous devez gérer les colonnes qui décrivent un chemin dans l'ordre à travers la structure arborescente.

Pour django-mptt , j'ai utilisé une structure comme celle-ci:

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

Qui décrit un arbre qui ressemble à ceci (avec idreprésentant chaque élément):

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

Ou, sous forme de diagramme d'ensemble imbriqué qui rend plus évident le fonctionnement des valeurs lftet rght:

 __________________________________________________________________________
| Racine 1 |
| ________________________________ ________________________________ |
| | Enfant 1.1 | | Enfant 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
| | ________________________________ | | ________________________________ | |
| __________________________________________________________________________ |

Comme vous pouvez le voir, pour obtenir la sous-arborescence entière pour un nœud donné, dans l'ordre de l'arborescence, vous devez simplement sélectionner toutes les lignes qui ont lftet des rghtvaleurs entre ses valeurs lftet rght. Il est également simple de récupérer l'arbre des ancêtres pour un nœud donné.

La levelcolonne est un peu de dénormalisation pour plus de commodité et la tree_idcolonne vous permet de redémarrer la numérotation lftet rghtpour chaque nœud de niveau supérieur, ce qui réduit le nombre de colonnes affectées par les insertions, les déplacements et les suppressions, car les colonnes lftet rghtdoivent être ajustés en conséquence lorsque ces opérations ont lieu afin de créer ou de combler des lacunes. J'ai pris quelques notes de développement au moment où j'essayais de faire le tour des requêtes requises pour chaque opération.

En termes de travail avec ces données pour afficher une arborescence, j'ai créé une tree_item_iteratorfonction utilitaire qui, pour chaque nœud, devrait vous donner suffisamment d'informations pour générer le type d'affichage que vous souhaitez.

Plus d'informations sur MPTT:

Jonny Buchanan
la source
9
Je souhaite que nous cessions d'utiliser des abréviations comme lftet rghtpour les noms de colonne, je veux dire combien de caractères nous n'avons pas eu à taper? une?!
orustammanapov
21

C'est une question assez ancienne, mais comme il y a beaucoup de points de vue, je pense qu'il vaut la peine de présenter une solution alternative et, à mon avis, très élégante.

Afin de lire une arborescence, vous pouvez utiliser des expressions de table communes récursives (CTE). Il donne la possibilité de récupérer la structure d'arbre entière à la fois, d'avoir les informations sur le niveau du nœud, son nœud parent et l'ordre dans les enfants du nœud parent.

Permettez-moi de vous montrer comment cela fonctionnerait dans PostgreSQL 9.1.

  1. Créer une structure

    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. Rédiger une requête

    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;

    Voici les résultats:

     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)

    Les nœuds d'arbre sont classés par niveau de profondeur. Dans la sortie finale, nous les présenterions dans les lignes suivantes.

    Pour chaque niveau, ils sont classés par parent_id et node_order dans le parent. Cela nous indique comment les présenter dans le nœud de liaison de sortie au parent dans cet ordre.

    Ayant une telle structure, il ne serait pas difficile de faire une très belle présentation en HTML.

    Les CTE récursifs sont disponibles dans PostgreSQL, IBM DB2, MS SQL Server et Oracle .

    Si vous souhaitez en savoir plus sur les requêtes SQL récursives, vous pouvez soit consulter la documentation de votre SGBD préféré, soit lire mes deux articles couvrant ce sujet:

Michał Kołodziejski
la source
18

Depuis Oracle 9i, vous pouvez utiliser 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)

Depuis SQL Server 2005, vous pouvez utiliser une expression de table commune récursive (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]

Les deux produiront les résultats suivants.

Nom
«Noeud 1»
«Node 1.1»
«Node 1.1.1»
«Node 1.2»
«Noeud 2»
«Node 2.1»
Eric Weilnau
la source
cte peut être utilisé dans sqlserver et oracle @Eric Weilnau
Nisar
9

La réponse de Bill est plutôt bonne, cette réponse y ajoute des choses qui me font souhaiter des réponses filetées SO supportées.

Quoi qu'il en soit, je voulais prendre en charge l'arborescence et la propriété Order. J'ai inclus une propriété unique dans chaque noeud appelé leftSiblingqui fait la même chose Orderque pour la question d'origine (maintenir l'ordre de gauche à droite).

mysql> nœuds desc;
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| Champ | Type | Null | Clé | Par défaut | Extra |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| id | int (11) | NON | PRI | NULL | auto_increment |
| nom | varchar (255) | OUI | | NULL | |
| leftSibling | int (11) | NON | | 0 | |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
3 rangées en jeu (0,00 sec)

mysql> desc adjacences;
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| Champ | Type | Null | Clé | Par défaut | Extra |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| relationId | int (11) | NON | PRI | NULL | auto_increment |
| parent | int (11) | NON | | NULL | |
| enfant | int (11) | NON | | NULL | |
| pathLen | int (11) | NON | | NULL | |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
4 lignes en jeu (0,00 sec)

Plus de détails et de code SQL sur mon blog .

Merci Bill, votre réponse a été utile pour commencer!

bobobobo
la source
7

Etant donné le choix, j'utiliserais des objets. Je créerais un objet pour chaque enregistrement où chaque objet a une childrencollection et les stockerais tous dans un tableau assoc (/ table de hachage) où l'ID est la clé. Et parcourez la collection une fois, en ajoutant les enfants aux champs enfants concernés. Facile.

Mais parce que vous n'êtes pas amusant en restreignant l'utilisation de bons POO, je répéterais probablement en fonction de:

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

PrintLine(0, 0)

Edit: c'est similaire à quelques autres entrées, mais je pense que c'est légèrement plus propre. Une chose que j'ajouterai: c'est extrêmement intensif en SQL. C'est méchant . Si vous avez le choix, suivez la route OOP.

Oli
la source
C'est ce que je voulais dire par "pas de frameworks" - vous utilisez LINQ, n'est-ce pas? Concernant votre premier paragraphe: le jeu de résultats est déjà là, pourquoi copier d'abord toutes les informations dans une nouvelle structure d'objet? (Je n'étais pas assez clair sur ce fait, désolé)
Tomalak
Tomalak - non, le code est un pseudo-code. Bien sûr, il faudrait diviser les choses en sélections et itérateurs appropriés ... et une vraie syntaxe! Pourquoi OOP? Parce que vous pouvez exactement refléter la structure. Cela garde les choses bien et il se trouve que c'est plus efficace (un seul choix)
Oli
Je n'avais pas non plus de choix répétés en tête. Concernant la POO: Mark Bessey a déclaré dans sa réponse: "Vous pouvez émuler n'importe quelle autre structure de données avec une table de hachage, donc ce n'est pas une limitation terrible.". Votre solution est correcte, mais je pense qu'il y a de la place pour une amélioration même sans POO.
Tomalak
5

Cela a été écrit rapidement, et n'est ni joli ni efficace (en plus il a beaucoup de boîtes automatiques, la conversion entre intet Integerest ennuyeux!), Mais cela fonctionne.

Cela brise probablement les règles puisque je crée mes propres objets mais bon je fais ça comme un détournement du vrai travail :)

Cela suppose également que le resultSet / table est complètement lu dans une sorte de structure avant de commencer à construire des nœuds, ce qui ne serait pas la meilleure solution si vous avez des centaines de milliers de lignes.

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;
    }
}
mat b
la source
J'ai toujours du mal à filtrer la partie spécifique à l'algorithme de la partie spécifique à l'implémentation lorsqu'elle est présentée avec beaucoup de code source. C'est pourquoi j'ai demandé une solution qui n'était pas spécifique à la langue en premier lieu. Mais il fait l'affaire, alors merci pour votre temps!
Tomalak
Je vois ce que vous voulez dire maintenant, si ce n'est pas évident, l'algorithme principal se trouve dans NodeBuilder.build () - j'aurais probablement pu mieux résumer cela.
mat
5

Il existe de très bonnes solutions qui exploitent la représentation interne des index sql. Ceci est basé sur de grandes recherches effectuées vers 1998.

Voici un exemple de table (en 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
)

Les seuls champs nécessaires à la représentation arborescente sont:

  • tw: L'index de pré-commande DFS de gauche à droite, où root = 1.
  • pa: La référence (en utilisant tw) au nœud parent, root a null.
  • sz: taille de la branche du nœud, y compris elle-même.
  • nc: est utilisé comme sucre syntaxique. il est tw + nc et représente le tw du "prochain enfant" du nœud.

Voici un exemple de population à 24 nœuds, ordonné par 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 |
+-----+---------+----+------+------+------+

Chaque résultat d'arbre peut être fait de manière non récursive. Par exemple, pour obtenir une liste des ancêtres du nœud à tw = '22 '

Les ancêtres

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

Les frères et sœurs et les enfants sont triviaux - utilisez simplement l'ordre des champs pa par tw.

Descendance

Par exemple, l'ensemble (branche) de nœuds enracinés à 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 |
+-----+---------+----+------+------+------+

Notes complémentaires

Cette méthodologie est extrêmement utile lorsque le nombre de lectures est beaucoup plus élevé qu'il n'y a d'insertions ou de mises à jour.

Étant donné que l'insertion, le déplacement ou la mise à jour d'un nœud dans l'arborescence nécessite que l'arborescence soit ajustée, il est nécessaire de verrouiller la table avant de commencer l'action.

Le coût d'insertion / suppression est élevé car les valeurs d'index tw et de sz (taille de branche) devront être mises à jour sur tous les nœuds après le point d'insertion et pour tous les ancêtres respectivement.

Le déplacement de branche implique de déplacer la valeur tw de la branche hors de portée, il est donc également nécessaire de désactiver les contraintes de clé étrangère lors du déplacement d'une branche. Il y a, essentiellement, quatre requêtes nécessaires pour déplacer une branche:

  • Déplacez la branche hors de portée.
  • Comblez l'écart qu'il a laissé. (l'arbre restant est maintenant normalisé).
  • Ouvrez l'écart où il ira.
  • Déplacez la branche dans sa nouvelle position.

Ajuster les requêtes d'arborescence

L'ouverture / fermeture des lacunes dans l'arborescence est une sous-fonction importante utilisée par les méthodes de création / mise à jour / suppression, donc je l'inclus ici.

Nous avons besoin de deux paramètres - un indicateur indiquant si nous réduisons ou augmentons ou non l'index tw du nœud. Ainsi, par exemple tw = 18 (qui a une taille de branche de 5). Supposons que nous réduisions (supprimions tw) - cela signifie que nous utilisons «-» au lieu de «+» dans les mises à jour de l'exemple suivant.

Nous utilisons d'abord une fonction ancêtre (légèrement modifiée) pour mettre à jour la valeur 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));

Ensuite, nous devons ajuster le tw pour ceux dont le tw est supérieur à la branche à supprimer.

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;

Ensuite, nous devons ajuster le parent pour ceux dont le tw de pa est supérieur à la branche à supprimer.

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;
Konchog
la source
3

En supposant que vous savez que les éléments racine sont nuls, voici le pseudocode à afficher en texte:

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)
wcm
la source
3

Vous pouvez émuler n'importe quelle autre structure de données avec une table de hachage, donc ce n'est pas une terrible limitation. De haut en bas, vous créez une table de hachage pour chaque ligne de la base de données, avec une entrée pour chaque colonne. Ajoutez chacun de ces hashmaps à un hashmap "maître", basé sur l'identifiant. Si un nœud a un "parent" que vous n'avez pas encore vu, créez une entrée d'espace réservé pour lui dans la table de hachage principale et remplissez-la lorsque vous voyez le nœud réel.

Pour l'imprimer, effectuez un simple passage en profondeur d'abord à travers les données, en gardant une trace du niveau de retrait en cours de route. Vous pouvez rendre cela plus facile en conservant une entrée "enfants" pour chaque ligne et en la remplissant lorsque vous numérisez les données.

Quant à savoir s'il existe une "meilleure" façon de stocker un arbre dans une base de données, cela dépend de la façon dont vous allez utiliser les données. J'ai vu des systèmes qui avaient une profondeur maximale connue qui utilisaient une table différente pour chaque niveau de la hiérarchie. Cela a beaucoup de sens si les niveaux dans l'arbre ne sont pas tout à fait équivalents après tout (les catégories de niveau supérieur étant différentes des feuilles).

Mark Bessey
la source
1

Si des cartes ou des tableaux de hachage imbriqués peuvent être créés, je peux simplement descendre le tableau depuis le début et ajouter chaque élément au tableau imbriqué. Je dois tracer chaque ligne jusqu'au nœud racine afin de savoir à quel niveau du tableau imbriqué insérer. Je peux utiliser la mémorisation pour ne pas avoir à rechercher le même parent encore et encore.

Edit: Je voudrais d'abord lire la table entière dans un tableau, donc il ne demandera pas la base de données à plusieurs reprises. Bien sûr, cela ne sera pas pratique si votre table est très grande.

Une fois que la structure est construite, je dois d'abord la parcourir en profondeur et imprimer le code HTML.

Il n'y a pas de meilleur moyen fondamental de stocker ces informations à l'aide d'une seule table (je peux me tromper cependant;), et j'aimerais voir une meilleure solution). Cependant, si vous créez un schéma pour utiliser des tables de base de données créées dynamiquement, vous avez ouvert un tout nouveau monde au prix de la simplicité et du risque d'enfer SQL;).

tchen
la source
1
Je préfère ne pas modifier la disposition de la base de données simplement parce qu'un nouveau niveau de sous-nœuds est nécessaire. :-)
Tomalak
1

Si les éléments sont dans l'ordre de l'arborescence, comme indiqué dans votre exemple, vous pouvez utiliser quelque chose comme l'exemple Python suivant:

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)

Cela permet de conserver une pile représentant la position actuelle dans l'arborescence. Pour chaque élément du tableau, il affiche les éléments de la pile (fermant les divisions correspondantes) jusqu'à ce qu'il trouve le parent de l'élément en cours. Ensuite, il sort le début de ce nœud et le pousse vers la pile.

Si vous souhaitez générer l'arborescence à l'aide d'éléments en retrait plutôt que imbriqués, vous pouvez simplement ignorer les instructions d'impression pour imprimer les divs et imprimer un nombre d'espaces égal à un multiple de la taille de la pile avant chaque élément. Par exemple, en Python:

print "  " * len(stack)

Vous pouvez également facilement utiliser cette méthode pour construire un ensemble de listes ou de dictionnaires imbriqués.

Edit: Je vois d'après votre clarification que les noms n'étaient pas destinés à être des chemins de noeud. Cela suggère une approche alternative:

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

Cela construit un arbre de tableaux de tuples (!). idx [0] représente la ou les racines de l'arbre. Chaque élément d'un tableau est un 2-tuple composé du nœud lui-même et d'une liste de tous ses enfants. Une fois construit, vous pouvez conserver idx [0] et ignorer idx, sauf si vous souhaitez accéder aux nœuds par leur ID.

Nick Johnson
la source
1

Pour étendre la solution SQL de Bill, vous pouvez essentiellement faire de même en utilisant un tableau plat. De plus, si vos chaînes ont toutes la même longueur et que votre nombre maximum d'enfants est connu (par exemple dans un arbre binaire), vous pouvez le faire en utilisant une seule chaîne (tableau de caractères). Si vous avez un nombre arbitraire d'enfants, cela complique un peu les choses ... Je devrais vérifier mes anciennes notes pour voir ce qui peut être fait.

Ensuite, en sacrifiant un peu de mémoire, surtout si votre arbre est clairsemé et / ou non équilibré, vous pouvez, avec un peu de calcul d'index, accéder à toutes les chaînes de façon aléatoire en stockant votre arbre, largeur d'abord dans le tableau comme ceci (pour un binaire arbre):

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

vous connaissez la longueur de votre chaîne, vous le savez

Je suis au travail maintenant, je ne peux donc pas y consacrer beaucoup de temps, mais avec intérêt, je peux récupérer un peu de code pour le faire.

Nous avons l'habitude de le faire pour rechercher dans des arbres binaires faits de codons d'ADN, un processus a construit l'arbre, puis nous l'avons aplati pour rechercher des modèles de texte et lorsqu'il est trouvé, bien que les mathématiques d'index (inverser d'en haut) nous récupérons le nœud ... très rapide et efficace, notre arbre avait rarement des nœuds vides, mais nous pouvions rechercher des gigaoctets de données en un tournemain.

Newtopian
la source
0

Pensez à utiliser des outils nosql comme neo4j pour les structures hiérarchiques. par exemple, une application en réseau comme linkedin utilise couchbase (une autre solution nosql)

Mais utilisez nosql uniquement pour les requêtes au niveau du magasin de données et non pour stocker / gérer les transactions

sreenivasulu kandakuru
la source
Ayant lu la complexité et les performances des structures SQL et "non-table", ce fut aussi ma première pensée, nosql. Bien sûr, il y a tellement de problèmes à exporter, etc. De plus, l'OP ne mentionnait que les tableaux. Tant pis. Je ne suis pas un expert DB, comme cela est évident.
Josef.B