106 votes

Convertir une série de relations parent-enfant en un arbre hiérarchique ?

J'ai un tas de paires nom-parent, que j'aimerais transformer en aussi peu d'arborescences hiérarchiques que possible. Donc, par exemple, voici les paires :

Child : Parent
    H : G
    F : G
    G : D
    E : D
    A : E
    B : C
    C : E
    D : NULL

Qui doit être transformé en un (des) arbre(s) hiérarchique(s) :

D
 E
    A
       B
    C   
 G
     F
     H

Le résultat final que je veux est un ensemble imbriqué de <ul> éléments, avec chaque <li> contenant le nom de l'enfant.

Il n'y a pas d'incohérence dans les appariements (l'enfant est son propre parent, le parent est l'enfant de l'enfant, etc), donc un tas d'optimisations peuvent probablement être faites.

Comment, en PHP, puis-je passer d'un tableau contenant des paires enfant=>parent à un ensemble d'éléments imbriqués ? <ul> s ?

J'ai le sentiment que la récursion est impliquée, mais je ne suis pas assez éveillé pour y réfléchir.

134voto

Tatu Ulmanen Points 52098

Cela nécessite une fonction récursive très basique pour analyser les paires enfant/parent en une structure arborescente et une autre fonction récursive pour l'imprimer. Une seule fonction suffirait, mais en voici deux pour plus de clarté (une fonction combinée se trouve à la fin de cette réponse).

Commencez par initialiser le tableau des paires enfant/parent :

$tree = array(
    'H' => 'G',
    'F' => 'G',
    'G' => 'D',
    'E' => 'D',
    'A' => 'E',
    'B' => 'C',
    'C' => 'E',
    'D' => null
);

Puis la fonction qui analyse ce tableau en une structure arborescente hiérarchique :

function parseTree($tree, $root = null) {
    $return = array();
    # Traverse the tree and search for direct children of the root
    foreach($tree as $child => $parent) {
        # A direct child is found
        if($parent == $root) {
            # Remove item from tree (we don't need to traverse this again)
            unset($tree[$child]);
            # Append the child into result array and parse its children
            $return[] = array(
                'name' => $child,
                'children' => parseTree($tree, $child)
            );
        }
    }
    return empty($return) ? null : $return;    
}

Et une fonction qui traverse cet arbre pour imprimer une liste non ordonnée :

function printTree($tree) {
    if(!is_null($tree) && count($tree) > 0) {
        echo '<ul>';
        foreach($tree as $node) {
            echo '<li>'.$node['name'];
            printTree($node['children']);
            echo '</li>';
        }
        echo '</ul>';
    }
}

Et l'utilisation réelle :

$result = parseTree($tree);
printTree($result);

Voici le contenu de $result :

Array(
    [0] => Array(
        [name] => D
        [children] => Array(
            [0] => Array(
                [name] => G
                [children] => Array(
                    [0] => Array(
                        [name] => H
                        [children] => NULL
                    )
                    [1] => Array(
                        [name] => F
                        [children] => NULL
                    )
                )
            )
            [1] => Array(
                [name] => E
                [children] => Array(
                    [0] => Array(
                        [name] => A
                        [children] => NULL
                    )
                    [1] => Array(
                        [name] => C
                        [children] => Array(
                            [0] => Array(
                                [name] => B
                                [children] => NULL
                            )
                        )
                    )
                )
            )
        )
    )
)

Si vous voulez un peu plus d'efficacité, vous pouvez combiner ces fonctions en une seule et réduire le nombre d'itérations effectuées :

function parseAndPrintTree($root, $tree) {
    $return = array();
    if(!is_null($tree) && count($tree) > 0) {
        echo '<ul>';
        foreach($tree as $child => $parent) {
            if($parent == $root) {                    
                unset($tree[$child]);
                echo '<li>'.$child;
                parseAndPrintTree($child, $tree);
                echo '</li>';
            }
        }
        echo '</ul>';
    }
}

Vous n'économiserez que 8 itérations sur un ensemble de données aussi petit que celui-ci, mais sur des ensembles plus importants, cela pourrait faire la différence.

2 votes

Tatu. Comment puis-je modifier la fonction printTree pour qu'elle ne renvoie pas directement le code html de l'arbre, mais qu'elle enregistre le code html de sortie dans une variable et le renvoie ?

0 votes

Bonjour, je pense que la déclaration de la fonction doit être parseAndPrintTree($tree, $Root = null) et l'appel récursif doit être parseAndPrintTree($child, $tree) ; Meilleures salutations

60voto

Encore une autre fonction pour faire un arbre (sans récursion, mais avec des références) :

$array = array('H' => 'G', 'F' => 'G', ..., 'D' => null);

function to_tree($array)
{
    $flat = array();
    $tree = array();

    foreach ($array as $child => $parent) {
        if (!isset($flat[$child])) {
            $flat[$child] = array();
        }
        if (!empty($parent)) {
            $flat[$parent][$child] =& $flat[$child];
        } else {
            $tree[$child] =& $flat[$child];
        }
    }

    return $tree;
}

Renvoie un tableau hiérarchique comme celui-ci :

Array(
    [D] => Array(
        [G] => Array(
            [H] => Array()
            [F] => Array()
        )
        ...
    )
)

Qui peut facilement être imprimé comme une liste HTML en utilisant la fonction récursive.

0 votes

+1 - Très astucieux. Bien que je trouve la solution récursive plus logique. Mais je préfère le format de sortie de votre fonction.

0 votes

@Eric plus logique ? Je ne suis pas d'accord. Il n'y a rien de "logique" dans la récursion ; il y a en revanche une sévère surcharge cognitive sur l'analyse des fonctions/appels récursifs. S'il n'y a pas d'allocation explicite de la pile, je prendrais l'itération sur la récursion tous les jours.

29voto

hakre Points 102271

Une autre façon, plus simplifiée, de convertir la structure plate dans le fichier $tree dans une hiérarchie. Un seul tableau temporaire est nécessaire pour l'exposer :

// add children to parents
$flat = array(); # temporary array
foreach ($tree as $name => $parent)
{
    $flat[$name]['name'] = $name; # self
    if (NULL === $parent)
    {
        # no parent, is root element, assign it to $tree
        $tree = &$flat[$name]; 
    }
    else
    {
        # has parent, add self as child    
        $flat[$parent]['children'][] = &$flat[$name];
    }
}
unset($flat);

C'est tout pour obtenir la hiérarchie dans un tableau multidimensionnel :

Array
(
    [children] => Array
        (
            [0] => Array
                (
                    [children] => Array
                        (
                            [0] => Array
                                (
                                    [name] => H
                                )

                            [1] => Array
                                (
                                    [name] => F
                                )

                        )

                    [name] => G
                )

            [1] => Array
                (
                    [name] => E
                    [children] => Array
                        (
                            [0] => Array
                                (
                                    [name] => A
                                )

                            [1] => Array
                                (
                                    [children] => Array
                                        (
                                            [0] => Array
                                                (
                                                    [name] => B
                                                )

                                        )

                                    [name] => C
                                )

                        )

                )

        )

    [name] => D
)

La sortie est moins triviale si vous voulez éviter la récursion (peut être un fardeau avec de grandes structures).

J'ai toujours voulu résoudre le "dilemme" UL/LI pour la sortie d'un tableau. Le dilemme est que chaque élément ne sait pas si des enfants suivront ou non ou combien d'éléments précédents doivent être fermés. Dans une autre réponse, j'ai déjà résolu ce problème en utilisant une fonction RecursiveIteratorIterator et à la recherche de getDepth() et d'autres méta-informations que mes propres écrits Iterator fourni : Obtenir un modèle d'ensemble imbriqué dans un <ul> mais en cachant les sous-arbres "fermés". . Ce réponse montre aussi qu'avec les itérateurs, on est assez souple.

Cependant, il s'agissait d'une liste pré-triée, qui ne conviendrait pas à votre exemple. De plus, j'ai toujours voulu résoudre ce problème pour une sorte de structure arborescente standard et pour le langage HTML. <ul> y <li> éléments.

Le concept de base auquel j'ai abouti est le suivant :

  1. TreeNode - Abstraite chaque élément dans un simple TreeNode qui peut fournir sa valeur (par ex. Name ) et s'il a ou non des enfants.
  2. TreeNodesIterator - A RecursiveIterator qui est capable d'itérer sur un ensemble (tableau) de ces éléments. TreeNodes . C'est assez simple puisque le TreeNode sait déjà s'il a des enfants et lesquels.
  3. RecursiveListIterator - A RecursiveIteratorIterator qui a tous les événements nécessaires lorsqu'il itère récursivement sur n'importe quel type de RecursiveIterator :
    • beginIteration / endIteration - Début et fin de la liste principale.
    • beginElement / endElement - Début et fin de chaque élément.
    • beginChildren / endChildren - Début et fin de chaque liste d'enfants. Ce site RecursiveListIterator ne fournit ces événements que sous la forme d'appels de fonction. les listes d'enfants, comme il est typique pour les <ul><li> sont ouverts et fermés à l'intérieur de son parent. <li> élément. Par conséquent, le endElement est déclenché après que l'événement endChildren événement. Cela pourrait être modifié ou rendu configurable pour élargir l'utilisation de cette classe. Les événements sont distribués comme des appels de fonction à un objet décorateur alors, pour garder les choses séparées.
  4. ListDecorator - Une classe "décorateur" qui est juste un récepteur des événements de RecursiveListIterator .

Je commence par la logique de sortie principale. Prenant le maintenant hiérarchique $tree le code final ressemble à ce qui suit :

$root = new TreeNode($tree);
$it = new TreeNodesIterator(array($root));
$rit = new RecursiveListIterator($it);
$decor = new ListDecorator($rit);
$rit->addDecorator($decor);

foreach($rit as $item)
{
    $inset = $decor->inset(1);
    printf("%s%s\n", $inset, $item->getName());
}

Examinons d'abord le ListDecorator qui ne fait qu'envelopper le <ul> y <li> et décide de la manière dont la structure de la liste est éditée :

class ListDecorator
{
    private $iterator;
    public function __construct(RecursiveListIterator $iterator)
    {
        $this->iterator = $iterator;
    }
    public function inset($add = 0)
    {
        return str_repeat('  ', $this->iterator->getDepth()*2+$add);
    }

Le constructeur prend l'itérateur de liste sur lequel il travaille. inset est juste une fonction d'aide pour une bonne indentation de la sortie. Les autres sont juste les fonctions de sortie pour chaque événement :

    public function beginElement()
    {
        printf("%s<li>\n", $this->inset());
    }
    public function endElement()
    {
        printf("%s</li>\n", $this->inset());
    }
    public function beginChildren()
    {
        printf("%s<ul>\n", $this->inset(-1));
    }
    public function endChildren()
    {
        printf("%s</ul>\n", $this->inset(-1));
    }
    public function beginIteration()
    {
        printf("%s<ul>\n", $this->inset());
    }
    public function endIteration()
    {
        printf("%s</ul>\n", $this->inset());
    }
}

Avec ces fonctions de sortie à l'esprit, voici à nouveau la boucle principale de sortie, que je parcours étape par étape :

$root = new TreeNode($tree);

Créer la racine TreeNode qui sera utilisé pour commencer l'itération :

$it = new TreeNodesIterator(array($root));

Ce site TreeNodesIterator est un RecursiveIterator qui permet une itération récursive sur l'unique $root nœud. Il est passé sous la forme d'un tableau parce que cette classe a besoin de quelque chose sur lequel itérer et permet la réutilisation avec un ensemble d'enfants qui est également un tableau de TreeNode éléments.

$rit = new RecursiveListIterator($it);

Ce site RecursiveListIterator est un RecursiveIteratorIterator qui fournit les dits événements. Pour l'utiliser, il suffit d'un ListDecorator doit être fournie (la classe ci-dessus) et affectée à l'aide de addDecorator :

$decor = new ListDecorator($rit);
$rit->addDecorator($decor);

Ensuite, tout est mis en place pour juste foreach sur elle et sortir chaque nœud :

foreach($rit as $item)
{
    $inset = $decor->inset(1);
    printf("%s%s\n", $inset, $item->getName());
}

Comme le montre cet exemple, toute la logique de sortie est encapsulée dans l'élément ListDecorator et cette unique foreach . L'ensemble de la traversée récursive a été entièrement encapsulée dans des itérateurs récursifs SPL qui fournissent une procédure empilée, ce qui signifie qu'en interne, aucun appel de fonction récursive n'est effectué.

L'événement basé sur ListDecorator vous permet de modifier la sortie de manière spécifique et de fournir plusieurs types de listes pour la même structure de données. Il est même possible de modifier l'entrée, car les données du tableau ont été encapsulées dans des fichiers TreeNode .

L'exemple de code complet :

<?php
namespace My;

$tree = array('H' => 'G', 'F' => 'G', 'G' => 'D', 'E' => 'D', 'A' => 'E', 'B' => 'C', 'C' => 'E', 'D' => null);

// add children to parents
$flat = array(); # temporary array
foreach ($tree as $name => $parent)
{
    $flat[$name]['name'] = $name; # self
    if (NULL === $parent)
    {
        # no parent, is root element, assign it to $tree
        $tree = &$flat[$name];
    }
    else
    {
        # has parent, add self as child    
        $flat[$parent]['children'][] = &$flat[$name];
    }
}
unset($flat);

class TreeNode
{
    protected $data;
    public function __construct(array $element)
    {
        if (!isset($element['name']))
            throw new InvalidArgumentException('Element has no name.');

        if (isset($element['children']) && !is_array($element['children']))
            throw new InvalidArgumentException('Element has invalid children.');

        $this->data = $element;
    }
    public function getName()
    {
         return $this->data['name'];
    }
    public function hasChildren()
    {
        return isset($this->data['children']) && count($this->data['children']);
    }
    /**
     * @return array of child TreeNode elements 
     */
    public function getChildren()
    {        
        $children = $this->hasChildren() ? $this->data['children'] : array();
        $class = get_called_class();
        foreach($children as &$element)
        {
            $element = new $class($element);
        }
        unset($element);        
        return $children;
    }
}

class TreeNodesIterator implements \RecursiveIterator
{
    private $nodes;
    public function __construct(array $nodes)
    {
        $this->nodes = new \ArrayIterator($nodes);
    }
    public function  getInnerIterator()
    {
        return $this->nodes;
    }
    public function getChildren()
    {
        return new TreeNodesIterator($this->nodes->current()->getChildren());
    }
    public function hasChildren()
    {
        return $this->nodes->current()->hasChildren();
    }
    public function rewind()
    {
        $this->nodes->rewind();
    }
    public function valid()
    {
        return $this->nodes->valid();
    }   
    public function current()
    {
        return $this->nodes->current();
    }
    public function key()
    {
        return $this->nodes->key();
    }
    public function next()
    {
        return $this->nodes->next();
    }
}

class RecursiveListIterator extends \RecursiveIteratorIterator
{
    private $elements;
    /**
     * @var ListDecorator
     */
    private $decorator;
    public function addDecorator(ListDecorator $decorator)
    {
        $this->decorator = $decorator;
    }
    public function __construct($iterator, $mode = \RecursiveIteratorIterator::SELF_FIRST, $flags = 0)
    {
        parent::__construct($iterator, $mode, $flags);
    }
    private function event($name)
    {
        // event debug code: printf("--- %'.-20s --- (Depth: %d, Element: %d)\n", $name, $this->getDepth(), @$this->elements[$this->getDepth()]);
        $callback = array($this->decorator, $name);
        is_callable($callback) && call_user_func($callback);
    }
    public function beginElement()
    {
        $this->event('beginElement');
    }
    public function beginChildren()
    {
        $this->event('beginChildren');
    }
    public function endChildren()
    {
        $this->testEndElement();
        $this->event('endChildren');
    }
    private function testEndElement($depthOffset = 0)
    {
        $depth = $this->getDepth() + $depthOffset;      
        isset($this->elements[$depth]) || $this->elements[$depth] = 0;
        $this->elements[$depth] && $this->event('endElement');

    }
    public function nextElement()
    {
        $this->testEndElement();
        $this->event('{nextElement}');
        $this->event('beginElement');       
        $this->elements[$this->getDepth()] = 1;
    } 
    public function beginIteration()
    {
        $this->event('beginIteration');
    }
    public function endIteration()
    {
        $this->testEndElement();
        $this->event('endIteration');       
    }
}

class ListDecorator
{
    private $iterator;
    public function __construct(RecursiveListIterator $iterator)
    {
        $this->iterator = $iterator;
    }
    public function inset($add = 0)
    {
        return str_repeat('  ', $this->iterator->getDepth()*2+$add);
    }
    public function beginElement()
    {
        printf("%s<li>\n", $this->inset(1));
    }
    public function endElement()
    {
        printf("%s</li>\n", $this->inset(1));
    }
    public function beginChildren()
    {
        printf("%s<ul>\n", $this->inset());
    }
    public function endChildren()
    {
        printf("%s</ul>\n", $this->inset());
    }
    public function beginIteration()
    {
        printf("%s<ul>\n", $this->inset());
    }
    public function endIteration()
    {
        printf("%s</ul>\n", $this->inset());
    }
}

$root = new TreeNode($tree);
$it = new TreeNodesIterator(array($root));
$rit = new RecursiveListIterator($it);
$decor = new ListDecorator($rit);
$rit->addDecorator($decor);

foreach($rit as $item)
{
    $inset = $decor->inset(2);
    printf("%s%s\n", $inset, $item->getName());
}

Outpupt :

<ul>
  <li>
    D
    <ul>
      <li>
        G
        <ul>
          <li>
            H
          </li>
          <li>
            F
          </li>
        </ul>
      </li>
      <li>
        E
        <ul>
          </li>
          <li>
            A
          </li>
          <li>
            C
            <ul>
              <li>
                B
              </li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

Démonstration (variante PHP 5.2)

Une variante possible serait un itérateur qui itère sur n'importe quel RecursiveIterator et fournit une itération sur tous les événements qui peuvent se produire. Un switch / case à l'intérieur de la boucle foreach pourrait alors traiter les événements.

En rapport :

3 votes

Aussi "bien conçue" que soit cette solution, comment se fait-il qu'elle soit "plus simplifiée" que les exemples précédents ? Cela ressemble simplement à une solution trop élaborée pour le même problème.

0 votes

@Andre : Par le grade d'encapsulation IIRC. Dans une autre réponse connexe, j'ai un fragment de code entièrement non encapsulé qui est beaucoup plus petit et pourrait donc être "plus simplifié" selon le point de vue.

0 votes

@hakre Comment puis-je modifier la classe "ListDecorator" pour ajouter "id" au LI, qui est récupéré à partir du tableau de l'arbre ?

8voto

ircmaxell Points 74865

Tout d'abord, je transformerais le tableau de paires clé-valeur en un tableau hiérarchique.

function convertToHeiarchical(array $input) {
    $parents = array();
    $root = array();
    $children = array();
    foreach ($input as $item) {
        $parents[$item['id']] = &$item;
        if ($item['parent_id']) {
            if (!isset($children[$item['parent_id']])) {
                $children[$item['parent_id']] = array();
            }
            $children[$item['parent_id']][] = &$item;
        } else {
            $root = $item['id'];
        }
    }
    foreach ($parents as $id => &$item) {
        if (isset($children[$id])) {
            $item['children'] = $children[$id];
        } else {
            $item['children'] = array();
        }
    }
    return $parents[$root];
}

Cela permet de convertir un tableau plat avec parent_id et id en un tableau hiérarchique :

$item = array(
    'id' => 'A',
    'blah' => 'blah',
    'children' => array(
        array(
            'id' => 'B',
            'blah' => 'blah',
            'children' => array(
                array(
                    'id' => 'C',
                    'blah' => 'blah',
                    'children' => array(),
                ),
             ),
            'id' => 'D',
            'blah' => 'blah',
            'children' => array(
                array(
                    'id' => 'E',
                    'blah' => 'blah',
                    'children' => array(),
                ),
            ),
        ),
    ),
);

Ensuite, il suffit de créer une fonction de rendu :

function renderItem($item) {
    $out = "Your OUtput For Each Item Here";
    $out .= "<ul>";
    foreach ($item['children'] as $child) {
        $out .= "<li>".renderItem($child)."</li>";
    }
    $out .= "</ul>";
    return $out;
}

2voto

Dan Heberden Points 6697

Voici ce que j'ai trouvé :

$arr = array(
            'H' => 'G',
            'F' => 'G',
            'G' => 'D',
            'E' => 'D',
            'A' => 'E',
            'B' => 'C',
            'C' => 'E',
            'D' => null );

    $nested = parentChild($arr);
    print_r($nested);

    function parentChild(&$arr, $parent = false) {
      if( !$parent) { //initial call
         $rootKey = array_search( null, $arr);
         return array($rootKey => parentChild($arr, $rootKey));
      }else { // recursing through
        $keys = array_keys($arr, $parent);
        $piece = array();
        if($keys) { // found children, so handle them
          if( !is_array($keys) ) { // only one child
            $piece = parentChild($arr, $keys);
           }else{ // multiple children
             foreach( $keys as $key ){
               $piece[$key] = parentChild($arr, $key);
             }
           }
        }else {
           return $parent; //return the main tag (no kids)
        }
        return $piece; // return the array built via recursion
      }
    }

sorties :

Array
(
    [D] => Array
        (
            [G] => Array
                (
                    [H] => H
                    [F] => F
                )

            [E] => Array
                (
                    [A] => A
                    [C] => Array
                        (
                            [B] => B
                        )    
                )    
        )    
)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X