Erlang. Сбалансированные деревья.
От: Lazy Cjow Rhrr Россия lj://_lcr_
Дата: 16.12.05 07:10
Оценка: 90 (6)
Есть книжка, называется "Concurrent Programming in Erlang", авторы Joe Armstrong, Robert Virding, Claes Wikström и Mike Williams.

Там есть глава по сбалансированным бинарным деревьям (AVL — деревьям) — как реализовать эту структуру данных на функциональном языке. Когда я тупо попытался вколотить этот код в редактор, компилятор дал мне понять, что я был не прав. Пришлось разбираться в этой реализации, заодно вспомнить AVL-деревья и сделать свой вариант (в принципе получилось то же самое, но есть и различия).

Когда мне это удалось, захотелось выложить код с комментариями сюда, потом показалось, что всё это тривиально и желание пропало. Сейчас оно снова появилось, потому что одному человеку стало интересно, чем же хорош паттерн матчинг, и чем он отличается от простых if'ов; Для демонстрации нужен был какой нибудь пример, желательно не совсем уж банальный.

Наверное, это будет интересно (полезно?) многим. В принципе, для гуру здесь ничего нового, они могут пробежаться глазами просто.

Дерево — это кортеж {Ключ, Высота, Меньшее_поддерево, Большее_поддерево}. Элемент Высота нужен для балансировки. В оригинале реализовывалось отображение Key->Value, где ключи располагались в узлах АВЛ-дерева, однако Value только мешается, поэтому его убираем. В левом поддереве все ключи меньше Ключа, в правом — больше. Дублирование исключено. Пустое дерево — это кортеж {nil,0,nil,nil}.

Заголовок.
-module (avltree).
-export ([test0/0, test1/0, test2/0]).

про функции test?() — в конце. Нам понадобятся маленькие вспомогательные функции:
empty_tree() -> {nil, 0, nil, nil}.

elt_k({K, _, _, _}) -> K.
elt_h({_, H, _, _}) -> H.
elt_s({_, _, S, _}) -> S.
elt_b({_, _, _, B}) -> B.

max(X, Y) when X > Y -> X;
max(X, Y) when X =< Y -> Y.


Функция lookup ищет в дереве данный ключ. Каждая функция определяется одним или несколькими "телами" (clauses), каждое "тело" — это реакция на совпадение фактических параметров с данным паттерном. Паттерн может быть сколь угодно сложным. Обратите внимание как сформулировано условие "если первый параметр и первый элемент в кортеже совпадают" (в данном случае совпадение — это равенство) и как сформулировано условие "... различаются"
lookup(_, {nil, 0, nil, nil}) ->
    key_not_found;
lookup(Key, {Key, H, S, B}) ->
    {Key, H, S, B};
lookup(Key, {Key1, _, Smaller, Bigger}) ->
    if
        Key < Key1 -> lookup(Key, Smaller);
        Key > Key1 -> lookup(Key, Bigger)
    end.


Функция insert вставляет ключ Key в дерево. Во втором "теле" вместо паттерна "K, {K,H,S,B}" использовано
соответствующее условие (guard).
insert(Key, {nil, 0, nil, nil}) ->
    E = empty_tree(),
    {Key, 1, E, E};
insert(Key, {K, H, S, B}) when Key == K ->
    {K, H, S, B};
insert(Key, {K, _, S, B}) when Key < K ->
    {Ks, _, Ss, Bs} = insert(Key, S),
    combine(Ss, Ks, Bs, K, B);
insert(Key, {K, _, S, B}) when Key > K ->
    {Kb, _, Sb, Bb} = insert(Key, B),
    combine(S, K, Sb, Kb, Bb).

Последние 2 "тела" нужно пояснить. Функция combine собирает сбалансированное дерево из 5 кусочков — трёх деревьев и двух ключей. Между этими кусочками должно выполняться соотношение T1 < Kx < T2 < Kx < T3, где T1, Kx, T2, Kx, T3 — это как раз набор параметров для функции combine (T? — деревья, K? — ключи, T < K означает, что все ключи в T меньше K).

Короче, в первом "теле" ключ Key должен быть добавлен в меньшее поддерево S, после добавление мы получаем сбалансированное дерево {Ks, _, Ss, Bs}, и к этому дереву нужно добавить ещё 2 кусочка K и B. После добавления, дерево может "перекосить",
поэтому надо, чтобы combine снова собрал нам сбалансированное дерево. Аналогично со вторым "телом".

Теперь функция delete — удаления ключа Key из дерева. Удаление несколько сложнее: здесь возникает много мелких частных случаев.

Удаляем из пустого дерева и одноэлементного дерева — здесь всё понятно. Хотя в случае пустого дерева можно вернуть какой-нибудь атом-варнинг.
delete(Key, {nil, 0, nil, nil}) ->
    {nil, 0, nil, nil};
