Ruby 的 max 函数顺序如何重复?

Posted

技术标签:

【中文标题】Ruby 的 max 函数顺序如何重复?【英文标题】:How does Ruby's max function order duplicates? 【发布时间】:2018-02-27 10:17:34 【问题描述】:

我一直在查看 Ruby 的 Enumerable mixin (v2.4.1) 中的 max method。

这是一个相当简单的方法,但是当存在重复项时它如何排序项目有点令人困惑。

例如:

x = [1,2,3,4,5,6,7,8,9]
x.max |a, b| a%2 <=> b%2
=> 1
10.times|y| p x.max(y) |a, b| a%2 <=> b%2
[]
[1]
[1, 7] # why is 7 the next element after 1?
[3, 1, 5] # why no more 7?
[7, 3, 1, 5] # 7 is now first
[9, 7, 3, 1, 5]
[9, 7, 3, 1, 5, 6]
[9, 7, 3, 1, 5, 4, 6]
[9, 7, 3, 1, 5, 2, 4, 6]
[9, 7, 5, 3, 1, 8, 6, 4, 2] # order has changed again (now seems more "natural")

7 是如何被选为第二项的?为什么取三个值时根本不选择?

如果你取更多的数字,顺序就会不一致(尽管集合中的项目)。

我看了一眼the source code,但是好像在做正常的比较;从代码中看不到这里看到的顺序。

谁能解释这种排序是如何实现的?我知道上面的排序都是“有效的”,但是它们是如何生成的呢?

【问题讨论】:

max 传入的块使用了2个变量,可以用来比较对象;你应该检查|a, b| (a%2) &lt;=&gt; (b%2) 或类似的东西。另外请不要使用输出截图 - 将实际输出复制/粘贴到代码块中 我的猜测是它比较项目的顺序不能保证? 你的排序关系真奇怪。基本上,所有奇数都大于所有偶数,但所有偶数和所有奇数都相等。由于1、3、5、7、9都相等,都是最大值,因此任意任意排列都是“最大值”。 @JörgWMittag ...您没有抓住重点。任何排列都是有效的,但为什么要选择特定的排列?什么实施引起了它?这种排列很“奇怪”,因为很难看出它是如何形成的。 也许这应该用C标记,因为它本质上是一个关于用该语言编码的算法的问题。 【参考方案1】:

您的示例可以通过使用 max_by 来简化以产生类似的结果:

10.times|y| p x.max_by(y) |t| t%2

我花了一些时间研究源代码,但找不到任何漏洞。

在我记得看到一个名为 Switch: A Deep Embedding of Queries into Ruby(Manuel Mayr 的论文)的出版物后,我找到了答案。

您可以在第 104 页找到max_by 的答案:

...这里,输入列表中的值在以下情况下假定为最大值 返回由函数评估的。如果产生多个值 最大值,在这些值中选择结果是任意的。 ...

类似的:sort & sort_by 来自 cmets @emu.c

结果不保证稳定。当两个键相等时, 对应元素的顺序是不可预测的。

第一,第二次编辑 - “我们需要更深入” => 我希望你会喜欢“骑行”。

简短的回答: 排序看起来像这样的原因是 max_by 块的组合(导致使用来自%2max 值开始排序,即1 然后继续使用0)和qsort_r(BSD快速排序)实现@ruby。

长答案: 全部基于 ruby​​ 2.4.2 或当前 2.5.0 的源代码(目前正在开发中)。

快速排序算法可能因您使用的编译器而异。您可以使用 qsort_r:GNU 版本、BSD 版本(您可以查看configure.ac)了解更多信息。 Visual Studio 使用的是 2012 或更高版本的 BSD 版本。

+Tue Sep 15 12:44:32 2015  Nobuyoshi Nakada  <nobu@ruby-lang.org>
+
+   * util.c (ruby_qsort): use BSD-style qsort_r if available.

Thu May 12 00:18:19 2016  NAKAMURA Usaku  <usa@ruby-lang.org>

    * win32/Makefile.sub (HAVE_QSORT_S): use qsort_s only for Visual Studio
      2012 or later, because VS2010 seems to causes a SEGV in
