Prolog中的双向链表

Posted

技术标签:

【中文标题】Prolog中的双向链表【英文标题】:Doubly Linked List in Prolog 【发布时间】:2019-06-20 22:29:59 【问题描述】:

我已经在业余时间学习 Prolog 大约 8 个月到一年,现在我正在着手解决一些经典数据结构和算法的实现。

我对在 Prolog 中实现双向链表很感兴趣,但对如何进行感到很困惑。我被 Prolog 所吸引是因为我对“逻辑纯度”感兴趣。

似乎我是如此适应面向对象的范式,以至于没有它我就无法继续前进!

作为双向链表的参考,我的意思类似于此链接中描述的内容:

Double Linked List

【问题讨论】:

那么你是在 Prolog 中说我不能制作双向链表,而是必须使用两个列表?似乎有点奇怪双向链表是我 12 岁时学习的基本数据结构! 我听说过并使用过差异列表。这就是为什么我问你是否建议使用 2 个列表。我想在另一个问题中,我可以想出一个场景,我将使用双向链表并寻求差异列表解决方案。但是我正在将 prolog 中的双向链表作为一个游乐场项目来追求,除了在 Prolog 中实现一个众所周知的数据结构之外没有特别的目标。 @S.Selfial 认为您必须牢记:Prolog 与命令式语言不同。这是一个完全不同的工具。当然,您可以在多种语言中创建双(或单)链表。但这些语言有不同的用途。螺丝刀是一把可怕的锤子。 :) 你能在 C# 中做一个不同的列表吗?可能是。但这会非常麻烦。您可以在 Prolog 中创建双向链表,但首先您需要发明自己的“链接”方式,因为 Prolog 在传统意义上不会这样做。你最终会得到一些非常笨重的东西。 致其他人:在问题中,OP 指出I was attracted to Prolog because I am interested in "logical purity" .,但随后在评论中指出But I'm pursuing the doubly linked list in Prolog as a playground project with no particular aim beyond having the task of implementing a well known data structure in Prolog . 我对这个问题的看法是,OP 知道他们想要什么,并且正在做自己学习锻炼。续 您使用表来制作列表的想法是有效的,我只是想知道您可以离双向链表多远并且仍然这样称呼它。在我的观点中,@lurker 的想法和答案有点过分了。如果一个表是一个双向链表,那么一对具有相同元素、方向相反的 Prolog 列表也是如此。那是一个双向链表吗?比桌子更接近(在精神和行为上),amirite? 【参考方案1】:

在 Prolog 中双链表的一种可能实现是使用 zipper。如果您不熟悉该概念,请参阅例如

https://en.wikipedia.org/wiki/Zipper_(data_structure)

拉链允许您在列表中前后导航,同时提供对当前元素的访问。因此,它提供了双链表共有的功能。 Logtalk(您可以使用大多数 Prolog 编译器运行)包括对 zippers 的库支持。 zipper 协议可以在以下位置浏览:

https://logtalk.org/library/zipperp_0.html

此协议由zlist 对象为列表实现。您可以在以下位置浏览其代码:

https://github.com/LogtalkDotOrg/logtalk3/blob/master/library/zlist.lgt

请注意,大多数谓词都是纯谓词,其中有几个是由事实定义的。还有一个用法示例:

https://github.com/LogtalkDotOrg/logtalk3/tree/master/examples/slides

【讨论】:

感谢您的链接。但是我认为这不适用;同样,上面的 Guy Coders cmets 提出了一个差异列表;因为它是对应于 list 维护的第二个数据结构(拉链)。双向链表 IMO 的优势在于,使用一个数据结构可以向后(然后向前)遍历。另外,至少在幻灯片示例中,似乎必须完全创建列表,然后才能制作拉链吗?即没有 zlist append . P.S. logtalk 看起来很酷,我只是不使用它,因为在学习 prolog 时试图避免 OO 范式。通过查看您的测试用例,我学到了很多关于 prolog 的知识 :-) @S.Selfial 拉链允许聚焦在一个元素上。因此,zlist 附加谓词没有明确的语义。另请注意,Logtalk 是一种声明性的面向对象逻辑编程语言,而不是命令式/过程式 OOP 语言。 @S.Selfial 请注意,您不一定需要 both 列表及其拉链。根据您的应用程序,您可以只拥有拉链并根据需要对其进行扩展和收缩,因为支持这些操作的谓词是纯的。 "你只能有拉链" .是的,我通过阅读幻灯片示例中的代码收集到这一点。但是当你说“增长它”时,正如上面评论中提到的,缺少某种附加,我不知道如何增长它。【参考方案2】:

