排序算法中,如果成对比较可以返回更多信息(而不仅仅是-1、0、+1),则称为什么?

22
大多数排序算法依赖于一种成对比较的方式,确定A < B、A = B或A > B。
我正在寻找利用可以区分整体与局部差异的成对比较函数的算法(并且额外加分的话,Python代码)。因此,比较函数返回{-2, -1, 0, 1, 2}或{-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5}甚至是介于(-1, 1)区间内的实数,而不是返回{-1, 0, 1}。
对于某些应用程序(例如近似排序或近似排序),这将使得通过更少的比较即可确定合理的排序。

你能保证对于比较函数f()和值x、y和z,距离f(x,y)+f(y,z)=f(x,z)吗?这个等式是否应该是<=?这很重要 :-)。 - Joel Bender
是的,我知道那个问题。在我的应用程序中,我不能保证完全排序,但我只需要一个近似排序而不是完全排序。 - James Tauber
3
如果您往下阅读,原帖发布者正在寻求减少由一组人类专家提供的比较结果,其中比较结果是主观的。 - Tom Leys
7个回答

14

额外的信息确实可以用来最小化总比较次数。调用超级比较函数可以做出相当于调用常规比较函数很多次的推理。例如,a much-less-than bc little-less-than b意味着a < c < b

这些推理可以被组织成箱或分区,每个箱或分区可以单独排序。有效地,这相当于使用n路划分的快速排序。以下是Python的一种实现:

from collections import defaultdict
from random import choice

def quicksort(seq, compare):
    'Stable in-place sort using a 3-or-more-way comparison function'
    # Make an n-way partition on a random pivot value
    segments = defaultdict(list)
    pivot = choice(seq)
    for x in seq:
        ranking = 0 if x is pivot else compare(x, pivot)
        segments[ranking].append(x)
    seq.clear()

    # Recursively sort each segment and store it in the sequence
    for ranking, segment in sorted(segments.items()):
        if ranking and len(segment) > 1:
            quicksort(segment, compare)
        seq += segment

if __name__ == '__main__':
    from random import randrange
    from math import log10

    def super_compare(a, b):
        'Compare with extra logarithmic near/far information'
        c = -1 if a < b else 1 if a > b else 0
        return c * (int(log10(max(abs(a - b), 1.0))) + 1)

    n = 10000
    data = [randrange(4*n) for i in range(n)]
    goal = sorted(data)
    quicksort(data, super_compare)
    print(data == goal)

通过使用 trace 模块来对这段代码进行仪器化,可以测量性能增益。在上述代码中,一个常规的三向比较使用了 133,000 次比较,而一个超级比较函数将调用次数降低到了 85,000。

该代码还使得尝试不同比较函数变得容易。这将表明,天真的 n 路比较函数对于排序几乎没有任何帮助。例如,如果比较函数对于大于四的差异返回 +/-2,对于小于等于四的差异返回 +/-1,则将只有5%的少量比较次数。根本原因是在开头使用的粗略分割仅有少数“接近匹配”,其他所有内容都属于“远程匹配”。

超级比较的一种改进方法是涵盖对数范围(即如果在十个内,则为+/-1,在一百个内则为+/-2,在一千个内则为+/-3)。

理想的比较函数应该是自适应的。对于任何给定的序列大小,比较函数都应该努力将序列划分为大致相等的分区。信息理论告诉我们,这将最大化每个比较的信息位数。

自适应方法也有很好的直观意义。人们应该首先被分为喜欢,然后再进行更精细的区分,如大爱 vs 小爱。随着每次分区传递,应该越来越精细。


7
你可以使用修改过的快速排序算法。让我举个例子,当你的比较函数返回 [-2, -1, 0, 1, 2] 时,你需要对数组 A 进行排序。
创建 5 个空数组:Aminus2、Aminus1、A0、Aplus1、Aplus2。
选择 A 的任意一个元素 X。
对于数组中的每个元素,将其与 X 进行比较。
根据结果,将元素放入 Aminus2、Aminus1、A0、Aplus1、Aplus2 数组之一。
对 Aminus2、Aminus1、Aplus1、Aplus2 递归地应用相同的排序方法(注意:你不需要对 A0 进行排序,因为其中所有元素都等于 X)。
将数组连接起来以获得最终结果:A = Aminus2 + Aminus1 + A0 + Aplus1 + Aplus2。

2
在一个美好的、均等的问题分布世界中(命中-2..+2个桶),这将是一种n log^4 n的排序解决方案,而不是n log^2 n的解决方案。 - Tom Leys
1
@Tom,这是相同的复杂度,对数底数就像一个常数乘子。 - wowest
另外,您指的是以4为底的对数log_4 n,而不是log^4 n(表示n的对数的四次方)。 - ShreevatsaR
1
+1 这是一个非常优秀的解决方案,而且它具有易于实现的良好特性。 - Raymond Hettinger

