如何使用Ruby在非连续范围内生成随机数?

时间:2010-12-23 22:03:07

标签: ruby

例如,假设我有以下范围

5031..5032 6248..6249

如何在这两个范围内生成随机数?

6 个答案:

答案 0 :(得分:5)

简单的方法:

(r1.to_a + r2.to_a).choice

更快,更节省内存的通用解决方案将涉及计算总范围大小,生成随机数,然后将数字标准化到它所属的范围。

更新:好的,明白了。此解决方案适用于任意数量的范围,并且生成巨型数组或迭代通过范围本身。 (它在 Ranges 数组上迭代两次,而不是Range元素数组。)

def rangerand *r
  r.inject(rand(
      r.inject(0) do |accum, rng|
        accum + rng.last - rng.first + 1
      end
    )) do |accum, rng|
      rngsize = rng.last - rng.first + 1
      return rng.first + accum if accum < rngsize
      accum - rngsize
    end
  # raise 'this "cannot happen"'
end

puts rangerand 1..3, 999..1001, 10_200..10_205

嘿,我之前从未在同一个表达中使用过两个#inject

答案 1 :(得分:4)

对于任意数量的范围

  1. 将可能值的数量计为total
  2. rnd0之间生成随机数total - 1
  3. 遍历范围列表,将范围大小从rnd减去可能的范围(直到rnd为非负数)。
  4. 如果无法减去更多,只需获取rnd - 当前范围内的数字。

答案 2 :(得分:3)

我编写了这段代码,现在看看Nikita的答案,我认为这或多或少是他的想法(我使用r.end / r.begin,因为Ruby范围没有预先计算的长度属性)

ranges = [1..10, 21..40, 81..120]

counts = ranges.map { |r| r.end - r.begin + (r.exclude_end? ? 0 : 1) }
random = rand(counts.inject(&:+))    
idx, remainder = counts.inject([0, random]) do |(idx, remainder), count|
  if remainder < count
    [idx, remainder] # break here if you care more for efficiency than for functional purity
  else
    [idx+1, remainder - count]
  end
end
p ranges[idx].begin + remainder

当然,如果我的合理长度的范围,我只是使用已经讨论过的其他解决方案。

答案 3 :(得分:1)

将范围添加到数组并将其展平以获得一系列数字,然后调用rand以查找此数组的特定元素。

range_1 = 1..10
range_2 = 2..30

range_array = [range_1.to_a, range_2.to_a].flatten
range_array[rand(range_array.length)]

答案 4 :(得分:1)

这就是我提出的:

require 'benchmark'

class RangeRandom
  def initialize(ranges=[])

    # sanity checks need to go here for things like:
    #   backward/inverted ranges: (0..-1)
    #   range overlaps: (0..2 & 1..3)
    #     could combine ranges, or raise an exception
    #
    # Restrict to ranges that pass ".inclusive?"?

    # the end value is informative only
    @prepped_ranges = ranges.map{ |r| [ r.begin, r.end - r.begin, r.end ] }
  end

  def rand
    range = @prepped_ranges[ Kernel::rand( @prepped_ranges.size ) ]
    range[0] + Kernel::rand( range[1] )
  end
end

一些测试:

range_random = RangeRandom.new([0..10, 90..100])
5.times do
  puts range_random.rand
end
puts

# >> 94
# >> 97
# >> 92
# >> 92
# >> 8

n = 500_000
Benchmark.bm(7) do |x|
  x.report('rand1:') { n.times do ; r = RangeRandom.new([ 0..1             ]); r.rand; end }
  x.report('rand2:') { n.times do ; r = RangeRandom.new([ 0..1_000_000     ]); r.rand; end }
  x.report('rand3:') { n.times do ; r = RangeRandom.new([ 0..1_000_000_000 ]); r.rand; end }

  x.report('rand4:') { n.times do ; r = RangeRandom.new([ 0..1 ]); r.rand; end }
  x.report('rand5:') { n.times do ; r = RangeRandom.new([ 0..1, 2..3, 4..5, 6..7 ]); r.rand; end }

  x.report('rand6:') { r = RangeRandom.new([ 0..1             ]) ; n.times do ; r.rand; end }
  x.report('rand7:') { r = RangeRandom.new([ 0..1_000_000_000 ]) ; n.times do ; r.rand; end }
end

500,000次迭代的总运行时间:

# >>              user     system      total        real
# >> rand1:   2.220000   0.000000   2.220000 (  2.224894)
# >> rand2:   2.250000   0.010000   2.260000 (  2.254730)
# >> rand3:   2.250000   0.000000   2.250000 (  2.247406)

初始化500,000次迭代的多个范围的时间:

# >> rand4:   2.220000   0.000000   2.220000 (  2.222983)
# >> rand5:   4.340000   0.000000   4.340000 (  4.337312)

单次初始化的时间,然后是500,000次迭代的小范围与大范围:

# >> rand6:   0.560000   0.000000   0.560000 (  0.559673)
# >> rand7:   0.580000   0.000000   0.580000 (  0.584331)

使用大范围时,时间会略微增加,但可能会因系统活动而出现变化。初始化数组中的多个范围会产生更大的影响,这正是我所期望的。

答案 5 :(得分:0)

对于宽范围 range.to_a 不是一个很好的解决方案。您可以创建不必要的许多大数组。最快的解决方案可能如下所示:

ruby-1.9.2-head > from, to = 1, 50000000
 => [1, 50000000] 
ruby-1.9.2-head > from + rand(to-from+1) 
 => 20698596 
ruby-1.9.2-head > from + rand(to-from+1) 
 => 15143263 
ruby-1.9.2-head > from + rand(to-from+1) 
 => 18469491 

Ranges的类似解决方案几乎与 range.to_a 一样慢:

ruby-1.9.2-head > range1 = 1..5000000
 => 50..50000 
ruby-1.9.2-head > range1.begin + rand(range1.count)   # TAKES ABOUT 1 SECOND!
 => 1788170