delete(Key, {Key, 1, {nil, 0, nil, nil}, {nil, 0, nil, nil}}) ->
    {nil, 0, nil, nil};


Далее распознаём следующие вырожденные деревья ({E} — пустое дерево):
%      Key
%     /   \     ===>    B
%   {E}     B
delete(Key, {Key, _, {nil, 0, nil, nil}, B}) ->
    B;

%     Key
%    /   \      ===>    S
%   S    {E}
delete(Key, {Key, _, S, {nil, 0, nil, nil}}) ->
    S;


Следующие два случая ниже не были учтены, из-за чего валился test1(). S1 должно быть всегда пустым деревом но можно оставить такой, чуть более общий, вариант. (S1 пустое потому, что S1 получается из S удалением элемента, и S не может быть
большим, так как исходное дерево сбалансированное). Аналогично рассуждая, B1 тоже должно быть пустым деревом, но опять же, оставим B1.
% 
%    Key                 Key       
%   /   \       ===>    /   \      
%  S    {E}           S1    {E} 
delete(Key, {K, _, S, {nil, 0, nil, nil}}) when Key < K ->
    S1 = delete(Key, S),
    {K, elt_h(S1) + 1, S1, {nil, 0, nil, nil}};

%        Key                 Key  
%       /   \     ===>      /   \ 
%     {E}    B            {E}    B1
delete(Key, {K, _, {nil, 0, nil, nil}, B}) when Key > K ->
    B1 = delete(Key, B),
    {K, elt_h(B1) + 1, {nil, 0, nil, nil}, B1};


Наконец, остался случай невырожденного дерева. Если искомый ключ меньше или больше текущего, то нужно удалить его в соответствующем поддереве и перебалансировать дерево (см. случаи Key < K и Key > K). Если же искомый ключ равен текущему, то удаление ключа Key разбивает дерево на два несвязных поддерева:
%    Key
%   /   \    ===>
%  S     B         S    B

и из этих двух кусочков нам нужно собрать новое дерево. Как? Нужно найти новый корень. Претендентами на новый корень являются либо максимальный ключ в S, либо минимальный ключ в B. Случаи совершенно равноправные, поэтому не сильно заморачиваясь выберем максимальный ключ в S. После этого удаляем этот ключ из S, для оставшегося куска будет справедливо S1 < K:
%             K
%  S  ===>   /
%           S1

Взглянув в деревце B более пристально, увидим, что оно есть {Kb, ?, Sb, Bb} и получим снова 5 кусочков (S1 < K < Sb < Kb < Bb), из которых мы можем собрать новое сбалансированное дерево:
%     K              K     Kb
%    /       ===>   /     /  \    ===> combine(S1, K, Sb, Kb, Bb)
%   S1   B         S1    Sb   Bb
delete(Key, {K, _, S, B}) ->
    if 
        Key == K ->
            {K1, S1} = extract_max(S),
            combine(S1, K1, elt_s(B), elt_k(B), elt_b(B));
        Key < K ->
            S1 = delete(Key, S),
            combine(S1, K, elt_s(B), elt_k(B), elt_b(B));
        Key > K ->
            B1 = delete(Key, B),
            combine(elt_s(S), elt_k(S), elt_b(S), K, B1)
    end.

Функция extract_max (бывшая deletesp) ищет максимальный ключ в дереве, удаляет его оттуда и возвращает пару из этого ключа и оставшегося после удаления дерева.
extract_max({K, _, S, {nil, 0, nil, nil}}) ->
    {K, S};
extract_max({_, _, {nil, 0, nil, nil}, B}) ->
    extract_max(B);
extract_max({K, _, {K1, _, S1, B1}, B}) ->
    {K2, B2} = extract_max(B),
    {K2, combine(S1, K1, B1, K, B2)}.


Осталась функция combine. Для функции combine важно, чтобы для параметров сохранялось условие T1 < Kx < T2 < Ky < T3, иначе результатом будет
combine(T1, Kx, T2, Ky, T3) ->
    H1 = elt_h(T1),
    H2 = elt_h(T2),
    H3 = elt_h(T3),
    if
        H2 > H1, H2 > H3 ->
            {elt_k(T2), H2 + 2, {Kx, H2 + 1, T1, elt_b(T2)}, {Ky, H2 + 1, elt_s(T2), T3}};
        H1 >= H2, H1 >= H3 ->
            Hy = max(H2, H3) + 1,
            Hx = max(H1, Hy) + 1,
            {Kx, Hx, T1, {Ky, Hy, T2, T3}};
        H3 >= H2, H3 >= H1 ->
            Hx = max(H1, H2) + 1,
            Hy = max(Hx, H3) + 1,
            {Ky, Hy, {Kx, Hx, T1, T2}, T3}
    end.

Сборка нового дерева зависит от того, какое дерево самое высокое. Если T2 самое высокое, то:
                                                            K2
                                                          /    \
    Kx      Ky              Kx    K2    Ky             Kx        Ky
   /    T2    \    ===>    /     /  \     \    ===>   /  \      /  \
 T1            T3        T1    S2    B2    T3       T1    S2  B2    T3

