【问题标题】:What is the run time of shift/unshift in a ruby arrayruby 数组中 shift/unshift 的运行时间是多少
【发布时间】:2019-05-08 11:43:41
【问题描述】:

有谁知道 ruby​​ 数组中 shift 和 unshift 的效率如何?

从数组的开头删除并且必须移动内存中的每个元素可能会变得非常低效。我认为 ruby​​ 以其他方式做到这一点。

以下任何信息都会有所帮助:
- 算法运行时
- 实施
- 一般效率
- 将 shift/unshift 用于队列是否可以接受(在 C++ 之类的东西中,这不会)

谢谢!

【问题讨论】:

    标签: ruby big-o


    【解决方案1】:

    在旧版本的 Ruby(~2012 之前)中,unshift 是一个 O(n) 操作。但是,this commitreleased in Ruby 2.0.0 中添加了优化,这使得 unshift 摊销 O(1),这意味着它保证平均为 O(1),但单个操作可能是 O(n)。这与shift 的运行时间相同。

    This CS Stack Exchange post 很好地解释了它是如何工作的,以及你如何最终得到 O(1) 的摊销运行时间(这是关于 C++ 的 vector::push_back,但它的工作方式相同)。

    【讨论】:

      【解决方案2】:

      您可以查看here 并查看unshift 方法的C 源代码(只需单击描述块)。很清楚:如果我们还没有足够的内存容量,则增加内存容量,向前移动数组的当前内容,将传递的参数复制到内存块开头的空闲空间。所以O(n)unshift

      【讨论】:

        【解决方案3】:

        我发现回答这个问题的最简单和最明确的方法是对其进行基准测试。

        require 'benchmark'
        
        Benchmark.bm do |x|
            iterations = 10000000
            x.report("push") {
                a = []
                iterations.times do a.push(10) end
            }
            x.report("unshift") {
                a = []
                iterations.times do a.unshift(10) end
            }
            a = []
            iterations.times do a.push(10) end
            x.report("shift") {
                iterations.times do a.shift() end
            }
            a = []
            iterations.times do a.push(10) end
            x.report("pop") {
                iterations.times do a.pop() end
            }
        end
        

        在我的系统上运行 ruby​​ 版本 2.0.0 这将返回结果:

                     user     system      total        real
        push     0.880000   0.030000   0.910000 (  0.917213)
        unshift  0.920000   0.090000   1.010000 (  1.026208)
        shift    0.780000   0.030000   0.810000 (  0.810293)
        pop      0.710000   0.000000   0.710000 (  0.724865)
        

        pushpopshiftunshift 似乎都花费了大致相同的时间。

        使用 iterations 的不同值再次运行此代码会得到与我更改 iterations 的数量成比例的结果。这意味着无论iterations 的值如何,每个操作的平均时间总是相同的,这意味着每个操作的运行时间与数组的长度无关,因此具有O(1) 的运行时间。

        我会说这可以作为队列使用。

        【讨论】:

        • 抱歉,“线性缩放”意味着 O(n)。 O(1) 意味着它需要相同的时间,而与项目的数量无关。使用来自一组运行的时间信息,您无法说出算法的扩展方式。按照您的测试设置方式,操作是在一个数组上测试的,每次迭代都有不同数量的项目。这也阻止了测量它的扩展方式。相反,请测试数组中的少量项目(例如 10 个)的操作需要多长时间,以及数组中的大量项目(例如 1000)需要多长时间。重复测试并对结果取平均值。
        • 重新阅读答案后,我的评论有点不准确。编辑为时已晚,请参阅下面的更新评论。
        • 抱歉,“线性缩放”意味着 O(n)。 O(1) 表示与项目数无关的恒定时间。您提供了一组运行的时间信息,因此我们无法看到算法的扩展方式。测量数组中少量项目(例如 10 个)的操作需要多长时间以及数组中的大量项目(例如 1000)需要多长时间会更清楚。重复测试并平均结果。如果操作在小型和大型阵列上花费的时间大致相同,那么它是 O(1)。如果大数组需要大约 100 倍的时间(线性),那么它是 O(n)。
        • @Tony,我说的是,“当我更改 iterations 时,它们似乎都是线性缩放的”,这意味着当我尝试将代码中 iterations 的值加倍时,所有时间结果也翻了一番。我想我应该更清楚,并将我的结果包含在不同的迭代值中。我想我没有这样做是因为我不想让评论变得混乱(我不记得我两年前的推理了)。但我确实相信这是测试每个操作的运行时间的有效方法。 (在下一条评论中继续)
        • 如果总时间与执行操作的次数成线性关系,那一定意味着操作在恒定时间内运行。例如,如果我以迭代次数 = 10 运行此测试,并且推送测试需要 10 毫秒,这意味着每次推送平均需要 1 毫秒。如果我然后以迭代次数 = 100 运行此测试并且推送测试花费了 100 毫秒,这意味着每次再次推送平均需要 1 毫秒。这意味着push 的运行时间相对于数组的长度是恒定的,因为数组的平均长度随迭代线性缩放。
        【解决方案4】:

        根据this article,它似乎并没有真正改变,只是增加一个指针并返回它。所以就效率而言,它的效率非常高(O(1))。然而,文章提到了一个潜在的内存泄漏,这在最近的版本中可能存在也可能不存在。

        【讨论】:

        • 在 1.9.3 中,shift 方法的实现变得有点复杂,在某些情况下它确实会移动内存。它与使用的指针类型有关,但我不太确定这个ARY_SHARED_P(ary) 何时为真,何时不是。
        猜你喜欢
        • 2014-08-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-25
        • 2019-06-22
        • 1970-01-01
        • 2013-10-07
        • 1970-01-01
        相关资源
        最近更新 更多