Erlang程式設計與問題解決/樹


樹結構是將幾個節點連通,使每一個節點對其他每一個節點的連通路徑各只有一條。樹符合下列性質。

  1. 空集合是樹。
  2. 以一個節點做為樹根,其下擁有若干子樹,這樣也是樹。

以下由一個二元樹的定義開始,介紹 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 函數,對指定的二元搜尋樹求平均走訪次數。提示:你須要取出樹的全部節點,並分別使用每個節點走訪這株樹;將每個走訪次數加總求平均。