Erlang程式设计与问题解决/树
树结构是将几个节点连通,使每一个节点对其他每一个节点的连通路径各只有一条。树符合下列性质。
- 空集合是树。
- 以一个节点做为树根,其下拥有若干子树,这样也是树。
以下由一个二元树的定义开始,介绍 Erlang 如何实作树及算法。
二元树
[编辑]树的基本构成,有两种情况:
nil % 空集合
{Root, Left, Right} % 有樹根、左子樹和右子樹
先有一个最简单的树生产式 create/1 ,产生 N 个节点并且大致平衡的树。用 x 标示节点位置。
-module(binary_tree).
-compile(export_all).
create(0) ->
nil;
create(N) ->
Half = (N-1) div 2,
{x, create(Half), create(N-1-Half)}.
> binary_tree:create(6).
{x,{x,nil,{x,nil,nil}},{x,{x,nil,nil},{x,nil,nil}}}
上述程式提供了雏型,让我们可以由简单的结果检查程式是否运作良好。我们可以转抄上述程式的框架,写成资料妥善分配的版本。
create([]) ->
nil;
create([X|Xs]) ->
Half = (length(Xs)-1) div 2,
{Left, Right} = split_at(Half, Xs),
{ X, create(Left), create(Right) }.
split_at(0, Xs) ->
{[], Xs};
split_at(N, [X|Xs]) ->
{Ys, Zs} = split_at(N-1, Xs),
{[X|Ys], Zs}.
程式执行结果为:
> binary_tree:create([a,b,c,d]).
{a,{b,nil,nil},{c,nil,{d,nil,nil}}}
接着还可以写一些函数检查树的一些属性:例如, height/1 检查树高度, node_count/1 检查节点数目, node_count_at/2 检查某一层的节点数目。
height(nil) ->
0;
height({_,L,R}) ->
1 + erlang:max(height(L), height(R)).
node_count(nil) ->
0;
node_count({_,L,R}) ->
1 + node_count(L) + node_count(R).
node_count_at(_, nil) ->
0;
node_count_at(1, {_,_,_}) ->
1;
node_count_at(Level, {_,L,R}) ->
node_count_at(Level-1, L) + node_count_at(Level-1, R).
接着,接到一个列表,产生全部可能的树,程式如下:
all_trees([]) ->
[nil];
all_trees([X|Xs]) ->
[ {X, L, R}
|| {Ls, Rs} <- [ split_at(Half,Xs)
|| Half <- lists:seq(0,length(Xs))],
L <- all_trees(Ls),
R <- all_trees(Rs) ].
稍做修改,可以求全部可能的树的高度:
all_trees_height([]) ->
[0];
all_trees_height([_|Xs]) ->
[ 1 + erlang:max(Ln,Rn)
|| {Ls, Rs} <- [ split_at(Half,Xs)
|| Half <- lists:seq(0,length(Xs))],
Ln <- all_trees_height(Ls),
Rn <- all_trees_height(Rs) ].
平衡树
[编辑]平衡树,又叫做自我平衡树,有好几种类别。其中一种称为 AVL tree ,是指在树的左子树与右子树的高度,最多不相差超过 1 。
AVL 树
[编辑]现在,要做的问题是,给一个列表,产生可能构成的 AVL 树。我们已经有一个 all_tree/1 可以产生一个列表全部可能有的树。对每一株树,我们可以有 is_avl_tree/1 判断是否为 AVL 树。
is_avl_tree(nil) ->
true;
is_avl_tree({_,L,R}) ->
case height(L) - height(R) of
0 ->
is_avl_tree(L) and is_avl_tree(R);
1 ->
is_avl_tree(L) and is_avl_tree(R);
-1 ->
is_avl_tree(L) and is_avl_tree(R);
_ ->
false
end.
于是,使用 lists 模组的 filter 函数,可以根据指定的准则,将符合的结果找出。我们可以找出全部的 AVL tree 。
avl_trees(Xs) ->
lists:filter(
fun(T) -> test:is_avl_tree(T) end,
all_trees(Xs)
).
> binary_tree:avl_trees([a,b,c,d]).
[{a,{b,nil,nil},{c,nil,{d,nil,nil}}},
{a,{b,nil,nil},{c,{d,nil,nil},nil}},
{a,{b,nil,{c,nil,nil}},{d,nil,nil}},
{a,{b,{c,nil,nil},nil},{d,nil,nil}}]
完整树
[编辑]接下来讨论另一种平衡树:完整树 ( complete tree ) 。完整树是在它最高高度以下的任何一层中,能够补满节点的位置都补满了。而在树的最高一层,所有的节点全部向左靠齐,留下右边全都是 nil 。或者,最高的一层也全部补满。
从上述建构树的程式的思路走来,我们发现,上述的树建构都思考子树本身的局部情况。然而,来处理完整树时,你必须思考自己子树和其他子树的一些节点的情况。所以,在此要写一些新的函数。
level_at/2 是从树中取出指定层次的一列节点;要注意的是,把一层里没有节点的位置填上 nil :
level_at(1, nil) ->
[nil];
level_at(1, {Rt,_,_}) ->
[Rt];
level_at(N, nil) when N > 1 ->
level_at(N-1, nil) ++ level_at(N-1, nil);
level_at(N, {_,L,R}) when N > 1 ->
level_at(N-1, L) ++ level_at(N-1, R).
对最高高度以下的任何一层,用 check_level_full/1 检查列表中全部填满节点,也就是没有 nil :
check_level_full([]) ->
true;
check_level_full([nil|_]) ->
false;
check_level_full([_|Ns]) ->
check_level_full(Ns).
对最高高度一层,用 check_level_complete/1 检查列表中的节点全部向左靠齐:
check_level_complete([]) ->
true;
check_level_complete([_]) ->
true;
check_level_complete([nil,Node|_]) when Node /= nil ->
false;
check_level_complete([_|Ns]) ->
check_level_complete(Ns).
于是,检查一株树是不是完整树,作法是先取树高,接着求低于树高的每一层都是填满节点,最后求最高层节点都向左靠齐:
check_complete_tree(nil) ->
true;
check_complete_tree(T) ->
Height = height(T),
check_full_level_at([ level_at(H,T) || H <- lists:seq(1,Height-1) ])
andalso check_complete_level_at(Height, T).
check_complete_level_at(H, T) ->
check_level_complete(level_at(H, T)).
check_full_level_at([]) ->
true;
check_full_level_at([Level|Ls]) ->
check_level_full(Level) andalso check_full_level_at(Ls).
使用下列主程式,先产生全部可能的树,然后对每一株树检查并筛选出完整树:
complete_trees(Xs) ->
lists:filter(
fun check_complete_tree/1,
all_trees(Xs)
).
% lists:filter/2 使用指定的檢查函數 fun check_complete_tree/1
% 判斷一個列表 all_trees(Xs) 中有哪些樹是完整樹。如果是,就篩選出來
执行情况为:
> binary_tree:complete_trees([a,b,c,d,e,f,g]).
[{a,{b,{c,nil,nil},{d,nil,nil}},{e,{f,nil,nil},nil}}]
> binary_tree:complete_trees([a,b,c,d,e,f,g,h]).
[{a,{b,{c,nil,nil},{d,nil,nil}},
{e,{f,nil,nil},{g,nil,nil}}}]
> binary_tree:complete_trees([a,b,c,d,e,f,g,h,i]).
[{a,{b,{c,{d,nil,nil},nil},{e,nil,nil}},
{f,{g,nil,nil},{h,nil,nil}}}]
> binary_tree:complete_trees([a,b,c,d,e,f,g,h,i]).
[{a,{b,{c,{d,nil,nil},{e,nil,nil}},{f,nil,nil}},
{g,{h,nil,nil},{i,nil,nil}}}]
- 练习题
- 最大堆积树 ( max heap ) 有若干性质为: (1) 是树,拥有树的各种性质 (2) 任一子树的左、右子树每一节点都不大于树根节点。请仿照本节的程式手法──产生并过滤──撰写 max_heap:create/1 ,输入一个数值列表,产生出各种可能的最大堆积树。
二元搜寻树
[编辑]二元搜寻树是有序的树结构,可以在其中以二元搜寻法寻找资料。首先,可以用最简单的二部分来建构二元搜寻树:关于列表的处理和节点的插入。
-module(bstree).
-compile(export_all).
create([]) ->
nil;
create([X|Xs]) ->
insert(X, create(Xs)).
insert(X, nil) ->
{X, nil, nil};
insert(X, {X, L, R}) ->
{X, insert(X, L), R};
insert(X, {Y, L, R}) when X > Y ->
{Y, L, insert(X, R)};
insert(X, {Y, L, R}) ->
{Y, insert(X, L), R}.
基本上已经可以将任何列表转换为二元搜寻树了。树的结构由列表资料决定:例如,以下案例产生最糟情况的二元搜寻树,看起来就像是链接串列。
> bstree:create([1,2,3,4,5,6,7,8]).
{8,
{7,{6,{5,{4,{3,{2,{1,nil,nil},nil},nil},nil},nil},nil},nil},
nil}
可想而知,须调整树的平衡程度。
树的平衡
[编辑]树的调整步骤可分为树左移和树右移。以树左移来说,是从树的右子树取最左端点当做新的树根,同时将旧树根插入左子树,就得到左移一步的新树。树右移的步骤与树左移对称。
为了做到树左右移,我们需要几个函数: node_count/1 求子数的节点总数、 take_leftest/1 取出子树最左端点并留下余树、 take_rightest/1 取出子树最右端点并留下余树。
node_count(nil) ->
0;
node_count({_,L,R}) ->
1 + node_count(L) + node_count(R).
take_leftest(nil) ->
{nil, nil};
take_leftest({Rt,nil,R}) ->
{Rt, R};
take_leftest({Rt,L,R}) ->
{Node, Rest} = take_leftest(L),
{Node, {Rt, Rest, R}}.
take_rightest(nil) ->
{nil, nil};
take_rightest({Rt,L,nil}) ->
{Rt, L};
take_rightest({Rt,L,R}) ->
{Node, Rest} = take_rightest(R),
{Node, {Rt, L, Rest}}.
已知树左移与树右移如下列程式:
shl(nil) ->
nil;
shl({Rt,L,R}) ->
{Node, Rest} = take_leftest(R),
{Node, insert(Rt, L), Rest}.
shr(nil) ->
nil;
shr({Rt,L,R}) ->
{Node, Rest} = take_rightest(L),
{Node, Rest, insert(Rt, R)}.
则树平衡为函数 balance/1 :求左右子树节点数目,当二子树节点数目相差超过一个,就要做树左移或树右移;并且,二子树也要做树平衡。
balance(nil) ->
nil;
balance({Rt,L,R}) ->
Nl = node_count(L),
Nr = node_count(R),
if
Nl - Nr > 1 ->
balance(shr({Rt,L,R}));
Nr - Nl > 1 ->
balance(shl({Rt,L,R}));
true ->
{Rt, balance(L), balance(R)}
end.
二元搜寻树的平衡情况如下例:
> bstree:balance(bstree:create([1,2,3,4,5,6,7,8])).
{5,
{3,{2,{1,nil,nil},nil},{4,nil,nil}},
{7,{6,nil,nil},{8,nil,nil}}}
- 练习一
- 以上述二元搜寻树为例,撰写 search/2 函数,在树中寻找指定资料。当资料找到时,传回 {Data, Traversal} , Traversal 为走访次数。 (由任一树根走往左子树树根算走访一次,同理,走往右子树树根也算走访一次。) 如果找不到资料,传回 {nil, Traversal} 。
- 练习二
- 撰写 avg_traversal/1 函数,对指定的二元搜寻树求平均走访次数。提示:你须要取出树的全部节点,并分别使用每个节点走访这株树;将每个走访次数加总求平均。