Преобразовать ряд родительско-дочерних отношений в иерархическое дерево?
у меня есть куча пар имя-parentname, которые я хотел бы превратить в как можно меньше наследственных древовидных структур. Так, например, это могут быть пары:
Child : Parent
H : G
F : G
G : D
E : D
A : E
B : C
C : E
D : NULL
, который должен быть преобразован в (а) дерево heirarchical(ы):
D
├── E
│ ├── A
│ │ └── B
│ └── C
└── G
├── F
└── H
конечный результат, который я хочу, является вложенным набором <ul>
элементы, с каждого <li>
содержит имя ребенка.
нет никаких несоответствий в парах (ребенок его собственный родитель, родитель это ребенок ребенка и т. д.), Поэтому, вероятно, можно сделать кучу оптимизаций.
как в PHP я мог бы перейти от массива, содержащего дочерние = > родительские пары, к набору вложенных <ul>
s?
у меня есть ощущение, что рекурсия участвует, но я не совсем проснулся, чтобы думать об этом.
8 ответов:
для этого требуется очень простая рекурсивная функция для разбора дочерних / родительских пар в древовидную структуру и другая рекурсивная функция для ее печати. Достаточно только одной функции, но вот две для ясности (комбинированная функция может быть найдена в конце этого ответа).
сначала инициализируйте массив дочерних / родительских пар:
$tree = array( 'H' => 'G', 'F' => 'G', 'G' => 'D', 'E' => 'D', 'A' => 'E', 'B' => 'C', 'C' => 'E', 'D' => null );
затем функция, которая анализирует этот массив в иерархическую древовидную структуру:
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; }
и функция, которая пересекает это дерево, чтобы распечатать неупорядоченный список:
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>'; } }
и фактическое использование:
$result = parseTree($tree); printTree($result);
вот содержание
$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 ) ) ) ) ) ) ) )
если вы хотите немного больше эффективности, вы можете объединить эти функции в одну и уменьшить количество итераций:
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>'; } }
вы сохраните только 8 итераций на таком маленьком наборе данных, но на больших наборах это может иметь значение.
еще одна функция для создания дерева (без рекурсии, использует ссылки вместо этого):
$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; }
возвращает иерархический массив вроде этого:
Array( [D] => Array( [G] => Array( [H] => Array() [F] => Array() ) ... ) )
который может быть легко напечатан в виде списка HTML с помощью рекурсивной функции.
еще один, более упрощенный способ преобразования плоской структуры в
$tree
в иерархию. Только один временный массив необходим, чтобы выставить его:// 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);
вот и все для получения иерархии в многомерном массиве:
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 )
выход менее тривиален, если вы хотите избежать рекурсии (может быть нагрузка с большими структурами).
я всегда хотел решить "дилемму" UL/LI для вывода массива. Дилемма заключается в том, что каждый элемент не знает, будут ли дети следовать или сколько предыдущих элементов необходимо закрыть. В другом ответе я уже решил, что с помощью
RecursiveIteratorIterator
и ищемgetDepth()
и другая метаинформация, которую я сам написалIterator
указано: получение вложенной модели набора в<ul>
но скрывая" закрытые " поддеревья. Это ответ показывает также, что с итераторами вы довольно гибки.однако это был предварительно отсортированный список, так что не подходит для вашего примера. Кроме того, я всегда хотел решить эту проблему для своего рода стандартной древовидной структуры и HTML
<ul>
и<li>
элементы.основная концепция, которую я придумал, заключается в следующем:
TreeNode
- абстрагирует каждый элемент в простойTreeNode
тип, который может предоставить это значение (например,Name
) и есть ли у него дети.TreeNodesIterator
- ARecursiveIterator
это возможность итерации по набору (массиву) этихTreeNodes
. Это довольно просто, какTreeNode
тип уже знает, есть ли у него дети и какие из них.RecursiveListIterator
- ARecursiveIteratorIterator
это имеет все события, необходимые, когда он рекурсивно перебирать любой видRecursiveIterator
:
beginIteration
/endIteration
- начало и конец основного списка.beginElement
/endElement
- начало и конец каждого элемента.beginChildren
/endChildren
- начать и конец каждого списка детей. ЭтоRecursiveListIterator
предоставляет эти события только в виде вызовов функций. списки детей, как это характерно для<ul><li>
списки, открываются и закрываются внутри его родителя<li>
элемент. Таким образом,endElement
событие запускается после соответствующегоendChildren
событие. Это может быть изменено или сделано настраиваемым для расширения использования этого класса. Затем события распределяются как вызовы функций к объекту декоратора, чтобы сохранить вещи отдельно.ListDecorator
- класс "декоратор", который является всего лишь приемником событийRecursiveListIterator
.я начинаю с основной логики вывода. Взят теперь иерархический
$tree
массив, окончательный код выглядит следующим образом:$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()); }
сначала давайте заглянем в
ListDecorator
это просто обертывает<ul>
и<li>
элементы и решает о том, как структура списка вывод: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); }
конструктор принимает итератор списка, над которым он работает.
inset
это просто вспомогательная функция для хорошего отступа на выходе. Остальные-это просто выходные функции для каждого события: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()); } }
имея в виду эти выходные функции, это основной вывод wrap-up / loop снова, я иду через него шаг за шагом:
$root = new TreeNode($tree);
создать корневой элемент
TreeNode
который будет использоваться для запуска итерации после:$it = new TreeNodesIterator(array($root));
этот
TreeNodesIterator
этоRecursiveIterator
что позволяет рекурсивную итерацию над одним$root
узел. Он передается как массив, потому что этот класс нуждается в чем-то для итерации и позволяет повторно использовать с набором дочерних элементов, который также является массивомTreeNode
элементы.$rit = new RecursiveListIterator($it);
этой
RecursiveListIterator
этоRecursiveIteratorIterator
что обеспечивает указанные события. Чтобы использовать его, толькоListDecorator
должен быть предоставлен (класс выше) и назначен сaddDecorator
:$decor = new ListDecorator($rit); $rit->addDecorator($decor);
тогда все настройка только на
foreach
над ним и вывести каждый узел:foreach($rit as $item) { $inset = $decor->inset(1); printf("%s%s\n", $inset, $item->getName()); }
как показывает этот пример, вся логика вывода инкапсулируется в
ListDecorator
класс и этот одинforeach
. Весь рекурсивный обход был полностью инкапсулирован в рекурсивные итераторы SPL, которые обеспечивали сложенную процедуру, что означает, что внутри не выполняются вызовы рекурсивных функций.событие на основе
ListDecorator
позволяет вам доработать выход специфически и обеспечить множественный тип списков для одной и той же структуры данных. Можно даже изменить входные данные, поскольку данные массива были инкапсулированы вTreeNode
.полный пример кода:
<?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>
возможным вариантом будет итератор, который повторяет любой
RecursiveIterator
и обеспечивает итерацию по всем событиям, которые могут произойти. Коммутатор / корпус внутри цикла foreach может иметь дело с событие.по теме:
ну, сначала я бы превратил прямой массив пар ключ-значение в иерархический массив
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]; }
Это может преобразовать плоский массив с parent_id и id в иерархический:
$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(), ), ), ), ), );
затем просто создайте функцию рендеринга:
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; }
пока Александр-Константиноварешение может показаться не так легко читать на первый взгляд, это и гений и экспоненциально лучше с точки зрения производительности, это должно было быть проголосовано как лучший ответ.
спасибо приятель, я сделал тест в вашу честь, чтобы сравнить 2 решения, представленные в этом посте.
у меня было плоское дерево @250k с 6 уровнями, которые я должен был преобразовать, и я искал лучший способ сделать это и избежать рекурсивного повторения.
рекурсия против ссылка:
// Generate a 6 level flat tree $root = null; $lvl1 = 13; $lvl2 = 11; $lvl3 = 7; $lvl4 = 5; $lvl5 = 3; $lvl6 = 1; $flatTree = []; for ($i = 1; $i <= 450000; $i++) { if ($i % 3 == 0) { $lvl5 = $i; $flatTree[$lvl6] = $lvl5; continue; } if ($i % 5 == 0) { $lvl4 = $i; $flatTree[$lvl5] = $lvl4; continue; } if ($i % 7 == 0) { $lvl3 = $i; $flatTree[$lvl3] = $lvl2; continue; } if ($i % 11 == 0) { $lvl2 = $i; $flatTree[$lvl2] = $lvl1; continue; } if ($i % 13 == 0) { $lvl1 = $i; $flatTree[$lvl1] = $root; continue; } $lvl6 = $i; } echo 'Array count: ', count($flatTree), PHP_EOL; // Reference function function treeByReference($flatTree) { $flat = []; $tree = []; foreach ($flatTree as $child => $parent) { if (!isset($flat[$child])) { $flat[$child] = []; } if (!empty($parent)) { $flat[$parent][$child] =& $flat[$child]; } else { $tree[$child] =& $flat[$child]; } } return $tree; } // Recursion function function treeByRecursion($flatTree, $root = null) { $return = []; foreach($flatTree as $child => $parent) { if ($parent == $root) { unset($flatTree[$child]); $return[$child] = treeByRecursion($flatTree, $child); } } return $return ?: []; } // Benchmark reference $t1 = microtime(true); $tree = treeByReference($flatTree); echo 'Reference: ', (microtime(true) - $t1), PHP_EOL; // Benchmark recursion $t2 = microtime(true); $tree = treeByRecursion($flatTree); echo 'Recursion: ', (microtime(true) - $t2), PHP_EOL;
вывод говорит сам за себя:
Array count: 255493 Reference: 0.3259289264679 (less than 0.4s) Recursion: 6604.9865279198 (almost 2h)
ну, чтобы разобрать на ULs и LIs, это было бы что-то вроде:
$array = array ( 'H' => 'G' 'F' => 'G' 'G' => 'D' 'E' => 'D' 'A' => 'E' 'B' => 'C' 'C' => 'E' 'D' => 'NULL' ); recurse_uls ($array, 'NULL'); function recurse_uls ($array, $parent) { echo '<ul>'; foreach ($array as $c => $p) { if ($p != $parent) continue; echo '<li>'.$c.'</li>'; recurse_uls ($array, $c); } echo '</ul>'; }
но я хотел бы видеть решение, которое не требует от вас, чтобы перебрать массив так часто...
вот что я придумал:
$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 } }
выходы:
Array ( [D] => Array ( [G] => Array ( [H] => H [F] => F ) [E] => Array ( [A] => A [C] => Array ( [B] => B ) ) ) )
как создать динамическое дерево и меню
Шаг 1: Сначала мы создадим таблицу treeview в базе данных mysql. эта таблица содержит четыре column.id это идентификатор задачи, а имя-имя задачи.
- -- Table structure for table `treeview_items` -- CREATE TABLE IF NOT EXISTS `treeview_items` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(200) NOT NULL, `title` varchar(200) NOT NULL, `parent_id` varchar(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=7 ; -- -- Dumping data for table `treeview_items` -- INSERT INTO `treeview_items` (`id`, `name`, `title`, `parent_id`) VALUES (1, 'task1', 'task1title', '2'), (2, 'task2', 'task2title', '0'), (3, 'task3', 'task1title3', '0'), (4, 'task4', 'task2title4', '3'), (5, 'task4', 'task1title4', '3'), (6, 'task5', 'task2title5', '5');
Шаг 2: древовидный рекурсивный метод Я создал ниже дерева createTreeView () метод, который вызывает рекурсивный, если текущий идентификатор задачи больше, чем идентификатор задачи prev.
function createTreeView($array, $currentParent, $currLevel = 0, $prevLevel = -1) { foreach ($array as $categoryId => $category) { if ($currentParent == $category['parent_id']) { if ($currLevel > $prevLevel) echo " <ol class='tree'> "; if ($currLevel == $prevLevel) echo " </li> "; echo '<li> <label for="subfolder2">'.$category['name'].'</label> <input type="checkbox" name="subfolder2"/>'; if ($currLevel > $prevLevel) { $prevLevel = $currLevel; } $currLevel++; createTreeView ($array, $categoryId, $currLevel, $prevLevel); $currLevel--; } } if ($currLevel == $prevLevel) echo " </li> </ol> "; }
Шаг 3: Создайте индексный файл, чтобы показать вид дерева. Это основной файл пример treeview здесь мы будем вызывать метод createTreeView () с необходимыми параметрами.
<body> <link rel="stylesheet" type="text/css" href="_styles.css" media="screen"> <?php mysql_connect('localhost', 'root'); mysql_select_db('test'); $qry="SELECT * FROM treeview_items"; $result=mysql_query($qry); $arrayCategories = array(); while($row = mysql_fetch_assoc($result)){ $arrayCategories[$row['id']] = array("parent_id" => $row['parent_id'], "name" => $row['name']); } ?> <div id="content" class="general-style1"> <?php if(mysql_num_rows($result)!=0) { ?> <?php createTreeView($arrayCategories, 0); ?> <?php } ?> </div> </body>
Шаг 4: Создайте стиль файла CSS.стиль CSS Здесь мы напишем весь класс, связанный с css, в настоящее время я использую список заказов для создания представления дерева. вы также можете изменить путь изображения.
img { border: none; } input, select, textarea, th, td { font-size: 1em; } /* CSS Tree menu styles */ ol.tree { padding: 0 0 0 30px; width: 300px; } li { position: relative; margin-left: -15px; list-style: none; } li.file { margin-left: -1px !important; } li.file a { background: url(document.png) 0 0 no-repeat; color: #fff; padding-left: 21px; text-decoration: none; display: block; } li.file a[href *= '.pdf'] { background: url(document.png) 0 0 no-repeat; } li.file a[href *= '.html'] { background: url(document.png) 0 0 no-repeat; } li.file a[href $= '.css'] { background: url(document.png) 0 0 no-repeat; } li.file a[href $= '.js'] { background: url(document.png) 0 0 no-repeat; } li input { position: absolute; left: 0; margin-left: 0; opacity: 0; z-index: 2; cursor: pointer; height: 1em; width: 1em; top: 0; } li input + ol { background: url(toggle-small-expand.png) 40px 0 no-repeat; margin: -0.938em 0 0 -44px; /* 15px */ height: 1em; } li input + ol > li { display: none; margin-left: -14px !important; padding-left: 1px; } li label { background: url(folder-horizontal.png) 15px 1px no-repeat; cursor: pointer; display: block; padding-left: 37px; } li input:checked + ol { background: url(toggle-small.png) 40px 5px no-repeat; margin: -1.25em 0 0 -44px; /* 20px */ padding: 1.563em 0 0 80px; height: auto; } li input:checked + ol > li { display: block; margin: 0 0 0.125em; /* 2px */} li input:checked + ol > li:last-child { margin: 0 0 0.063em; /* 1px */ }