Prolog列表是简单的数据结构:./2
- 空列表是原子
[]
。
- 一个元素的列表
[a]
实际上是这个结构:.(a,[])
。
- 两个元素的列表
[a,b]
实际上是这个结构:.(a,.(b,[]))
- 三个元素的列表
[a,b,c]
实际上是这个结构:.(a,.(b,.(c,[])))
- 以此类推。
方括号表示法是“语法糖”,可以节省您在键入括号时的时间。更不用说,它对眼睛更友好。
由此,我们得到了列表的 头部(最外层 ./2
结构中的数据)和列表的 尾部(包含在那个最外层 ./2
数据结构中的子列表)的概念。
本质上,这与 C 中经典的单向链表所使用的数据结构相同:
struct list_node
{
char payload ;
struct list_node *next ;
}
其中next
指针要么为空,要么是另一个列表结构。
因此,我们可以得到简单[天真的]实现reverse/2:
reverse( [] , [] ) .
reverse[ [X] , [X] ) .
reverse( [X|Xs] , R ) :-
reverse(Xs,T) ,
append( T , [X] , R )
.
这个算法同样适用于在更常规的编程语言中反转单向链表。
然而,这个算法并不是非常高效:首先它表现出O(n2)的行为。其次,它不是尾递归的,这意味着足够长的列表会导致堆栈溢出。
需要注意的是,要将一个项附加到Prolog列表中,需要遍历整个列表,而由于Prolog列表的结构,将一个项放在列表前面是一个微不足道的操作。我们可以简单地将一个项预置到现有列表中:
prepend( X , Xs , [X|Xs] ) .
在Prolog中常见的习惯用法是使用一个带有累加器的“工作谓词”。这使得reverse/2
的实现更加高效,同时也可能更容易理解。在这里,我们通过将累加器初始化为空列表来反转列表。我们遍历源列表。当我们遇到源列表中的项目时,我们将其前置到反转列表中,从而在进行操作的同时生成反转列表。
reverse(Xs,Ys) :-
reverse_worker(Xs,[],Ys) .
reverse_worker( [] , R , R ).
reverse_worker( [X|Xs] , T , R ) :-
reverse_worker( Xs , [X|T] , R )
.
现在您已经有了一个运行时间为O(n)的reverse/2
实现。它也是尾递归的,这意味着它可以处理任何长度的列表而不会导致栈溢出。