test/ruby/test_enum.rb.

    如果你有 GNU qsort_r 但没有 BSD: 仅使用内部 ruby​​_qsort 实现。检查util.c,了解 Tomoyuki Kawamura 的快速排序 (ruby_qsort(void* base, const size_t nel, const size_t size, cmpfunc_t *cmp, void *d)) 功能的内部实现。 @util.h

    如果 HAVE_GNU_QSORT_R=1 那么#define ruby_qsort qsort_r

    #ifdef HAVE_GNU_QSORT_R
    #define ruby_qsort qsort_r
    #else    void ruby_qsort(void *, const size_t, const size_t,
        int (*)(const void *, const void *, void *), void *);
    #endif
    

    如果检测到 BSD 样式: 然后使用下面的代码(可以在util.c 找到)。注意cmp_bsd_qsortruby_qsort 之前是如何被调用的。原因?可能是标准化、堆栈空间和速度(我自己没有测试 - 必须创建基准,这非常耗时)。

节省堆栈空间在BSD qsort.c源代码中注明:

    /*
    * To save stack space we sort the smaller side of the partition first
    * using recursion and eliminate tail recursion for the larger side.
    */

ruby 源代码处的 BSD 分支:

     #if defined HAVE_BSD_QSORT_R
    typedef int (cmpfunc_t)(const void*, const void*, void*);

    struct bsd_qsort_r_args 
        cmpfunc_t *cmp;
        void *arg;
    ;

    static int
    cmp_bsd_qsort(void *d, const void *a, const void *b)
    
        const struct bsd_qsort_r_args *args = d;
        return (*args->cmp)(a, b, args->arg);
    

    void
    ruby_qsort(void* base, const size_t nel, const size_t size, cmpfunc_t *cmp, void *d)
    
        struct bsd_qsort_r_args args;
        args.cmp = cmp;
        args.arg = d;
        qsort_r(base, nel, size, &args, cmp_bsd_qsort);
    

如果您使用 MSYS2 在 Windows 上编译您的 ruby​​(不再使用 DevKit,而是用于 Windows 安装程序的 MSYS2,我大部分时间都在使用)NetBSD 版本的 qsort_r(从 02-07-2012 开始提供)。最新的NetBSDqsort.c (revision:1.23)。

现在来看现实生活中的例子 - “我们需要更深入”

测试将在两个(Windows)红宝石上执行:

第一个 ruby​​:将基于 DevKit 版本 2.2.2p95(于 2015 年 4 月 13 日发布)并且不包含 BSD qsort 实现。

第二个 ruby​​:将基于 MSYS2 tool-chain 版本 ruby​​ 2.4.2-p198(于 2017 年 9 月 15 日发布)并包含用于 BSD qsort 实现的补丁(见上文)。

代码:

x=[1,2,3,4,5,6,7,8,9]
10.times|y| p x.max_by(y) |t| t%2

鲁比2.2.2p95:

The result:
[]
[5]
[7, 1]
[3, 1, 5]
[7, 3, 1, 5]
[9, 7, 3, 1, 5]
[5, 9, 1, 3, 7, 6]
[5, 1, 9, 3, 7, 6, 4]
[5, 1, 3, 7, 9, 6, 4, 2]
[9, 1, 7, 3, 5, 4, 6, 8, 2]

鲁比2.4.2-p198:

The result:
[]
[1]
[7, 1]
[5, 3, 1]
[5, 7, 3, 1]
[5, 9, 7, 3, 1]
[5, 1, 9, 7, 3, 6]
[5, 1, 3, 9, 7, 4, 6]
[5, 1, 3, 7, 9, 2, 6, 4]
[9, 1, 3, 7, 5, 8, 4, 6, 2]

现在换成不同的xx=[7,9,3,4,2,6,1,8,5]

鲁比2.2.2p95:

The result:
[]
[1]
[9, 7]
[1, 7, 3]
[5, 1, 7, 3]
[5, 1, 3, 9, 7]
[7, 5, 9, 3, 1, 2]
[7, 9, 5, 3, 1, 2, 4]
[7, 9, 3, 1, 5, 2, 4, 8]
[5, 9, 1, 3, 7, 4, 6, 8, 2]

