J'ai terminé un devoir pour mon cours de programmation. J'étais censé créer un programme Prolog qui inverse une liste. Cependant, j'ai du mal à comprendre pourquoi cela fonctionne exactement.
%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
En quoi consiste exactement RevT dans ce cas? Je sais qu'il est censé représenter l'inverse de T ou le reste de la liste donnée, mais je ne vois pas comment cela pourrait avoir une valeur car je ne l'ai assigné à rien. Est-ce qu'il sert simplement le même objectif que RevList mais pour chaque appel récursif?
Aussi, pourquoi dois-je utiliser [H] au lieu de simplement H dans mon appel de fonction conc ()? H ne fait-il pas référence au début de la liste (ex: [H])? Ou fait-il simplement référence à l'élément en tête de liste (juste H)?
S'il vous plaît, aidez-moi à clarifier les choses. J'ai du mal à comprendre la logique derrière ce type de programmation.
Votre solution expliquée: Si nous inversons la liste vide, nous obtenons la liste vide. Si nous inversons la liste [H | T], nous nous retrouvons avec la liste obtenue en inversant T et en concaténant avec [H]. Pour voir que la clause récursive est correcte, considérons la liste [a, b, c, d]. Si nous inversons la queue de cette liste, nous obtenons [d, c, b]. La concaténation avec [a] donne [d, c, b, a], qui est l'inverse de [a, b, c, d]
Une autre solution inverse:
reverse([],Z,Z).
reverse([H|T],Z,Acc) :- reverse(T,Z,[H|Acc]).
appel:
?- reverse([a,b,c],X,[]).
Pour plus d'informations, veuillez lire: http://www.learnprolognow.org/lpnpage.php?pagetype=html&pageid=lpn-htmlse25
Les listes de prologues sont de simples structures de données: ./2
[]
.[a]
, Est en fait cette structure: .(a,[])
.[a,b]
Est en fait cette structure: .(a,.(b,[]))
[a,b,c]
Est en fait cette structure: .(a,.(b,.(c,[])))
La notation entre crochets est sucre syntaxique pour vous éviter de passer votre vie à taper des parenthèses. Sans oublier que c'est plus agréable pour les yeux.
De cela, nous obtenons la notion de head de la liste (la donnée dans la structure ./2
La plus externe) et la tail de la liste (la sous-liste contenue dans cette structure de données la plus externe ./2
.
Il s'agit essentiellement de la même structure de données que celle que vous voyez pour une liste à liaison unique classique en C:
struct list_node
{
char payload ;
struct list_node *next ;
}
où le pointeur next
est NULL ou une autre structure de liste.
Donc, à partir de cela, nous obtenons l'implémentation [naïve] simple de 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
. %
Ce même algorithme fonctionnerait pour inverser une liste à liaison unique dans un langage de programmation plus conventionnel.
Cependant, cet algorithme n'est pas très efficace: il présente O (n2) comportement, pour commencer. Ce n'est pas non plus récursif, ce qui signifie qu'une liste de longueur suffisante entraînera un débordement de pile.
Il convient de noter que pour ajouter un élément à une liste de prologues, il faut parcourir la liste entière, le préfixe est une opération triviale, en raison de la structure d'une liste de prologues. Nous pouvons ajouter un élément à une liste existante aussi simplement que:
prepend( X , Xs , [X|Xs] ) .
Un idiome commun dans prolog est d'utiliser un prédicat de travail avec un accumulateur. Cela rend l'implémentation de reverse/2
Beaucoup plus efficace et (peut-être) un peu plus facile à comprendre. Ici, nous inversons la liste en semant notre accumulateur comme une liste vide. Nous parcourons la liste des sources. Lorsque nous rencontrons un élément dans la liste source, nous le ajoutons à la liste inversée, produisant ainsi la liste inversée au fur et à mesure.
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
.
Vous avez maintenant une implémentation reverse/2
Qui s'exécute en O(n) temps. Elle est également récursive, ce qui signifie qu'elle peut gérer une liste de n'importe quelle longueur sans faire exploser sa pile.
Pensez à utiliser un DCG à la place, ce qui est beaucoup plus facile à comprendre:
reverse([]) --> [].
reverse([L|Ls]) --> reverse(Ls), [L].
Exemple:
?- phrase(reverse([a,b,c]), Ls).
Ls = [c, b, a].
En quoi consiste exactement RevT dans ce cas? Je sais qu'il est censé représenter l'inverse de T ou le reste de la liste donnée, mais je ne vois pas comment cela pourrait avoir une valeur car je ne l'ai assigné à rien. Est-ce qu'il sert simplement le même objectif que RevList mais pour chaque appel récursif?
Les variables dans Prolog sont des "espaces réservés" pour les arguments des relations. Ce que nous savons, après un appel réussi, c'est exactement que les arguments spécifiés sont valables pour la relation that.
RevT
aura alors une valeur si l'appel réussit. Plus précisément, sera le dernier argument de l'appel conc(RevT, [H], RevList)
, quand la liste est pas vide. Sinon, sera la liste vide.
Aussi, pourquoi dois-je utiliser [H] au lieu de simplement H dans mon appel de fonction conc ()? H ne fait-il pas référence au début de la liste (ex: [H])? Ou fait-il simplement référence à l'élément en tête de liste (juste H)?
Oui, H fait référence au premier item (généralement appelé element) de la liste, alors nous devons le "remodeler" pour qu'il soit une liste (d'un seul élément), comme requis par conc/3, c'est une autre relation entre listes.
Juste une note sur testreverse/2
définitions de prédicat, trop longues pour contenir un commentaire.
Inverser une liste est l'exemple "bonjour le monde" pour introduire QuickCheck, ce qui signifie que vous pouvez l'utiliser pour vous aider à tester votre définition. Tout d'abord, nous définissons une propriété qui vaut pour le reverse/2
prédicat: inverser une liste deux fois doit donner la liste d'origine, que nous pouvons traduire en:
same_list(List) :-
reverse(List, Reverse),
reverse(Reverse, ReverseReverse),
List == ReverseReverse.
En utilisant l'implémentation QuickCheck de l'outil lgtunit
de Logtalk:
% 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
Ou simplement:
% either bound or unbound first argument:
| ?- lgtunit::quick_check(same_list(?list)).
% 100 random tests passed
yes
Mais nous avons besoin d'une autre définition de propriété pour tester avec le deuxième argument lié:
same_list_2(Reverse) :-
reverse(List, Reverse),
reverse(List, ListReverse),
Reverse == ListReverse.
On peut maintenant faire:
% second argument bound:
| ?- lgtunit::quick_check(same_list_2(+list)).
% 100 random tests passed
yes
Mais notez que ces tests basés sur des propriétés/randomisés ne vérifient pas les cas non terminés car ils ne se produisent que lors du retour en arrière après la première solution.
Voici l'implémentation typique de reverse/2. Il a cependant le problème marqué ci-dessous avec "non-résiliation".
?- ['/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