web-dev-qa-db-fra.com

Inverser une liste dans Prolog

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.

9
Jared

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

16
Cornel Marian

Les listes de prologues sont de simples structures de données: ./2

  • La liste vide est le atom [].
  • Une liste d'un élément, [a], Est en fait cette structure: .(a,[]).
  • Une liste de deux éléments, [a,b] Est en fait cette structure: .(a,.(b,[]))
  • Une liste de trois éléments, [a,b,c] Est en fait cette structure: .(a,.(b,.(c,[])))
  • etc.

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.

7
Nicholas Carey

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].
2
mat

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.

1
CapelliC

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.

1
Paulo Moura

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
0
Kintalken