表处理与递归
1、表头与表尾
表是 Prolog 中一种非常有用的数据结构.表的表述能力很强,数字中的序列、集合,通常语言中的数组、记录等均可用表来表示.表的最大特点是其长度不固定,在程序的运行过程中可动态地变化.具体来讲,就是在程序运行时,可对表施行一些操作,如给表中添加一个元素,或从中删除一个元素,或者将两个表合并为一个表等.用表还可以方便地构造堆栈、队列、链表、树等动态数据结构.
表还有一个重要特点,就是它可分为头和尾两部分.表头是表中第一个元素,而表尾是表中除第一个元素外的其余元素按原来顺序组成的表.例如下面的表所示就是一个例子.
表表 头表 尾
[1, 2, 3, 4, 5] 1 2, 3, 4, 5]
[apple, orange, banana] apple [orange, banana]
[[a, b], [c], [d, e]] [a, b] [[c], [d, e]]
[“Prolog”] “Prolog” []
[]无定义无定义
**表头与表尾示例** 表 2、**表的匹配合一** 在程序中是用“|”来区分表头和表尾的,而且还可以使用变量.例如一般地用“`[H|T]“`来表示一个表,其中 H、T 都是变量,H 为表头,T为表尾.注意,此处 H 是一个元素(表中第一个元素),而 T 则是一个表(除第一个元素外表中的其余元素按原来顺序组成的表).表的这种表示法很有用,它为表的操作提供了极大的方便.如下面的表所示即为用这种表示法通过匹配合一提取表头和表尾的例子. | 表1 | 表2 | 合一后的变量值 | |:————-:|:————-:|:————-:| | [X | Y] | [a, b, c] | X = a, Y = [b, c] | | [X | Y] | [a] | X = a, Y = [] | | [a | Y] | [X, b] | X = a, Y = [b] | | [X, Y, Z] | [a, b, c] | X = a, Y = b, Z = c | | [[a, Y] | Z] | [[X, b], [c]] | X = a, Y = b, Z = [[c]] |
**表的匹配合一示例** 表 还需说明的是,表中的“|”后面只能有一个变量.例如写法 [X | Y, Z] 就是错误的.但竖杠前面的变量可以多于一个.例如写法 [ X, Y | Z] 是允许的.这样,这个表同 [a, b, c] 匹配合一后,有:
X = a, Y = b, Z = [c]1
另外,竖杠的前面和后面也可以是常量,例如 [a | Y] 和 [X | b] 都是允许的,但需要注意,后一个表称为无尾表,如果它同表 [a | Y] 匹配,则有:
X = a, Y = b (而不是 Y = [b])1
如果无“|”,则不能分离出表尾.例如,表 [X, Y, Z] 与 [a, b, c] 合一后得 X = a,Y = b,Z = c,其中变量 Z 并非等于 [c] .
接下来我们通过三个例子来更详细地了解表的操作.
例6-1:设计一个能判断对象 X 是表 L 的成员的程序.
我们可以这样设想:
如果 X 与表 L 中的第一个元素(即表头)是同一个对象,则 X 就是 L的成员;
如果 X 是 L 的尾部的成员,则 X 也就是 L 的成员.
根据这种逻辑关系,有下面的 Prolog 程序:
member(X, [X | Tail]).
member(X, [Head | Tail]) :- member(X, Tail).12
其中第一个子句的语义就是上面的第一句话;第二个子句的语义就是上面的第二句话.可以看出,这个程序中使用了递归技术,因为谓词 member 的定义中又含有它自身.利用这个程序就可以判定任意给定的一个对象和一个表之间是否具有 member(成员)关系.例如,取表 L 为 [a, b, c, d],取 X 为 a,对上面的程序提出如下询问:
Goal : member(a, [a, b, c, d]).1
则回答“yes”.同样对于询问:
Goal : member(b, [a, b, c, d]).
Goal : member(c, [a, b, c, d]).
Goal : member(d, [a, b, c, d]).123
均回答“yes”.但若询问:
Goal : member(e, [a, b, c, d]).1
则回答“no”.如果我们这样询问:
Goal : member(X, [a, b, c, d]).1
意思是要证明存在这样的 X,它是该表的成员,这时系统返回 X 的值,即:
X = a1
如果需要的话,系统还会给出 X 的其他所有值.
例6-2:写一个表的拼接程序,即把两个表连接成一个表.
append([], L, L).
append([H | T], L2, [H | Tn]) :- append(T, L2, Tn).12
程序中第一个子句的意思是空表同任一表 L 拼接的结果仍为表 L;第二个子句的意思是说,一个非空的表 L1 与另一个表 L2 拼接的结果 L3 是这样一个表,它的头是 L1 的头,它的尾是由 L1 的尾 T 同 L2 拼接的结果 Tn.这个程序刻画了两个表与它们的拼接表之间的逻辑关系.
可以看出,谓词 append 是递归定义的,子句append([], L, L).为终结条件即递归出口.
对于这个程序,如果我们询问:
Goal : append([1, 2, 3], [4, 5], L).1
则系统便三次递归调用程序中的第二个子句,最后从第一个子句终止,然后反向依次求出每次的拼接表,最后输出:
L=[1, 2, 3, 4, 5]1
当然,对于这个程序也可以给出其他各种询问,如:
Goal : append([1, 2, 3], [4, 5], [1, 2, 3, 4, 5]).1
系统回答yes.
Goal : append([1, 2, 3], [4, 5], [1, 2, 3, 4, 5, 6]).1
系统回答no.
Goal : append([1, 2, 3], Y, [1, 2, 3, 4, 5]).1
系统回答X = [4, 5].
Goal : append(X, [4, 5], [1, 2, 3, 4, 5]).1
系统回答X = [1, 2, 3].
Goal : append(X, Y, [1, 2, 3, 4, 5]).1
系统回答
X = [], Y = [1, 2, 3, 4, 5]
X = [1], Y = [2, 3, 4, 5]
X = [1, 2], Y = [3, 4, 5]
X = [1, 2, 3], Y = [4, 5]
等(如果需要所有解的话).12345
例6-3:表的输出
print([]).
print([H | T]) :- write(H), print(T).12
例6-4:表的倒置,即求一个表的逆序表.
reverse([], []).
reverse([H | T], L) :- reverse(T, L1), append(L1, [H], L).12
这里,reverse的第一个项是输入,即原表;第二个项是输出,即原表的倒置.
回溯控制
Prolog 在搜索目标解的过程中,具有回溯机制,即当某一个子目标“Gi”不能满足时,就返回到该子目标的前一个子目标“Gi-1”,并放弃“Gi-1”的当前约束值,使它重新匹配合一.在实际问题中,有时却不需要回溯,为此 Prolog 中就专门定义了一个阻止回溯的内部谓同——“!”,称为截断谓词.
截断谓词的语法格式很简单,就是一个感叹号“!”.! 的语义如下.
若将“!”插在子句体内作为一个子目标,它总是立即成功.
若“!”位于子句体的最后,则它就阻止对它所在子句的头谓词的所有子句的回溯访向,而让回溯跳过该头谓词(子目标),去访问前一个子目标(如果有的话).
若“!”位于其他位置,则当其后发生回溯且回溯到“!”处时,就在此处失败,并且“!”还使它所在子句的头谓词(子目标)整个失败(即阻止再去访问头谓词的其余子向(如果有的话),即迫使系统直接回溯到该头谓词(子目标)的前一个子目标(如果有的话)).
举个例子:
考虑下面的程序
p(a). (7 - 1)
p(b). (7 - 2)
q(b). (7 - 3)
r(X) :- p(X), q(X). (7 - 4)
r(c).12345
对于目标:r(X).可有一个解:
Y = b
但当我们把式(7 - 4)改为:
r(X) :- p(X), !, q(X). (7 - 4‘)1
时,却无解.为什么?
这是由于添加了截断谓词“!”.因为式(7 - 4’)中求解子目标 p(X) 时,X 被约束到 a,然后跳过“!”,但在求解子目标 q(a) 时遇到麻烦,于是又回溯到“!”,而“!”阻止了对 p(X)的下一个子句 p(b) 和 r 的下一个定义子句 r(c) 的访问.从而导致整个求解失败.
再举个例子:
设有程序:
g0 :- g11, g12, g13. (7 - 5)
g0 :- g14. (7 - 6)
g12 :- g21, !, g23. (7 - 7)
g12 :- g24, g25. (7 - 8)
... ...12345
给出目标:g0.
假设运行到子目标 g23 时失败,这时如果子句(7 - 7)中无“!”的话,则会回溯到 g21,并且如果 g21 也失败的话,则会访问下面的子句(7 - 8).但由于有“!”存在,所以不能回溯到 g21,而直接宣告 g12 失败.于是由子句(7 - 5),这时则回溯到 g11.如果我们把子句(7 - 7)改为:
g12 :- g21, g23, !. (7 - 9)1
当然这时若 g23 失败时,便可回溯到 g21,而当 g21 也失败时,便回溯到 g12,即子句(7 - 8)被“激活”.但对于修改后的程序,如果 g13 失败,则虽然可回溯到 g12,但对 g12 不做任何事情便立即跳过它,而回溯到 g11.如果子句(7 - 9)中无“!”,则当 g13 失败时,回溯到 g12 便去考虑子句(7 - 8),只有当子句(7 - 8)再失败时才回溯到 g11.
八、程序举例
下面给出几个简单而又典型的程序实例.通过这些程序,读者可以进一步体会和理解 Prolog 程序的风格和能力,也可以掌握一些基本的编程技巧.
例8-1:下面是一个简单的路径查询程序.程序中的事实描述了如下图所示的有向图,规则是图中两节点间有通路的定义.
predicates
road(symbol, symbol)
path(symbol, symbol)
clauses
road(a, b).
road(a, c).
road(b, d).
road(c, d).
road(d, e).
road(b, e).
path(X, Y) :- road(X, Y).
path(X, Y) :- road(X, Z), path(Z, Y).123456789101112
程序中未含目标,所以运行时需给出外部目标.例如当给出目标:
path(a, e).1
时,系统将回答yes,但当给出目标:
path(e, a).1
时,系统则回答no,如果给出目标:
run.1
且在程序中增加子句:
run :- path(a, X), write(”X =“, X), nl, fail.
run.12
屏幕上将会输出:
X = b
X = c
X = d
X = e
X = d
X = e
X = e1234567
即从 a 出发到其他节点的全部路径.
例8-2:下面是一个求阶乘程序,程序中使用了递归.
/* a Factorial Program */
domains
n, f = integer
predicates
factorial(n, f)
goal
readint(I),
factorial(I, F),
write(I, ”!=“, F).
clauses
factorial(1, 1).
factorial(N, Res) :-
N》 0,
N1 = N - 1,
factorial(N1, FacN1),
Res = N * FacN1.12345678910111213141516
程序运行时,从键盘上输入一个整数,屏幕上将显示其阶乘数.
例8-3:下面是一个表的排序程序,采用插入排序法.
/* insert sort */
domains
listi = integer*
predicates
insert_sort(listi, listi)
insert(integer, listi, listi)
asc_order(integer, integer)
clauses
insert_sort([], []).
insert\_sort([H | Tail], Sorted_list) :-
insert_sort(Tail, Sorted\_Tail),
insert(H, Sorted_Tial, Sorted\_list).
insert(X, [Y | Sorted_list1]) :-
asc_order(X, Y), !,
insert(X, Sorted_list, Sorted\_list1).
insert(X, Sorted_list, [X | Sorted\_list]).
asc_order(X, Y) :- X》 Y.1234567891011121314151617
程序中对表的处理使用了递归.程序中也未给出目标,需要在运行时临时给出.例如当给出目标:
insert_sort([5, 3, 4, 2, 6, 1, 7, 8, 9, 0], L).1
时,系统将输出:
L = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]1
评论
查看更多