プログラミングクラスの宿題を終えました。リストを逆にするPrologプログラムを作成することになっていた。しかし、なぜそれが正確に機能するのか理解できません。
%1. reverse a list
%[a,b,c]->[c,b,a]
%reverse(list, rev_List).
reverse([],[]). %reverse of empty is empty - base case
reverse([H|T], RevList):-
reverse(T, RevT), conc(RevT, [H], RevList). %concatenation
この場合、RevTとは正確には何ですか?私はそれがTの逆または指定されたリストの残りを表すことになっていることを知っていますが、何にも割り当てていないので、どのようにして値を持つことができるのかわかりません。 RevListと同じ目的を果たしますが、再帰呼び出しごとにですか?
また、conc()関数呼び出しでHだけでなく[H]を使用する必要があるのはなぜですか。 Hはリストの先頭を参照しませんか(例:[H])?それとも、リストの先頭にある項目(単にH)を参照しているだけですか?
これを片付けてください。このタイプのプログラミングの背後にある論理を理解するのに苦労しています。
あなたの解決策は説明しました:空のリストを元に戻すと、空のリストを取得します。リスト[H | T]を逆にすると、Tを逆にして[H]と連結して得られたリストになります。再帰句が正しいことを確認するには、リスト[a、b、c、d]を検討してください。このリストの末尾を逆にすると、[d、c、b]が得られます。これを[a]と連結すると[d、c、b、a]が生成されます。これは[a、b、c、d]の逆です。
別の逆の解決策:
reverse([],Z,Z).
reverse([H|T],Z,Acc) :- reverse(T,Z,[H|Acc]).
コール:
?- reverse([a,b,c],X,[]).
詳細については、次をお読みください: http://www.learnprolognow.org/lpnpage.php?pagetype=html&pageid=lpn-htmlse25
プロローグリストは単純なデータ構造です:_./2
_
[]
_です。[a]
_は、実際には次の構造です:.(a,[])
。[a,b]
_は実際には次の構造です:.(a,.(b,[]))
[a,b,c]
_は実際にはこの構造です:.(a,.(b,.(c,[])))
角括弧表記は構文糖であり、括弧を入力して人生を費やさないようにします。目に優しいことは言うまでもありません。
これから、リストのhead(最も外側の_./2
_構造のデータ)とリストのtail(含まれるサブリスト)の概念が得られます。その最も外側の_./2
_データ構造内。
これは基本的に、Cの従来の単一リンクリストと同じデータ構造です。
_struct list_node
{
char payload ;
struct list_node *next ;
}
_
ここで、next
ポインターはNULLまたは別のリスト構造です。
したがって、そこから、reverse/2の単純な[素朴な]実装が得られます。
_reverse( [] , [] ) . % the empty list is already reversed.
reverse[ [X] , [X] ) . % a list of 1 item is already reversed. This special case is, strictly speaking, optional, as it will be handled by the general case.
reverse( [X|Xs] , R ) :- % The general case, a list of length >= 1 , is reversed by
reverse(Xs,T) , % - reversing its tail, and
append( T , [X] , R ) % - appending its head to the now-reversed tail
. %
_
これと同じアルゴリズムは、より従来型のプログラミング言語で単一にリンクされたリストを逆にする場合にも機能します。
ただし、このアルゴリズムはあまり効率的ではありません。O(n2)初心者向けの動作。また、末尾再帰ではありません。つまり、十分な長さのリストがあると、スタックオーバーフローが発生します。
プロローグリストに項目を追加するには、リスト全体をたどる必要があることに注意してください。プロローグリストの構造により、前置は簡単な操作です。次のように、既存のリストの前に項目を追加できます。
_prepend( X , Xs , [X|Xs] ) .
_
プロローグの一般的なイディオムはworker predicateをaccumulatorと一緒に使用することです。これにより、_reverse/2
_の実装がはるかに効率的になり、(おそらく)理解しやすくなります。ここでは、アキュムレータを空のリストとしてシードすることにより、リストを逆にします。ソースリストを反復処理します。ソースリスト内のアイテムに遭遇すると、それを逆順リストの前に追加し、その結果、逆順リストを作成します。
_reverse(Xs,Ys) :- % to reverse a list of any length, simply invoke the
reverse_worker(Xs,[],Ys) . % worker predicate with the accumulator seeded as the empty list
reverse_worker( [] , R , R ). % if the list is empty, the accumulator contains the reversed list
reverse_worker( [X|Xs] , T , R ) :- % if the list is non-empty, we reverse the list
reverse_worker( Xs , [X|T] , R ) % by recursing down with the head of the list prepended to the accumulator
.
_
これで、O(n)時間で実行される_reverse/2
_実装ができました。これは末尾再帰でもあります。つまり、スタックを壊すことなく、任意の長さのリストを処理できます。
代わりに、理解しやすいDCGの使用を検討してください。
reverse([]) --> [].
reverse([L|Ls]) --> reverse(Ls), [L].
例:
?- phrase(reverse([a,b,c]), Ls).
Ls = [c, b, a].
この場合、RevTとは正確には何ですか?私はそれがTの逆または指定されたリストの残りを表すことになっていることを知っていますが、何にも割り当てていないため、どのようにして値を持つことができるのかわかりません。 RevListと同じ目的を果たしますが、再帰呼び出しごとにですか?
Prologの変数は、関係の引数の「プレースホルダー」です。私たちが知っていることは、呼び出しが成功した後、指定された引数がthat関係に当てはまるということです。
呼び出しが成功した場合、RevT
には値が含まれます。具体的には、呼び出しの最後の引数になりますconc(RevT, [H], RevList)
、whenリストはnot空です。それ以外の場合は、空のリストになります。
また、conc()関数呼び出しでHだけでなく[H]を使用する必要があるのはなぜですか。 Hはリストの先頭を参照しませんか(例:[H])?それとも、リストの先頭にある項目(単にH)を参照しているだけですか?
はい、Hはリストの最初のitem(通常はelementと呼ばれます)を参照しており、次のようにリストを(1つの要素のみ)になるように「再形成」する必要があります。 conc/3で必要です。これはlists間の別の関係です。
testingreverse/2
述語の定義に関するメモ。コメントを収めるには長すぎます。
リストを逆にすることは、QuickCheckを導入するための「hello world」の例です。つまり、リストを使用して、定義のテストに役立てることができます。最初に、reverse/2
述語を保持するpropertyを定義します。リストを2回反転すると、元のリストが得られ、これを次のように変換できます。 :
same_list(List) :-
reverse(List, Reverse),
reverse(Reverse, ReverseReverse),
List == ReverseReverse.
Logtalkのlgtunit
ツールのQuickCheck実装を使用する:
% first argument bound:
| ?- lgtunit::quick_check(same_list(+list)).
% 100 random tests passed
yes
% both arguments unbound
| ?- lgtunit::quick_check(same_list(-list)).
% 100 random tests passed
yes
または単に:
% either bound or unbound first argument:
| ?- lgtunit::quick_check(same_list(?list)).
% 100 random tests passed
yes
ただし、2番目の引数をバインドしてテストするには、別のプロパティ定義が必要です。
same_list_2(Reverse) :-
reverse(List, Reverse),
reverse(List, ListReverse),
Reverse == ListReverse.
これで次のことができます。
% second argument bound:
| ?- lgtunit::quick_check(same_list_2(+list)).
% 100 random tests passed
yes
ただし、このプロパティベースの/ランダム化されたテストでは、非終了のケースはチェックされないことに注意してください。これは、最初のソリューションの後のバックトラック時にのみ発生するためです。
以下は、reverse/2の典型的な実装です。ただし、次のように「非終了」とマークされている問題があります。
?- ['/dev/tty'] .
reverse(_source_,_target_) :-
reverse(_source_,_target_,[]) .
reverse([],_target_,_target_) .
reverse([_car_|_cdr_],_target_,_collect_) :-
reverse(_cdr_,_target_,[_car_|_collect_]) .
end_of_file.
。
?- reverse([],Q) .
Q = []
?- reverse([a],Q) .
Q = [a]
?- reverse([a,b],Q) .
Q = [b,a]
?- reverse([a,b,c],Q) .
Q = [c,b,a]
?- reverse(P,[]) .
P = [] ? ;
%% non-termination ! %%
^CAction (h for help): a
?- reverse(P,[a]) .
P = [a] ? ;
%% non-termination ! %%
^CAction (h for help): a
?- reverse(P,[a,b]) .
P = [b,a] ? ;
%% non-termination ! %%
^CAction (h for help): a
?- reverse(P,[a,b,c]) .
P = [c,b,a] ? ;
%% non-termination ! %%
^CAction (h for help): a