если T1, то
                            Kx
                           /  \
    Kx      Ky           T1    Ky
   /    T2    \    ===>       /  \
 T1            T3           T2    T3

наконец, если самое высокое T3, то
                               Ky
                              /  \
    Kx      Ky              Kx    T3
   /    T2    \    ===>    /  \
 T1            T3        T1    T2


Для сравнения, вот как выглядели функции combine и deletesp раньше:
combine({K1,V1,H1,S1,B1},AK,AV,
        {K2,V2,H2,S2,B2},BK,BV,
        {K3,V3,H3,S3,B3} ) when H2 > H1, H2 > H3 ->
            {K2,V2,H1 + 2,
            {AK,AV,H1 + 1,{K1,V1,H1,S1,B1},S2},
            {BK,BV,H3 + 1,B2,{K3,V3,H3,S3,B3}}
            };
combine({K1,V1,H1,S1,B1},AK,AV,
        {K2,V2,H2,S2,B2},BK,BV,
        {K3,V3,H3,S3,B3} ) when H1 >= H2, H1 >= H3 ->
            HB = max_add_1(H2,H3),
            HA = max_add_1(H1,HB),
            {AK,AV,HA,
            {K1,V1,H1,S1,B1},
            {BK,BV,HB,{K2,V2,H2,S2,B2},{K3,V3,H3,S3,B3}}
            };
combine({K1,V1,H1,S1,B1},AK,AV,
        {K2,V2,H2,S2,B2},BK,BV,
        {K3,V3,H3,S3,B3} ) when H3 >= H1, H3 >= H2 ->
            HA = max_add_1(H1,H2),
            HB = max_add_1(HA,H3),
            {BK,BV,HB ,
            {AK,AV,HA,{K1,V1,H1,S1,B1},{K2,V2,H2,S2,B2}},
            {K3,V3,H3,S3,B3}
            }.

deletesp({Key,Value,1,{nil,nil,0,nil,nil},{nil,nil,0,nil,nil}}) ->
        {Key,Value,{nil,nil,0,nil,nil}};
deletesp({Key,Value,_,Smaller,{nil,nil,0,nil,nil}}) ->
        {Key,Value,Smaller};
deletesp({K1,V1,2,{nil,nil,0,nil,nil},
         {K2,V2,1,{nil,nil,0,nil,nil},{nil,nil,0,nil,nil}}}) ->
            {K2,V2,
             {K1,V1,1,{nil,nil,0,nil,nil},{nil,nil,0,nil,nil}}
            };
deletesp({Key,Value,_,{K3,V3,_,S3,B3},Bigger}) ->
        {K2,V2,Bigger2} = deletesp(Bigger),
        {K2,V2,combine(S3, K3, V3, B3, Key, Value, Bigger2)}.

Вроде бы получилось лучше


Что осталось? Осталось распечатать деревце, и написать небольшие тестики.
write_tree(T) ->
    write_tree(0, T),
    io:format("\n").

write_tree(D, nil) ->
    indent(D),
    io:format('nil', []);
write_tree(D, {K, H, S, B}) ->
    D1 = D + 4,
    write_tree(D1, B),
    io:format('~n', []),
    indent(D),
    io:format('~w:~w ==>|~n', [K, H]),
    write_tree(D1, S).

indent(D) when D > 0 -> 
    io:format("\t"),
    indent(D-4);
indent(D) when D =< 0 ->
    noop.

test0() ->
    E  = empty_tree(),
    T1 = insert(13, E),
    T2 = insert(13, T1),
    T  = insert(15, T2),
    TE = lookup(13, T),
    TN = lookup(99, T),
    write_tree(T),
    write_tree(TE),
    io:format(TN).


test1() ->
    E   = empty_tree(),
    T1  = insert(1,E),
    T2  = insert(2,T1),
    T3  = insert(3,T2),
    T4  = insert(4,T3),
    T5  = insert(5,T4),
    T6  = insert(6,T5),
    T7  = insert(7,T6),
    T8  = insert(8,T7),
    T9  = insert(9,T8),
    T10 = insert(10,T9),
    T11 = insert(11,T10),
    T12 = insert(12,T11),
    T13 = insert(13,T12),
    T14 = insert(14,T13),
    T15 = insert(15,T14),
    T16 = insert(16,T15),
    write_tree(T16),

    T17 = delete(16, T16),
    write_tree(T17).


test2() ->
    E  = empty_tree(),
    X1 = {13, 1, E, E},
    X2 = {15, 1, E, E},
    X3 = {17, 1, E, E},
    write_tree(combine(X1, 14, X2, 16, X3)).


Работает!
quicksort =: (($:@(<#[),(=#[),$:@(>#[)) ({~ ?@#)) ^: (1<#)
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.