1

使用Raindog修改后的快速排序似乎可以让您更快地流式传输结果,也许更快地分页。

也许这些功能已经可以通过精心控制的qsort操作实现?我还没有仔细考虑过。

这听起来有点像基数排序,只不过不是查看每个数字(或其他类型的桶规则),而是从丰富的比较中创建桶。我很难想象在可用丰富的比较但数字(或类似数字)不可用的情况下的案例。


1
我所考虑的特定应用程序是,人类实际上(主观地)提供成对比较。 - James Tauber
1
一个有趣的应用。理论上,您正在尝试将比较次数减少到可能的最小值。 - Tom Leys
汤姆,是的,请减少比较次数,以换取近似排序。 - James Tauber

1
我想不出任何情况下这会真正有用。即使我能,我怀疑排序模糊值所需的额外CPU周期将超过你提到的“额外比较”。但我仍然会提供一个建议。
考虑一下这种可能性(所有字符串都使用27个字符a-z和_):
            11111111112
   12345678901234567890
1/ now_is_the_time
2/ now_is_never
3/ now_we_have_to_go
4/ aaa
5/ ___

显然,字符串1和2比1和3更相似,而且比1和4相似得多。

一种方法是为每个相同的字符位置缩放差异值,并使用第一个不同的字符设置最后位置。

暂时忽略符号,将字符串1与2进行比较,在位置8处,它们之间存在'n'-'t'的差异。这是6的差异。为了将其转换为单个数字1-9,我们使用以下公式:

digit = ceiling(9 * abs(diff) / 27)

由于最大差值为26。最小差值1变成数字1。最大差值26变成数字9。我们的差值6变成3。

因为差异在位置8,所以我们的比较函数将返回3x10-8(实际上它会返回负数,因为字符串1在字符串2之后)。

对于字符串1和4,使用类似的过程,比较函数返回-5x10-1。最高可能的返回值(字符串4和5)在位置1有一个'-' - 'a'(26)的差异,生成数字9,因此给出了9x10-1

采用这些建议,并根据您的需要使用它们。我很想知道您的模糊比较代码最终的效果如何。


1

考虑到你想基于人类比较来订购一些物品,你可能想像体育锦标赛一样解决这个问题。你可以允许每个人的投票将胜者的分数增加3分,并将败者减少3分,+2和-2、+1和-1或只是0 0平局。

然后,你可以按照得分进行常规排序。

另一个选择是单淘汰赛或双淘汰赛结构。


我考虑过先进行近似排序,作为种子赛制的一种方式。 - James Tauber

0

你可以使用两个比较来实现这个目的。将更重要的比较乘以2,然后将它们相加。

以下是一个在Perl中的例子。它通过第一个元素和第二个元素比较两个数组引用。

use strict;
use warnings;
use 5.010;

my @array = (
  [a => 2],
  [b => 1],
  [a => 1],
  [c => 0]
);

say "$_->[0] => $_->[1]" for sort {
  ($a->[0] cmp $b->[0]) * 2 +
  ($a->[1] <=> $b->[1]);
} @array;
a => 1
a => 2
b => 1
c => 0

你可以很容易地将其扩展到任意数量的比较。


0

也许这样做有一个很好的原因,但我认为在任何情况下都不如其他选择,并且对于一般情况肯定不好。原因是什么?除非您了解输入数据的领域和值分布,否则实际上无法改进,例如快速排序。如果您确实了解这些内容,则通常还有更有效的方法。

反例:假设您的比较返回“巨大差异”的值,以表示数字相差超过1000,而输入是{0,10000,20000,30000,...}

反例:与上述相同,但输入为{0,10000,10001,10002,20000,20001,...}

但是,您说,我知道我的输入不是那样的!好吧,在这种情况下,请详细告诉我们您的输入真正是什么样子。然后有人可能会真正地帮助您。

例如,有一次我需要对历史数据进行排序。数据已经被排序好了。当新数据被添加时,它会被附加到列表中,然后再次运行列表。我不知道新数据被附加到哪里。为了解决这种情况,我设计了一个混合排序算法,它比快速排序和其他算法更有效,因为它选择了一种在已排序数据上快速的排序算法,并将其调整为快速排序(基本上是切换到快速排序)当遇到未排序数据时。
唯一能够提高通用排序算法性能的方法就是了解您的数据。如果您想得到答案,您必须在这里清楚地传达您的需求。

任务是让人以成对的方式主观地表达他们对集合中物品的偏好,以便能够根据个人的喜好近似排序该集合。 - James Tauber

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接