Сегодня я расскажу, как на и MySQL создавать иерархическое дерево.

Такие деревья используются при построении категорий динамического сайта, например в интернет-магазине или при выводе комментариев к посту.

Вообще они строятся где только возможно. Главное правильно его построить и применить.

Самое главное, когда строишь иерархическое дерево — это правильная структура базы данных! Для примера рассмотрим структуру базы данных, где хранятся категории сайта. Для простого примера, таблица будет иметь 3 поля:

  1. id — ключ категории
  2. parent_id — id родительской категории
  3. name – название раздела

Создадим таблицу, выполнив SQL-запрос в PHPMyAdmin:

CREATE TABLE `categories` (`id` INT NOT NULL AUTO_INCREMENT , `parent_id` INT NOT NULL , `name` VARCHAR(50) NOT NULL , PRIMARY KEY (`id`));

Теперь нужно заполнить нашу таблицу записями. В результате, должна получится примерно такая таблица:

Можно заполнить тестовую таблицу запросом:

INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (1, 0, "Раздел 1"), (2, 0, "Раздел 2"), (3, 0, "Раздел 3"), (4, 1, "Раздел 1.1"), (5, 1, "Раздел 1.2"), (6, 4, "Раздел 1.1.1"), (7, 2, "Раздел 2.1"), (8, 2, "Раздел 2.2"), (9, 3, "Раздел 3.1");

И сейчас внимание! Дальше по логике нужно делать выборки из БД в цикле для выбора каждой категории и её подкатегории. НО! Ладно, если в БД несколько категорий, что тоже в принципе не правильно. А если сайт — интернет-магазин и у него сотня категорий и столько же подкатегорий? Тогда беда! Неведомое количество запросов к базе данных приведет к замедлению работы сайта или же к полному краху mysql-сервера.

Можно используя только один запрос к БД выбрать все категории и ихние подкатегории.

Сделаем запрос и сформируем удобный массив для дальнейшей работы.

//Выбираем данные из БД $result=mysql_query("SELECT * FROM categories"); //Если в базе данных есть записи, формируем массив if (mysql_num_rows($result) > 0){ $cats = array(); //В цикле формируем массив разделов, ключом будет id родительской категории, а также массив разделов, ключом будет id категории while($cat = mysql_fetch_assoc($result)){ $cats_ID[$cat["id"]] = $cat; $cats[$cat["parent_id"]][$cat["id"]] = $cat; } }

Выбираем все данные из таблицы categories и формируем ассоциативный массив $cats , ключем будет id родительской категорий.

Сейчас будем строить дерево. Для построения будем использовать рекурсивную функцию .

Иерархическое дерево будет иметь такую структуру:

  • Раздел 1
    • Раздел 1.1
      • Раздел 1.1.1
    • Раздел 1.2
  • Раздел 2
    • Раздел 1.1
    • Раздел 1.2
  • Раздел 3
    • Раздел 3.1

Создадим рекурсивную функцию build_tree() . Она будет строить наше иерархическое дерево абсолютно любой вложенности.

