从红宝石中的独家Range获得最大价值的最快方法

时间:2010-02-18 07:56:31

标签: ruby optimization performance range

好的,所以说你在红宝石中有一个非常大的范围。我想找到一种方法来获得范围中的最大值。

范围是独占的(用三个点定义)意味着它不包括结果中的结束对象。它可以由Integer,String,Time或任何响应#<=>#succ的对象组成。 (这是Range中开始/结束对象的唯一要求)

以下是专属范围的示例:

  past  = Time.local(2010, 1, 1, 0, 0, 0)
  now   = Time.now
  range = past...now

  range.include?(now)  # => false

现在我知道我可以做这样的事情来获得最大值:

  range.max  # => returns 1 second before "now" using Enumerable#max

但这需要花费很多时间来执行。我也知道我可以从最终对象中减去1秒。但是,该对象可能不是Time,它甚至可能不支持#-。我更愿意找到一个有效的通用解决方案,但我愿意将特殊案例代码与一般解决方案的后备结合起来(稍后将详细介绍)。

如上所述,使用Range#last也不会有效,因为它是一个独占范围,并且不包括结果中的最后一个值。

我能想到的最快的方法是:

  max = nil
  range.each { |value| max = value }

  # max now contains nil if the range is empty, or the max value

这类似于Enumerable#max所做的(Range继承),除了它利用了每个值将大于前一个的事实,因此我们可以跳过使用#<=>进行比较每个值与前一个(Range#max的方式)节省了一点点时间。

我想到的另一种方法是为常见的ruby类型(如Integer,String,Time,Date,DateTime)提供特殊的案例代码,然后使用上面的代码作为后备。它有点难看,但遇到这些对象类型时效率可能更高,因为我可以使用Range#last的减法来获得最大值而无需任何迭代。

有人能想到比这更有效/更快的方法吗?

3 个答案:

答案 0 :(得分:8)

我能想到的最简单的解决方案,适用于包容性和独家范围:

range.max

其他一些可能的解决方案:

range.entries.last
range.entries[-1]

这些解决方案都是O(n),对于大范围来说非常慢。问题原则上是Ruby中的范围值是从所有值迭代地使用succ方法枚举的,从头开始。元素必须实现返回先前值的方法(即pred)。

最快的方法是找到最后一项的前身(O(1)解决方案):

range.exclude_end? ? range.last.pred : range.last

对于包含实现pred的元素的范围,这仅适用于 。更高版本的Ruby为整数实现pred。如果它不存在,你必须自己添加方法(基本上等同于你建议的特殊情况代码,但实现起来稍微简单)。

一些快速基准测试表明,对于大范围(在这种情况下为range = 1...1000000),最后一种方法是最快的数量级,因为它是O(1):

                                          user     system      total        real
r.entries.last                       11.760000   0.880000  12.640000 ( 12.963178)
r.entries[-1]                        11.650000   0.800000  12.450000 ( 12.627440)
last = nil; r.each { |v| last = v }  20.750000   0.020000  20.770000 ( 20.910416)
r.max                                17.590000   0.010000  17.600000 ( 17.633006)
r.exclude_end? ? r.last.pred : r.last 0.000000   0.000000   0.000000 (  0.000062)

Benchmark code is here

在评论中建议使用range.last - (range.exclude_end? ? 1 : 0)。它适用于没有其他方法的日期,但永远不会用于非数字范围。 String#-不存在,并且对整数参数没有意义。但是,String#pred can be implented

答案 1 :(得分:1)

我不确定速度(并且初始测试看起来不会非常快),但以下可能会满足您的需求:

past  = Time.local(2010, 1, 1, 0, 0, 0)
now   = Time.now
range = past...now

range.to_a[-1]

非常基本的测试(在我脑海中计算)表明,你提供的方法需要大约4秒,大约需要5秒。希望这会有所帮助。

编辑1:删除了第二个解决方案,因为它完全错误。

答案 2 :(得分:1)

我不认为有任何方法可以实现这个不涉及枚举范围的方法,至少除非已经提到过,否则你有关于如何构造范围的其他信息,因此可以在没有枚举的情况下推断出所需的值。在所有建议中,我会选择#max,因为它似乎最具表现力。

require 'benchmark'
N = 20
Benchmark.bm(30) do |r|
  past, now  = Time.local(2010, 2, 1, 0, 0, 0), Time.now
  @range = past...now
  r.report("range.max") do
    N.times { last_in_range = @range.max }
  end
  r.report("explicit enumeration") do
    N.times { @range.each { |value| last_in_range = value } }
  end
  r.report("range.entries.last") do
    N.times { last_in_range = @range.entries.last }
  end
  r.report("range.to_a[-1]") do
    N.times { last_in_range = @range.to_a[-1] }
  end
end
                                user     system      total        real
range.max                      49.406000   1.515000  50.921000 ( 50.985000)
explicit enumeration           52.250000   1.719000  53.969000 ( 54.156000)
range.entries.last             53.422000   4.844000  58.266000 ( 58.390000)
range.to_a[-1]                 49.187000   5.234000  54.421000 ( 54.500000)

我注意到第3和第4选项显着增加了系统时间。我希望这与显式创建一个数组有关,这似乎是避免它们的一个很好的理由,即使它们在经过的时间里显然不是那么昂贵。