使它成为双向链表的原因在于它有两个链接而不是一个链接,即对列表中上一项和下一项的引用。所以我们可以创建一个node(Value, Previous, Next) 结构并手动创建列表,如下所示:A = node(1, nil, B), B = node(2, A, nil).。我们可以用类似的方式制作更长的列表,只是创建更多的中间变量。

将其转换回“正常”列表将如下所示:

dl2list(node(X, _, nil), [X]).
dl2list(node(A, _, node(X,Y,Z)), [A|Rest]) :- dl2list(node(X,Y,Z), Rest).

这并没有特别使用“previous”指针,但您可以看到它有效:

?- A = node(1, nil, B), 
   B = node(2, A, C), 
   C = node(3, B, D), 
   D = node(4, C, nil), 
   dl2list(A, L).
A = node(1, nil, _S1), % where
    _S1 = node(2, node(1, nil, _S1), _S2),
    _S2 = node(3, _S1, node(4, _S2, nil)),
B = node(2, node(1, nil, _S1), _S2),
C = node(3, _S1, node(4, _S2, nil)),
D = node(4, _S2, nil),
L = [1, 2, 3, 4] .

我们也可以从头开始向后构建:

dl2listrev(node(X, nil, _), [X]).
dl2listrev(node(A, node(X,Y,Z), _), [A|Rest]) :- dl2listrev(node(X,Y,Z), Rest).

?- A = node(1, nil, B), 
   B = node(2, A, C), 
   C = node(3, B, D), 
   D = node(4, C, nil), 
   dl2listrev(D, L).
A = node(1, nil, _S1), % where
    _S1 = node(2, node(1, nil, _S1), _S2),
    _S2 = node(3, _S1, node(4, _S2, nil)),
B = node(2, node(1, nil, _S1), _S2),
C = node(3, _S1, node(4, _S2, nil)),
D = node(4, _S2, nil),
L = [4, 3, 2, 1] 

要从列表构造双向链表,您需要比以下任何一个更强大的东西:

l2dl(L, DL) :- l2dl(L, DL, nil).

l2dl([X], node(X, Prev, nil), Prev).
l2dl([X,Y|Xs], node(X, Prev, Next), Prev) :- 
    l2dl([Y|Xs], Next, node(X, Prev, Next)).

你可以在这里看到双向工作:

?- l2dl([1,2,3,4], X), dl2list(X, L).
X = node(1, nil, node(2, _S1, node(3, _S2, _S3))), % where
    _S1 = node(1, nil, node(2, _S1, node(3, _S2, _S3))),
    _S2 = node(2, _S1, node(3, _S2, _S3)),
    _S3 = node(4, node(3, _S2, _S3), nil),
L = [1, 2, 3, 4] 

这里:

?- A = node(1, nil, B), 
   B = node(2, A, C), 
   C = node(3, B, D), 
   D = node(4, C, nil), 
   l2dl(L, A).
A = node(1, nil, _S1), % where
    _S1 = node(2, node(1, nil, _S1), _S2),
    _S2 = node(3, _S1, node(4, _S2, nil)),
B = node(2, node(1, nil, _S1), _S2),
C = node(3, _S1, node(4, _S2, nil)),
D = node(4, _S2, nil),
L = [1, 2, 3, 4] 

【讨论】:

啊,看起来很完美!请考虑将此用法示例添加到您的答案中:` ?- dl2list(NODES,[1,2,3,4]) 。节点=节点(1,_A,节点(2,_B,节点(3,_C,节点(4,_D,nil))))? ; ` 好主意!就个人而言,我有点害怕循环术语。 @repeat 是的,不幸的是,你不能在不重新创建整个事物的情况下从双向链表创建派生列表,这与 Prolog 使用的单链表不同,其中前置可以共享剩余的结构。我同意 Paulo 的观点,拉链可能是适应实际用例的更好方法。但我认为您通常可以通过在结构中使用变量来复制 C 数据结构,就像 C 使用指针一样,即使实用程序在复制过程中丢失。【参考方案3】:

正如原始问题的 cmets 中所讨论的,就像在 SQL 中一样,您可以在 Prolog 中断言可用作链表的事实:

head_node(Id).
node(Id, Data, LeftId, RightId).

您可以将原子nil 指定为您的空值。

举个很简单的例子:

head_node(a).

node(a, 123, nil, c).
node(b, 214, c, nil).
node(c, 312, a, b).

然后您可以编写谓词来处理这些数据:

remove_node(NodeId) :-
    node(NodeId, _, LeftId, RightId),
    ...

其余的……可以写成retractassertz等。然而,正如Guy Coder在他的cmets中指出的那样,这缺乏逻辑纯度,这似乎是最初的追求。数据结构使用起来很麻烦,正如我在 cmets 中提到的,最好找到一种更合适的 Prolog-esque 方法来解决给定问题,而不是假设必须使用更适合不同类型的模式来解决它语言。

【讨论】:

只是为了确保我们在同一页面上:我喜欢你的回答、+1 和所有这些东西。但它确实不能称为真诚的双向链表。在 Prolog VM 的某个深处,可能会发生这种情况,也可能不会,但这是无关紧要的。 “链表”是一种众所周知且易于理解的低级数据结构,使用更高的抽象模拟它当然可以,但不一样。 @User9213 谢谢,我明白了。我不认为持久数据结构正在模拟链表。只是存储方式不同而已。我当然同意它没有预期的效率,但我认为它不会使它成为一个“链接列表”。不过,我想我们需要指出“链表”的严格定义来解决这个问题。我看到你在你的定义中来自哪里。您发布的答案深入探讨了我只是在抓挠的问题,即:实际用例是什么,正确的 Prolog 方法是什么? @lurker 这似乎是一个很好的探索选择。但是有很多工作要做。例如:id 是如何分配的?后续记录如何找到上一条记录的id,以便可以链接回它?在 SQL 中,我可以使 id 自动递增,以便数据库分配 id。当我插入记录时,我可以得到 id 作为我的查询的返回,并将 id 用于后续记录。在将事实断言到数据库中时,Prolog 是否提供了类似的功能?还是有其他方法? @lurker 也许***.com/questions/54407104/… 的解决方案可以证明这种方法。 找到了。 “XY 问题”是针对问题“X”尝试解决“Y”并且用户询问有关“Y”为什么不起作用但未提供有关“X”的任何详细信息或描述的特定问题”。不利的方面是尝试解决方案“Y”遇到的特定问题甚至可能与实际问题“X”的良好解决方案无关。【参考方案4】:

这是一个不断出现的问题。你真的需要解释你试图用这个双向链表做什么。我很想再次将其归档到我令人愉快的XY problem 展品收藏中。

该主题的popular opinion 是“解决真正问题的最简单方法通常是问为什么五次”。

那么:为什么需要双向链表?您是否正在实施队列?您想要双向遍历的排序列表?还有什么?

为了让这个答案更真实:

如果您使用普通列表,则可以在需要另一端时将其反转。

如果您需要一个可以从两端推入并从一端弹出的队列,您可以使用 Prolog 队列:

queue_empty(q(0, Q, Q)).
queue_back(q(N, Q, [X|Q0]), X, q(s(N), Q, Q0)).
queue_front(q(N, Q, Q0), X, q(s(N), [X|Q], Q0)).

这真的取决于,你为什么需要双向链表?你的用例是什么?

【讨论】:

@GuyCoder 在我所写的内容中,OP 的问题有两个不同的答案。如果这对您来说还不够好,那么请随心所欲地投票。我不在乎赞成票或反对票。 @GuyCoder 是的。双向链表是一种具体的数据结构,(可能)不可能在纯 Prolog 中实现。例如,队列是一种抽象数据类型,它可以有许多不同的实现(在此处的答案之间,至少有 4 个!)。这不会让 OP 的问题更容易回答,也不会让我更关心你投反对票的原因。 你说得对,队列有时就是双向链表的用途。但我想到了一些更有能力的东西。您要求提供一个用例,我在这里放了一个 ***.com/questions/54407104/… 我期待在那里看到您的解决方案!

以上是关于Prolog中的双向链表的主要内容,如果未能解决你的问题,请参考以下文章

双向链表

数据结构 链表_双向链表的实现与分析

数据结构——双向链表

双向链表

c++中的双向链表写法,主要实现(增删查改,链表逆置,构造函数,运算符重载,等)

双向链表排序c语言程序设计