鲁比2.4.2-p198:

The result:
[]
[9]
[9, 7]
[3, 1, 7]
[3, 5, 1, 7]
[7, 5, 1, 3, 9]
[7, 9, 5, 1, 3, 2]
[7, 9, 3, 5, 1, 4, 2]
[7, 9, 3, 1, 5, 8, 2, 4]
[5, 9, 3, 1, 7, 2, 4, 6, 8]

现在对于源数组中的相同项目(qsort 不稳定,见下文): x=[1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9]

使用以下代码处理它: 12.times|y| p x.max_by(y) |t| t%2

鲁比2.2.2p95:

The result:
[]
[3]
[1, 1]
[9, 1, 7]
[3, 9, 1, 7]
[5, 3, 9, 1, 7]
[1, 5, 3, 9, 1, 7]
[5, 9, 3, 7, 1, 1, 1]
[1, 5, 9, 1, 7, 1, 3, 4]
[1, 1, 5, 9, 1, 7, 3, 4, 2]
[1, 1, 1, 5, 7, 3, 9, 4, 2, 8]
[9, 1, 7, 1, 5, 3, 1, 2, 6, 8, 4]

鲁比2.4.2-p198:

The Result:
[]
[1]
[1, 1]
[7, 9, 1]
[7, 3, 9, 1]
[7, 5, 3, 9, 1]
[7, 1, 5, 3, 9, 1]
[1, 5, 9, 3, 7, 1, 1]
[1, 1, 5, 9, 3, 7, 1, 4]
[1, 1, 1, 5, 9, 3, 7, 2, 4]
[1, 7, 3, 1, 5, 9, 1, 2, 4, 8]
[9, 3, 1, 7, 1, 5, 1, 2, 8, 6, 4]

现在是个大问题 --> 为什么结果不同?

第一个明显的答案是,当使用 GNU 或 BSD 实现时,结果会有所不同?正确的?那么实现是不同的,但是产生(检查链接的实现以获取详细信息)相同的结果。问题的核心在别处。

这里真正的问题是算法本身。当使用快速排序时,你得到的是不稳定的排序(当你比较两个相等的值时,它们的顺序不会保持不变)。当您拥有 [1,2,3,4,5,6,7,8,9] 后,您将其在块中转换为 [1,0,1,0,1,0,1,0,1]使用 max(_by) 将数组排序为 [1,1,1,1,1,0,0,0,0]。你从 1 开始,但是哪一个呢?那么你会得到不可预测的结果。 (max(_by) 是先得到奇数再得到偶数的原因)。

见GNU qsort评论:

警告:如果两个对象比较相等,排序后的顺序是 不可预测的。也就是说,排序是不稳定的。这个可以 当比较只考虑部分 元素。具有相同排序键的两个元素在其他元素中可能不同 尊重。

现在像引擎一样对其进行排序:

[1,2,3,4,5,6,7,8,9] -> 首先考虑的是奇数[1,3,5,7,9],那些被认为与max_by|t| t%2 相等的数产生[1,1,1,1,1]

结论:

现在该拿哪一个?那么在你的情况下它是不可预测的,它是你得到的。即使对于相同的 ruby​​ 版本,我也会得到不同的结果,因为底层 quick-sort 算法本质上是不稳定的。

【讨论】:

@River:我同意所以我改进了一点答案。 这太棒了,我本来打算自己做类似的事情,但一直没有解决。 @river:我很高兴你喜欢它。这比我预期的要难。一路上有很多宏。我什至遇到过一些错误的 cmets 和黑客攻击。

以上是关于Ruby 的 max 函数顺序如何重复?的主要内容,如果未能解决你的问题,请参考以下文章

ruby - 如何从字符串数组中生成可能的字母顺序组合?

Ruby 异常处理:反向堆栈跟踪顺序

如何按顺序执行承诺并返回所有结果[重复]

mysql按顺序生成一个不重复的id

如何在没有顺序重复的情况下获得 PHP 中的所有排列?

您如何在 Ruby 应用程序中以树的形式跟踪“require”的完整序列和顺序?