Function build_tree($cats,$parent_id,$only_parent = false){ if(is_array($cats) and isset($cats[$parent_id])){ $tree = "

    "; if($only_parent==false){ foreach($cats[$parent_id] as $cat){ $tree .= ""; } }elseif(is_numeric($only_parent)){ $cat = $cats[$parent_id][$only_parent]; $tree .= "
  • ".$cat["name"]." #".$cat["id"]; $tree .= build_tree($cats,$cat["id"]); $tree .= "
  • "; } $tree .= "
"; } else return null; return $tree; }

Функция принимает массив разделов и id раздела. В цикле перебираем подкатегории и если в них есть еще разделы, тогда функция запускается еще раз с новыми параметрами (новый массив разделов и id раздела, который нужно построить). Так формируется дерево любой вложенности!

Для построения дерева, в коде прописываем:

Echo build_tree($cats,0);

Так вот в два шага мы создали иерархическое дерево разделов сайта и не важно сколько там разделов!

UPD Если нужно дерево категорий в обратном порядке зная id категории, тогда нужно воспользоваться функцией:

Function find_parent ($tmp, $cur_id){ if($tmp[$cur_id]["parent_id"]!=0){ return find_parent($tmp,$tmp[$cur_id]["parent_id"]); } return (int)$tmp[$cur_id]["id"]; }

Данная функция принимает массив категорий, ключом которой есть id рубрики, и id категории от которой нужно идти вверх.

Для построения такого дерева запускаем функцию build_tree c такими параметрами:

Echo build_tree($cats,0,find_parent($cats_ID,ВАШ_ID_КАТЕГОРИИ));

Есть вопросы? Задавайте в комментариях

  • Алгоритмы
  • К написанию статьи сподвигли часы раздумий и экспериментов в области построения иерархических списков. Изначально логика обкатывалась на SQL запросах, но в последствии решил реализовать на PHP, дабы снять зависимость от СУБД. На простом примере я покажу как можно пройти от корня иерархии до каждого конечного элемента и обратно, информация скорее для новичков.

    Итак, тестовая иерархия, с которой нам предстоит работать:

    В базе данных имеется самая простая таблица на самом простом MSSQL сервере, тонкости подключения опустим, наша цель - разобраться с иерархией и рекурсией.

    Создадим таблицу:

    CREATE TABLE .( IDENTITY(1,1) NOT NULL, -- уникальное поле, автоинкрементное NULL, -- это поле указывает на элемент уровнем выше, содержит uid родителя (255) NULL, (50) NULL, -- права доступа) ON
    Наполним сведениями:

    Описание полей есть в комментариях, чуть подробнее о поле access :

    По умолчанию в моей системе для каждого нового документа проставляется inherit , то есть наследование от родителя. Для нашего эксперимента для некоторых эелементов пропишем доменные группы. В группе Domain Users моя учётная запись имеется, а вот в AD Group Secret меня нет.

    Что ещё мы имеем. Массив, содержащий список моих доменных групп. Достаётся он достаточно просто, на IIS включена аутентификация Windows, всё работает прозрачно, в PHP логин зашедшего находится в переменной $_SERVER[«AUTH_USER»], далее LDAP запросом получаем список групп.

    Теперь предлагаю получить необходимые данные и перейти непосредственно к делу:

    $stmt = $PDO->query("SELECT * FROM Test"); $table = $stmt->fetchAll(); //Получим таблицу из БД $groups = LDAP::getGroups("$login"); //Получим группы ActiveDirectory

    Задача №1

    Необходимо научиться работать с иерархией как с деревом а не списком. Уровень вложенности заранее не известен и может быть любым, следовательно должно быть универсальное средство, позволяющее выполнять проход по дереву как сверху вниз, так и в обратном направлении.

    Задача №2

    Необходимо гибко управлять доступами, то есть, давать права на группы, отдельные документы и т.д., по аналогии с файловой системой NTFS, можно закрыть права на всю папку, но для одного документа в этой папке доступ нарезать - тоже самое должно получиться и у нас.

    Задача №3

    Необходимо скрыть от пользователей ресурсы, к которым у них нет доступа, но самое главное, при наличии прав хотя бы на один документ где то в глубине закрытой для него ветки, делать видимыми элементы ведущие к этому документу (иначе как пользователь до него доберётся?)

    Вот собственно базовая функция:

    $array = array(); //выходной массив function recursive($data, $pid = 0, $level = 0){ global $array; foreach ($data as $row) { //перебираем строки if ($row["pid"] == $pid) { //Начинаем со строк, pid которых передан в функцию, у нас это 0, т.е. корень сайта //Собираем строку в ассоциативный массив $_row["uid"] = $row["uid"]; $_row["pid"] = $row["pid"]; $_row["name"] = $_row["name"] = str_pad("", $level*3, ".").$row["name"]; //Функцией str_pad добавляем точки $_row["level"] = $level; //Добавляем уровень $array = $_row; //Прибавляем каждую строку к выходному массиву //Строка обработана, теперь запустим эту же функцию для текущего uid, то есть //пойдёт обратотка дочерней строки (у которой этот uid является pid-ом) recursive($data, $row["uid"], $level + 1); } } } recursive($table); //Запускаем
    Описание по большей части привёл в комментариях, но если говорить просто - после того как цикл foreach проходит строку и делает что то с данными(в нашем случае просто копирует данные в другой массив, добавляя поле level и точки к имени), он запускает эту же функцию, передав ей uid строки, и поскольку в условии if мы сравниваем его с pid, то следующий запуск однозначно захватит дочерние элементы. Цикл foreach перебирает все строки у которых uid родителя совпадает с переданным значением, поэтому перезапуская саму себя, функция отработает на каждом элементе каждого уровня. Для наглядности, мы так же передаём level увеличивая его на единицу. В итоге мы увидим какой документ какой уровень вложенности имеет.

    Выводим массив $array в браузер:

    Уже не плохо, не так ли?

    А теперь немного усложним нашу функцию:

    $array = array(); //выходной массив $array_idx_lvl = array(); //индекс по полю level function recursive($data, $pid = 0, $level = 0, $path = "", $access_parent = "inherit"){ global $array; global $array_idx_lvl; //Индекс по level global $groups; //доменные группы //перебираем строки foreach ($data as $row) { //Начинаем со строк, pid которых передан в функцию, у нас это 0, т.е. корень сайта if ($row["pid"] == $pid) { //Собираем строку в ассоциативный массив $_row["uid"] = $row["uid"]; $_row["pid"] = $row["pid"]; $_row["name"] = str_pad("", $level*3, ".").$row["name"]; $_row["level"] = $level; //Добавляем уровень $_row["path"] = $path."/".$row["name"]; //Добавляем имя к пути $_row["view"] = ""; //Разруливаем доступы if($row["access"] == "inherit") { $_row["access"] = $access_parent; //Если стоит наследование, делаем как у родителя } else { $_row["access"] = (in_array($row["access"], $groups)) ? "allow" : "deny"; } $array[$row["uid"]] = $_row; //Результирующий массив индексируемый по uid //Для быстрой выборки по level, формируем индекс $array_idx_lvl[$level][$row["uid"]] = $row["uid"]; //Строка обработана, теперь запустим эту же функцию для текущего uid, то есть //пойдёт обратотка дочерней строки (у которой этот uid является pid-ом) recursive($data, $row["uid"], $level + 1, $_row["path"], $_row["access"]); } } } recursive($table); //Запускаем
    Разбираем по порядку:

    1. Добавлено поле path - для формирования пути, добавляем к значению "/" и имя строки, затем полученное значение передаём в функцию, где история повторяется и на выходе получается путь от корня до элемента.

    2. Результирующий массив теперь формируется не по порядку, начиная с нуля а с привязкой к uid - $array[$row["uid"]] = $_row; . В данном случае это ни как не влияет на работу скрипта, но возможность обращаться к строке по индексу, а не перебором в цикле потребуется нам в дальнейшем, когда будем разбирать проход по дереву в обратную сторону.

    3. Добавлен индекс $array_idx_lvl = array(); . Этот индекс нам так же потребуется позже, смысл таков - результирующий набор складывается не в одну кучу, а с разбивкой на массивы индексируемые по level.

    4. Поле Access . Когда функция запускает саму себя, вместе с остальными параметрами она передаёт свою настройку прав $_row["access"] дочерям, а далее происходит следующее, проверяются права - если выставлено наследование (inherit), то применяются права родителя, если нет - через in_array проверяем, есть ли указанная в access доменная группа среди групп зашедшего пользователя. Если есть - добавляем в строку allow (разрешить), иначе deny (запрет).

    Итоговый результат:

    Ну что же, со спуском разобрались, теперь осталось разобраться с подъёмом и заполнением последнего поля view , определяющего видимость элементов. В начале статьи, я говорил для чего это нужно, но можно предположить иную ситуацию. Допустим вы решили привязать древовидный список к навигационному меню сайта, сделанному в виде многоуровневого выпадающего списка с кучей пунктов, и вы просто не хотите, чтобы пользователь, имеющий доступ всего лишь к одному документу ворочал весь этот массив и в объёмном меню искал свой пункт, ведь по сути ему нужно показать всего лишь одну ветку ведущую к нужной кнопке.

    Почему здесь нужен проход в обратную сторону? Предположим у пользователя закрыт доступ для всего контента за исключением одного, самого дальнего(на последнем уровне) документа, если подумать, логично было бы брать начало от доступного, и вести его к корню дерева, показывая только нужные элементы.

    Приступим:

    //Функция прохода вверх по дереву на один уроверь от заданного uid, устанавливает //свойство видимости себе и родителю в зависимости от доступа или заданной ранее видимости... function backRecursive($uid, $view = null, $ident = 0) { global $array; //Если поднялись не больше чем на один уровень if($ident <= 1) { //Если видимость уже есть - не меняем текущую строку, иначе //проверяем доступ и то что пришло от дочки if($array[$uid]["view"] != "show") { $array[$uid]["view"] = ($array[$uid]["access"] == "allow" or $view == "show") ? "show" : "hide"; } backRecursive($array[$uid]["pid"], $array[$uid]["view"], $ident+1); } }
    Что делает эта функция - принимает в качестве параметра uid строки, с которой нужно начать действовать, обращается к этой строке и проверяет видимость. Если в поле view не show(т.е. показывать), а что то другое, проверяет что находится в безопасности, и если там стоит allow (доступ открыт), делает элемент видимым, в противном случае скрытым(hide ), затем запускает себя же, передавая свой pid и настройку видимости, а так же переменную $ident увеличенную на 1, тем самым блокируя последующие самозапуски. При втором проходе, по переданному pid находится родительский элемент, выполняется та же проверка, за исключением одного, если от дочернего в переменной $view передано "show ", то не смотря ни на что, текущему элементу так же присвоится show , то есть видимый.

    На мой взгляд, работа с ограничителем - самый оптимальный вариант, ибо представьте ситуацию, на 10 уровне у нас 100 документов, для полного обхода всего дерева, нам нужно запускать эту функцию на каждом элементе, т.к. если на последнем уровне мы запустим функцию 100 раз, то выполняя самозапуски, перебор 100 раз дойдёт до корня. Если умножить на 10 уровней - уже получится 1000 циклов, что не есть хорошо, поэтому подъём нужно осуществлять равномерно, уровень за уровнем.

    Запускает эту функцию следующий код:

    Function startBack(){ global $array_idx_lvl; $levels = array_keys($array_idx_lvl); //получаем массив уровней $maxLevel = max($levels); //Находим самый глубокий уровень дерева //Проходимся циклом по каждому уровню начиная с самого большого for ($i = $maxLevel; $i > 0; $i--) { $uids = array_keys($array_idx_lvl[$i]); //На текущем уровне обходим все элементы и по каждому инициируем обработку и движение на 1лвл foreach ($uids as $uid) { backRecursive($uid); } } }
    Вот тут как раз и потребовался индекс по уровню. Здесь мы движемся от самого дальнего уровня, заходя в каждый, обрабатывая в нём каждый элемент.

    А вот и картинка:

    Перед запуском, я намеренно прописал разрешающую группу для пункта «Отчет для налоговой», чтобы наглядно показать что код отрабатывает корректно. Несмотря на то, что доступ к разделу «Бухгалтерская отчетность» закрыт, он видимый.

    Вот и собственно всё, думаю с задачей мы справились, основа получена, алгоритм работает, можно применить в реальной системе.

    К написанию статьи сподвигли часы раздумий и экспериментов в области построения иерархических списков. Изначально логика обкатывалась на SQL запросах, но в последствии решил реализовать на PHP, дабы снять зависимость от СУБД. На простом примере я покажу как можно пройти от корня иерархии до каждого конечного элемента и обратно, информация скорее для новичков.

    Итак, тестовая иерархия, с которой нам предстоит работать:

    В базе данных имеется самая простая таблица на самом простом MSSQL сервере, тонкости подключения опустим, наша цель - разобраться с иерархией и рекурсией.

    Создадим таблицу:

    CREATE TABLE .( IDENTITY(1,1) NOT NULL, -- уникальное поле, автоинкрементное NULL, -- это поле указывает на элемент уровнем выше, содержит uid родителя (255) NULL, (50) NULL, -- права доступа) ON
    Наполним сведениями:

    Описание полей есть в комментариях, чуть подробнее о поле access :

    По умолчанию в моей системе для каждого нового документа проставляется inherit , то есть наследование от родителя. Для нашего эксперимента для некоторых эелементов пропишем доменные группы. В группе Domain Users моя учётная запись имеется, а вот в AD Group Secret меня нет.

    Что ещё мы имеем. Массив, содержащий список моих доменных групп. Достаётся он достаточно просто, на IIS включена аутентификация Windows, всё работает прозрачно, в PHP логин зашедшего находится в переменной $_SERVER[«AUTH_USER»], далее LDAP запросом получаем список групп.

    Теперь предлагаю получить необходимые данные и перейти непосредственно к делу:

    $stmt = $PDO->query("SELECT * FROM Test"); $table = $stmt->fetchAll(); //Получим таблицу из БД $groups = LDAP::getGroups("$login"); //Получим группы ActiveDirectory

    Задача №1

    Необходимо научиться работать с иерархией как с деревом а не списком. Уровень вложенности заранее не известен и может быть любым, следовательно должно быть универсальное средство, позволяющее выполнять проход по дереву как сверху вниз, так и в обратном направлении.

    Задача №2

    Необходимо гибко управлять доступами, то есть, давать права на группы, отдельные документы и т.д., по аналогии с файловой системой NTFS, можно закрыть права на всю папку, но для одного документа в этой папке доступ нарезать - тоже самое должно получиться и у нас.

    Задача №3

    Необходимо скрыть от пользователей ресурсы, к которым у них нет доступа, но самое главное, при наличии прав хотя бы на один документ где то в глубине закрытой для него ветки, делать видимыми элементы ведущие к этому документу (иначе как пользователь до него доберётся?)

    Вот собственно базовая функция:

    $array = array(); //выходной массив function recursive($data, $pid = 0, $level = 0){ global $array; foreach ($data as $row) { //перебираем строки if ($row["pid"] == $pid) { //Начинаем со строк, pid которых передан в функцию, у нас это 0, т.е. корень сайта //Собираем строку в ассоциативный массив $_row["uid"] = $row["uid"]; $_row["pid"] = $row["pid"]; $_row["name"] = $_row["name"] = str_pad("", $level*3, ".").$row["name"]; //Функцией str_pad добавляем точки $_row["level"] = $level; //Добавляем уровень $array = $_row; //Прибавляем каждую строку к выходному массиву //Строка обработана, теперь запустим эту же функцию для текущего uid, то есть //пойдёт обратотка дочерней строки (у которой этот uid является pid-ом) recursive($data, $row["uid"], $level + 1); } } } recursive($table); //Запускаем
    Описание по большей части привёл в комментариях, но если говорить просто - после того как цикл foreach проходит строку и делает что то с данными(в нашем случае просто копирует данные в другой массив, добавляя поле level и точки к имени), он запускает эту же функцию, передав ей uid строки, и поскольку в условии if мы сравниваем его с pid, то следующий запуск однозначно захватит дочерние элементы. Цикл foreach перебирает все строки у которых uid родителя совпадает с переданным значением, поэтому перезапуская саму себя, функция отработает на каждом элементе каждого уровня. Для наглядности, мы так же передаём level увеличивая его на единицу. В итоге мы увидим какой документ какой уровень вложенности имеет.

    Выводим массив $array в браузер:

    Уже не плохо, не так ли?

    А теперь немного усложним нашу функцию:

    $array = array(); //выходной массив $array_idx_lvl = array(); //индекс по полю level function recursive($data, $pid = 0, $level = 0, $path = "", $access_parent = "inherit"){ global $array; global $array_idx_lvl; //Индекс по level global $groups; //доменные группы //перебираем строки foreach ($data as $row) { //Начинаем со строк, pid которых передан в функцию, у нас это 0, т.е. корень сайта if ($row["pid"] == $pid) { //Собираем строку в ассоциативный массив $_row["uid"] = $row["uid"]; $_row["pid"] = $row["pid"]; $_row["name"] = str_pad("", $level*3, ".").$row["name"]; $_row["level"] = $level; //Добавляем уровень $_row["path"] = $path."/".$row["name"]; //Добавляем имя к пути $_row["view"] = ""; //Разруливаем доступы if($row["access"] == "inherit") { $_row["access"] = $access_parent; //Если стоит наследование, делаем как у родителя } else { $_row["access"] = (in_array($row["access"], $groups)) ? "allow" : "deny"; } $array[$row["uid"]] = $_row; //Результирующий массив индексируемый по uid //Для быстрой выборки по level, формируем индекс $array_idx_lvl[$level][$row["uid"]] = $row["uid"]; //Строка обработана, теперь запустим эту же функцию для текущего uid, то есть //пойдёт обратотка дочерней строки (у которой этот uid является pid-ом) recursive($data, $row["uid"], $level + 1, $_row["path"], $_row["access"]); } } } recursive($table); //Запускаем
    Разбираем по порядку:

    1. Добавлено поле path - для формирования пути, добавляем к значению "/" и имя строки, затем полученное значение передаём в функцию, где история повторяется и на выходе получается путь от корня до элемента.

    2. Результирующий массив теперь формируется не по порядку, начиная с нуля а с привязкой к uid - $array[$row["uid"]] = $_row; . В данном случае это ни как не влияет на работу скрипта, но возможность обращаться к строке по индексу, а не перебором в цикле потребуется нам в дальнейшем, когда будем разбирать проход по дереву в обратную сторону.

    3. Добавлен индекс $array_idx_lvl = array(); . Этот индекс нам так же потребуется позже, смысл таков - результирующий набор складывается не в одну кучу, а с разбивкой на массивы индексируемые по level.

    4. Поле Access . Когда функция запускает саму себя, вместе с остальными параметрами она передаёт свою настройку прав $_row["access"] дочерям, а далее происходит следующее, проверяются права - если выставлено наследование (inherit), то применяются права родителя, если нет - через in_array проверяем, есть ли указанная в access доменная группа среди групп зашедшего пользователя. Если есть - добавляем в строку allow (разрешить), иначе deny (запрет).

    Итоговый результат:

    Ну что же, со спуском разобрались, теперь осталось разобраться с подъёмом и заполнением последнего поля view , определяющего видимость элементов. В начале статьи, я говорил для чего это нужно, но можно предположить иную ситуацию. Допустим вы решили привязать древовидный список к навигационному меню сайта, сделанному в виде многоуровневого выпадающего списка с кучей пунктов, и вы просто не хотите, чтобы пользователь, имеющий доступ всего лишь к одному документу ворочал весь этот массив и в объёмном меню искал свой пункт, ведь по сути ему нужно показать всего лишь одну ветку ведущую к нужной кнопке.

    Почему здесь нужен проход в обратную сторону? Предположим у пользователя закрыт доступ для всего контента за исключением одного, самого дальнего(на последнем уровне) документа, если подумать, логично было бы брать начало от доступного, и вести его к корню дерева, показывая только нужные элементы.

    Приступим:

    //Функция прохода вверх по дереву на один уроверь от заданного uid, устанавливает //свойство видимости себе и родителю в зависимости от доступа или заданной ранее видимости... function backRecursive($uid, $view = null, $ident = 0) { global $array; //Если поднялись не больше чем на один уровень if($ident <= 1) { //Если видимость уже есть - не меняем текущую строку, иначе //проверяем доступ и то что пришло от дочки if($array[$uid]["view"] != "show") { $array[$uid]["view"] = ($array[$uid]["access"] == "allow" or $view == "show") ? "show" : "hide"; } backRecursive($array[$uid]["pid"], $array[$uid]["view"], $ident+1); } }
    Что делает эта функция - принимает в качестве параметра uid строки, с которой нужно начать действовать, обращается к этой строке и проверяет видимость. Если в поле view не show(т.е. показывать), а что то другое, проверяет что находится в безопасности, и если там стоит allow (доступ открыт), делает элемент видимым, в противном случае скрытым(hide ), затем запускает себя же, передавая свой pid и настройку видимости, а так же переменную $ident увеличенную на 1, тем самым блокируя последующие самозапуски. При втором проходе, по переданному pid находится родительский элемент, выполняется та же проверка, за исключением одного, если от дочернего в переменной $view передано "show ", то не смотря ни на что, текущему элементу так же присвоится show , то есть видимый.

    На мой взгляд, работа с ограничителем - самый оптимальный вариант, ибо представьте ситуацию, на 10 уровне у нас 100 документов, для полного обхода всего дерева, нам нужно запускать эту функцию на каждом элементе, т.к. если на последнем уровне мы запустим функцию 100 раз, то выполняя самозапуски, перебор 100 раз дойдёт до корня. Если умножить на 10 уровней - уже получится 1000 циклов, что не есть хорошо, поэтому подъём нужно осуществлять равномерно, уровень за уровнем.

    Запускает эту функцию следующий код:

    Function startBack(){ global $array_idx_lvl; $levels = array_keys($array_idx_lvl); //получаем массив уровней $maxLevel = max($levels); //Находим самый глубокий уровень дерева //Проходимся циклом по каждому уровню начиная с самого большого for ($i = $maxLevel; $i > 0; $i--) { $uids = array_keys($array_idx_lvl[$i]); //На текущем уровне обходим все элементы и по каждому инициируем обработку и движение на 1лвл foreach ($uids as $uid) { backRecursive($uid); } } }
    Вот тут как раз и потребовался индекс по уровню. Здесь мы движемся от самого дальнего уровня, заходя в каждый, обрабатывая в нём каждый элемент.

    А вот и картинка:

    Перед запуском, я намеренно прописал разрешающую группу для пункта «Отчет для налоговой», чтобы наглядно показать что код отрабатывает корректно. Несмотря на то, что доступ к разделу «Бухгалтерская отчетность» закрыт, он видимый.

    Вот и собственно всё, думаю с задачей мы справились, основа получена, алгоритм работает, можно применить в реальной системе.

    Данный урок посвящен рекурсии в PHP. Из функции можно вызывать другую функцию. Программа может вызвать функцию f1(), которая вызывает функцию f2(), и т.д. Вызывающая себя функция называется рекурсивной. Такой тип рекурсии называется явной рекурсией. Если функция f1() вызывает другую функцию f2(), которая в свою очередь вызывает функцию f1(), то эти функции также рекурсивные. Такой тип рекурсии называется неявной рекурсией. Очевидно, что возможны более сложные формы неявной рекурсии.

    Предположим, что для решения некоторой задачи нужно создать рекурсивную функцию. В этом уроке опишем одну из известных стратегий решения задачи с использованием рекурсивной функции. Процесс рекурсивного решения задачи разделяется на этапы. На первом шаге для решения задачи вызывается рекурсивная функция. В этой функции задача решается для простейшей ситуации. Простейшая ситуация данной задачи называется базисной задачей. Если функция используется для решения базисной задачи, то она возвращает решение или результат.

    Если функция вызывается для решения задачи более сложной, чем базисная задача, то функция разделяет задачу на две части:

    • часть 1, которую функция может решить;
    • часть 2, которую функция не может решить.

    Для применения рекурсии часть 2 должна быть подобна начальной задаче, но относительно меньше или проще.

    Так как образовавшаяся в части 2 задача подобна начальной задаче, поэтому функция вызывает новый собственный экземпляр с целью обработки новой задачи. Это действие называется рекурсивным вызовом или шагом рекурсии.

    Так как на каждом шаге рекурсии (при каждом рекурсивном вызове) задача делится на две части, поэтому то количество этих шагов рекурсии может быть достаточно большое.

    Для завершения рекурсии рекурсивная функция должна образовать последовательность упрощающихся (уменьшающихся) задач, которые приближаются к базисной задаче. Когда, на каком-то шаге рекурсии, функция обнаруживает базисную задачу, она возвращает решение (результат) базисной задачи в предыдущий вызов (предыдущий шаг рекурсии). В этом вызове, в свою очередь, полученный результат объединяется с частью, которую функция может решить. Образовавшееся таким путем решение возвращается на шаг выше, и т.д. Так генерируется последний результат, который возвращается в начальную точку вызова рекурсивной функции. То есть, чтобы был возможен возврат, каждый шаг функции нужно строить таким образом, чтобы он содержал в себе зарезервированное слово return.

    Для демонстрации вышеизложенной идеи приведем пример использования рекурсивной функции.

    Пример 1. Факториал. Факториал целого неотрицательного числа n равен n*(n-1)*(n-2)*...2*1 и обозначается n!. Принято, что 1!=1 и 0!=1. Например, 7!=7*6*5*4*3*2*1=5040.

    Итеративное (нерекурсивное) решение для вычисления факториала таково (файл factorial1.php):



    Факториал



    Вычисление факториала



    Адади натурали (n>=0):





    ";
    exit;
    }
    $f=1;
    for($i=$n;$i>=1;$i--)
    $f*=$i;
    echo "$n!=$f";
    ?>

    Пусть f(n)=n!, тогда
    f(n)=n!=n*(n-1)!=n*f(n-1),
    то есть рекурсивная формула вычисления факториала такова:
    f(n)=n*f(n-1).

    Пример:
    f(5)=5*f(4)=5*4*f(3)=5*4*3*f(2)= 5*4*3*2*f(1)= 5*4*3*2*1=120.

    На основе рекурсивной формулы составим рекурсивную функцию для вычисления факториала (файл factorial2.php):



    Факториал



    Вычисление факториала



    Адади натурали (n>=0):




    if(!isset($_GET["n"]) || ($n = $_GET["n"])=="") {
    echo "Введите натуральное число!
    ";
    exit;
    }
    $f=factorial($n);
    echo "$n!=$f";

    function factorial($i) {
    if($i<2) return 1;
    $k=$i*factorial($i-1);
    return $k;
    }

    ?>

    Если в программу ввести число 5, то для вычисления 5! программа действует следующим образом:

    При первом вызове функции factorial() проверяется является ли посланное функции число меньше 2. Если полученное функцией число не меньше 2, то задача больше или сложнее базисной задачи, следовательно, задача делится на две части:

    1. $k=$i* - часть 1, которую функция может решить;
    2. factorial($n-1) – часть 2, которую функция не может решить.

    Это действие повторяется до получения базисной задачи, то есть до вызова factorial(1). Если число меньше 2 (1 или 0), то функция factorial() возвращает число 1 ($k=1), то есть решается базисная задача. Если данный экземпляр функции factorial() был вызван ранее другим экземпляром функции factorial(), то результат возвращается вызвавшему экземпляру функции. Там возращенный результат $k умножается на переданный функции параметр $n и присваивается $k. Если данный экземпляр функции factorial() был вызван ранее другим экземпляром функции factorial(), то результат возвращается вызвавшему экземпляру функции и описанный процесс повторяется. Если данный экземпляр функции factorial() был вызван из главной части программы, то $k возвращается в эту часть, где результат выводится на экран и работа программы завершается.



    Эта статья также доступна на следующих языках: Тайский

    • Next

      Огромное Вам СПАСИБО за очень полезную информацию в статье. Очень понятно все изложено. Чувствуется, что проделана большая работа по анализу работы магазина eBay

      • Спасибо вам и другим постоянным читателям моего блога. Без вас у меня не было бы достаточной мотивации, чтобы посвящать много времени ведению этого сайта. У меня мозги так устроены: люблю копнуть вглубь, систематизировать разрозненные данные, пробовать то, что раньше до меня никто не делал, либо не смотрел под таким углом зрения. Жаль, что только нашим соотечественникам из-за кризиса в России отнюдь не до шоппинга на eBay. Покупают на Алиэкспрессе из Китая, так как там в разы дешевле товары (часто в ущерб качеству). Но онлайн-аукционы eBay, Amazon, ETSY легко дадут китайцам фору по ассортименту брендовых вещей, винтажных вещей, ручной работы и разных этнических товаров.

        • Next

          В ваших статьях ценно именно ваше личное отношение и анализ темы. Вы этот блог не бросайте, я сюда часто заглядываю. Нас таких много должно быть. Мне на эл. почту пришло недавно предложение о том, что научат торговать на Амазоне и eBay. И я вспомнила про ваши подробные статьи об этих торг. площ. Перечитала все заново и сделала вывод, что курсы- это лохотрон. Сама на eBay еще ничего не покупала. Я не из России , а из Казахстана (г. Алматы). Но нам тоже лишних трат пока не надо. Желаю вам удачи и берегите себя в азиатских краях.

    • Еще приятно, что попытки eBay по руссификации интерфейса для пользователей из России и стран СНГ, начали приносить плоды. Ведь подавляющая часть граждан стран бывшего СССР не сильна познаниями иностранных языков. Английский язык знают не более 5% населения. Среди молодежи — побольше. Поэтому хотя бы интерфейс на русском языке — это большая помощь для онлайн-шоппинга на этой торговой площадке. Ебей не пошел по пути китайского собрата Алиэкспресс, где совершается машинный (очень корявый и непонятный, местами вызывающий смех) перевод описания товаров. Надеюсь, что на более продвинутом этапе развития искусственного интеллекта станет реальностью качественный машинный перевод с любого языка на любой за считанные доли секунды. Пока имеем вот что (профиль одного из продавцов на ебей с русским интерфейсом, но англоязычным описанием):
      https://uploads.disquscdn.com/images/7a52c9a89108b922159a4fad35de0ab0bee0c8804b9731f56d8a1dc659655d60.png