【问题标题】:A program that finds the Kth largest element in a list查找列表中第 K 个最大元素的程序
【发布时间】:2025-12-17 13:15:01
【问题描述】:

我正在为 kth_largest(Xs,K) 编写一个逻辑程序,它实现了查找 列表 Xs 的第 k 个最大元素 K。该算法有以下步骤:

  1. 将列表分成五组。
  2. 有效地找到每个组的中位数,这可以通过固定数量的 比较。
  3. 递归求中位数的中位数。
  4. 根据中位数对原始列表进行分区。
  5. 在适当的较小列表中递归查找第 k 个最大元素。

我该怎么做?我可以从列表中选择一个元素,但我不知道如何使用上述过程获得最大的元素。这是我从列表中选择一个元素的定义

select(X; HasXs; OneLessXs)  
% The list OneLessXs is the result of removing
% one occurrence of X from the list HasXs.
select(X,[X|Xs],Xs).
select(X,[Y|Ys],[Y|Zs]) :-  select(X,Ys,Zs).

【问题讨论】:

标签: prolog


【解决方案1】:

我将加入,因为没有人尝试过回答,并希望对要编程的过程有所了解。

我发现 Selection algorithm 上的 Wikipedia 文章对于理解此类“快速”(最坏情况线性时间)算法的全局非常有帮助。

但是您在问题末尾提出的问题比较简单。您写道“我该怎么做?我可以从列表中选择一个元素,但我不知道如何使用上述过程获得最大的。” (重点由我添加)

现在,您是否要实施“上述过程”似乎有点困惑,这是通过连续搜索中位数来找到第 k 个最大元素的通用方法,或者您是否询问如何使用该方法简单地找到最大的元素(一种特殊情况)。请注意,该配方没有专门使用查找最大元素的步骤来定位中位数或第 k 个最大元素。

但是您给出的代码是在删除该元素后查找列表中的一个元素以及该列表的其余部分,这是一个不确定的谓词,并且允许回溯列表的所有成员。

找到最大元素的任务是确定性的(至少在所有元素不同的情况下),而且它比一般选择第 k 大元素(与订单统计等相关的任务)更容易。

让我们给出一些简单但显然正确的代码来找到最大的元素,然后讨论一种更优化的方法。

maxOfList(H,[H|T]) :-  upperBound(H,T), !.
maxOfList(X,[_|T]) :-  maxOfList(X,T).

upperBound(X,[ ]).
upperBound(X,[H|T]) :-
    X >= H,
    upperBound(X,T).

这个想法应该是可以理解的。我们查看列表的头部并询问该条目是否是列表其余部分的上限。如果是这样,那必须是最大值,我们就完成了(切割使其具有确定性)。如果不是,则最大值必须出现在列表的后面,因此我们丢弃头部并继续递归搜索作为所有后续元素上限的条目。剪切在这里是必不可少的,因为我们必须在第一个这样的条目处停下来才能知道它是原始列表的最大值。

我们使用了一个辅助谓词 upperBound/2,这并不罕见,但这个实现的总体复杂性是列表长度的最坏情况二次方。所以还有改进的余地!

让我在这里暂停一下,以确保我在尝试解决您的问题时不会完全偏离轨道。毕竟你可能想问如何使用“上述过程”来找到 kth 最大的元素,所以我所描述的可能过于专业。然而,它可能有助于理解一般选择算法的聪明之处,以了解简单案例的细微优化,找到最大的元素。

添加:

直观地说,我们可以减少最坏情况下所需的比较次数 通过浏览列表并跟踪找到的最大值“所以 远”。在程序语言中,我们可以通过重新分配轻松地完成此任务 变量的值,但 Prolog 不允许我们直接这样做。

Prolog 的一种做法是引入一个额外的参数并 通过调用辅助谓词来定义谓词 ma​​xOfList/2 三个参数:

maxOfList(X,[H|T]) :- maxOfListAux(X,H,T).

ma​​xOfListAux/3 中的额外参数可用于跟踪 “到目前为止”的最大值如下:

maxOfListAux(X,X,[ ]).
maxOfListAux(Z,X,[H|T]) :-
    ( X >= H  -> Y = X ; Y = H ),
    maxOfListAux(Z,Y,T).

这里 maxOfListAux 的第一个参数代表最终答案 列表中最大的元素,但我们不知道答案,直到我们 已清空列表。所以这里的第一个子句“最终确定”了答案 发生这种情况时,将第一个参数与第二个参数统一起来 (“到目前为止”的最大值)就在列表的尾部到达时 结束。

maxOfListAux 的第二个子句未绑定第一个参数,并且 相应地“更新”第二个参数作为列表的下一个元素 是否超过之前的最大值。

在这种情况下,使用辅助谓词并不是绝对必要的, 因为我们可能已经通过使用 列表的头部而不是额外的参数:

maxOfList(X,[X]) :- !.
maxOfList(X,[H1,H2|T]) :-
    ( H1 >= H2  -> Y = H1 ; Y = H2 ),
    maxOfList(X,[Y|T]).

【讨论】:

  • 您的解决方案完美运行。但我想要的是使用上述程序搜索连续的中位数。您不必使用选择,这只是一个想法。但是我将如何实施中位数程序。否则,您的解决方案既简单又简洁。我也看到了很多辅助谓词这个词,但它是什么。它是一个自身具有一个递归目标的子句吗?
  • @Wasswa Samuel:辅助谓词就像一个辅助函数,通常通过将我们需要的谓词定义扩展为允许最后调用优化(尾递归)的形式来“帮助”。在列表中查找最大元素的更有效方法就是一个示例。让我解释一件事,然后我们将看看“中位数的中位数”的想法以及它如何给出一般的选择算法。
  • 如何将随机列表拆分为 5 个元素的较小列表。以及如何检索中位数。我可以使用显示的谓词子列表(Xs,Ys)生成任何子列表:-前缀(Xs,Ys)。子列表(Xs,[Y|Ys]):- 子列表(Xs,Ys)。前缀([],Ys)。前缀([X|Xs],[X|Ys]):- 前缀(Xs,Ys)。
  • @Wasswa Samuel:将列表分组为五个批次很简单,只需将前五个元素作为一个批次(并继续)。上下文不需要以任何特殊方式对五个元素进行分组。另一个关于中位数算法的好文章是这些lecture notes on deterministic selection。作者指出,只需 6 次比较就可以找到 5 个元素的中位数!
  • @Wasswa Samuel:用最少的比较次数 (6) 计算 5 的中位数并不重要。您可以简单地对列表进行排序并选择中间条目。然而,在其他 * 问题中获得最小比较次数 have been explored 的详细信息,尽管不是专门针对 Prolog。