Вообще, чем мне не нравится Adjacency List, так это рекурсией, особенно, когда нужно выбрать дерево, без каких либо ограничений, например:
Perl код (1)
оригинал
- Все дерево комментариев;
- Карта сайта;
- Навигационное меню;
- и т.д.;
Задача
Сделать один запрос к базе и в один проход собрать одноуровневый список в нужном порядке.Решение
Как решить такую задачу на уровне базы данных у меня пока идей нет, но на уровне приложения — легко.Основная сложность состоит в том, что для определенного узла мы можем еще не получить его родителя и детей. Поэтому воспользуемся временными списками и будем их склеивать по мере надобности. Алгоритм предлагаю на Perl.Perl код (1)
#!/usr/bin/perl
use warnings; use strict;
# Выборка данных производится как ORDER BY pid DESC, [ORDER] ASC
# сейчас не буду использовать для этого базу данных, а возьму уже как бы выбранный список
my @select = (
# ID PID ORDER OTHER DATA...
{ id => 14, pid => 1, ord => 1, data => 'other data' },
{ id => 15, pid => 4, ord => 1, data => 'other data' },
{ id => 24, pid => 4, ord => 2, data => 'other data' },
{ id => 23, pid => 7, ord => 1, data => 'other data' },
{ id => 27, pid => 7, ord => 2, data => 'other data' },
{ id => 25, pid => 7, ord => 3, data => 'other data' },
{ id => 17, pid => 7, ord => 4, data => 'other data' },
{ id => 28, pid => 11, ord => 1, data => 'other data' },
{ id => 21, pid => 11, ord => 2, data => 'other data' },
{ id => 5, pid => 12, ord => 1, data => 'other data' },
{ id => 11, pid => 12, ord => 2, data => 'other data' },
{ id => 13, pid => 12, ord => 3, data => 'other data' },
{ id => 10, pid => 12, ord => 4, data => 'other data' },
{ id => 22, pid => 12, ord => 5, data => 'other data' },
{ id => 2, pid => 13, ord => 1, data => 'other data' },
{ id => 6, pid => 15, ord => 1, data => 'other data' },
{ id => 20, pid => 15, ord => 2, data => 'other data' },
{ id => 9, pid => 15, ord => 3, data => 'other data' },
{ id => 7, pid => 16, ord => 1, data => 'other data' },
{ id => 1, pid => 20, ord => 1, data => 'other data' },
{ id => 26, pid => 20, ord => 2, data => 'other data' },
{ id => 8, pid => 25, ord => 1, data => 'other data' },
# Самое главное, что бы root узлы были в самом конце
{ id => 12, pid => 0, ord => 1, data => 'other data' },
{ id => 4, pid => 0, ord => 2, data => 'other data' },
{ id => 3, pid => 0, ord => 3, data => 'other data' },
{ id => 18, pid => 0, ord => 4, data => 'other data' },
{ id => 16, pid => 0, ord => 5, data => 'other data' },
{ id => 19, pid => 0, ord => 6, data => 'other data' },
);
my %indexes = (); # В этом хеше будем хранить координаты узлов, (key списка, и порядок в списке)
my %lists = (); # Списки узлов изначально по PID
my $tpid = undef; # Текущий PID списка
foreach my $row (@select) {
# Неизветсно, какой pid у корневых узлов, может undef, а может 0
$row->{pid} ||= 'root';
# Если не определен текущий ID родительского узла, то подставляем его
$tpid ||= $row->{pid};
# Устанавливаем уровень узла изначально 1
$row->{level} = 1;
# Вставляем наш узел в список PID
push @{$lists{$row->{pid}}}, $row;
# Проставляем координату для узла
$indexes{$row->{id}} = [$row->{pid}, $#{$lists{$row->{pid}}}];
# Если уже есть сформированный список для ID текущего узла
if ($lists{$row->{id}}) {
# Для этого списка проставляем новые координаты, что он присоединится к текущему списку,
# порядок в новом списке, и уровень увеличиваем на 1
map {
$indexes{$_->{id}}->[0] = $row->{pid};
$indexes{$_->{id}}->[1] += scalar @{$lists{$row->{pid}}};
$_->{level}++
} @{$lists{$row->{id}}};
# Присоединяем подчиненный список к текущему
push @{$lists{$row->{pid}}}, @{$lists{$row->{id}}};
# Удаляем его за ненадобностью
delete $lists{$row->{id}};
}
# Если у нас текущий PID не соответствует PID узла, значит список с текущим PID сформирован до конца
if ($tpid ne $row->{pid}) {
# Был ли у нас родительский узел до этого момента, если нет, то он сам подцепит этот список
# когда до него дойдет очередь
if ($indexes{$tpid}) {
# Увеличиваем порядок узлов в родительском списке от родительского узла до конца на количество
# узлов внедряемого списка
map {
$indexes{$_}->[1] += scalar @{$lists{$tpid}}
} @{$lists{$indexes{$tpid}->[0]}}[$indexes{$tpid}->[1] + 1..$#{$lists{$indexes{$tpid}->[0]}}];
# проставляем новые координаты для узлов внедряемого списка,
# и уровень относительно родительского узла
map {
$indexes{$_->{id}}->[0] = $indexes{$tpid}->[0];
$indexes{$_->{id}}->[1] += $indexes{$tpid}->[1] + 1;
$_->{level} += $lists{$indexes{$tpid}->[0]}->[$indexes{$tpid}->[1]]->{level};
} @{$lists{$tpid}};
# Внедряем список относительно координат родительского узла
splice @{$lists{$indexes{$tpid}->[0]}}, $indexes{$tpid}->[1] + 1, 0, @{$lists{$tpid}};
# Удаляем список за ненадобностью
delete $lists{$tpid};
}
# Проставляем новый PID
$tpid = $row->{pid};
}
}
# Собственно готовый список у нас будет под ключом, который мы указали для корневых узлов.
my @result = @{$lists{root}};
# Посмотрим для проверки
foreach my $row (@result) {
print $row->{id}, "\t", $row->{pid}, "\t", $row->{ord}, "\t", $row->{level}, "\t", $row->{data}, "\n"
}
1;
Пояснения
Изначальная сортировка обязательна только для PID (в обратном порядке) только потому, что список корневых узлов вклеивать в общем-то никуда не надо. Иначе бы пришлось после завершения цикла «доклеить» последний список.Также я внедрил в алгоритм еще расчет уровня узлов, так как в алгоритме Adjacency List данного поля нет по определению.Кстати, в хеше %lists кроме списка root останутся списки-артефакты, ни к чему не привязанные.оригинал
