【问题标题】:Split array into sub-arrays based on value根据值将数组拆分为子数组
【发布时间】:2011-06-15 13:55:27
【问题描述】:

我在 Ruby Core 中寻找一个等效的数组 String#split,但惊讶地发现它不存在。有没有比以下更优雅的方法来根据值将数组拆分为子数组?

class Array
  def split( split_on=nil )
    inject([[]]) do |a,v|
      a.tap{
        if block_given? ? yield(v) : v==split_on
          a << []
        else
          a.last << v
        end
      }
    end.tap{ |a| a.pop if a.last.empty? }
  end
end

p (1..9 ).to_a.split{ |i| i%3==0 },
  (1..10).to_a.split{ |i| i%3==0 }
#=> [[1, 2], [4, 5], [7, 8]]
#=> [[1, 2], [4, 5], [7, 8], [10]]

编辑:对于那些感兴趣的人,引发此请求的“现实世界”问题可以在this answer 中看到,我在下面使用@fd 的答案来实现。

【问题讨论】:

  • 嗯,在 Python 中,您可以将其转换为字符串(以逗号或其他内容分隔的值),将其拆分,然后返回列表。不知道这是否是 Ruby 中的一个选项。
  • @Rafe 会的,但前提是内容只是字符串。即便如此,这也很难被认为是优雅的。 :p
  • @Phrogz 如果它们是数字,它也可以正常工作。您只需执行','.join([str(x) for x in list_of_nums]),然后拆分任何内容,然后重新加入并以逗号拆分。实用,是的,优雅的,嗯,不。
  • @Rafe 也许我也应该接受大多数迂回黑客的答案。到/从 YAML,有人吗? :)
  • 仅供参考:我在您的解决方案中看不到任何需要 self 成为 Array 的内容。您可以将该方法拉入Enumerable,因为您只依赖self 响应inject。 (顺便说一句,这也可以让您摆脱两个测试用例中的to_a。)

标签: ruby arrays


【解决方案1】:

有时partition 是做类似事情的好方法:

(1..6).partition { |v| v.even? } 
#=> [[2, 4, 6], [1, 3, 5]]

【讨论】:

  • 与问题无关:作者想要拆分分隔的运行序列。
【解决方案2】:

我试着打了一点高尔夫球,但仍然不是一个单一的方法:

(1..9).chunk{|i|i%3==0}.reject{|sep,ans| sep}.map{|sep,ans| ans}

或者更快:

(1..9).chunk{|i|i%3==0 || nil}.map{|sep,ans| sep&&ans}.compact

另外,Enumerable#chunk 似乎是 Ruby 1.9+,但它非常接近您想要的。

例如,原始输出将是:

(1..9).chunk{ |i|i%3==0 }.to_a                                       
=> [[false, [1, 2]], [true, [3]], [false, [4, 5]], [true, [6]], [false, [7, 8]], [true, [9]]]

to_a 是为了让 irb 打印一些漂亮的东西,因为chunk 给你一个枚举器而不是一个数组)


编辑:请注意,上述优雅的解决方案比最快的实现慢 2-3 倍:

module Enumerable
  def split_by
    result = [a=[]]
    each{ |o| yield(o) ? (result << a=[]) : (a << o) }
    result.pop if a.empty?
    result
  end
end

【讨论】:

  • 不错!我以前没见过chunk。作为记录,它是 1.9.2+,但我完全可以接受。
  • 毫不奇怪(由于拒绝/映射需要额外的迭代)块慢了一点;我添加了一个基准测试“答案”收集实现。
  • (1..10).chunk{|n| n % 3 == 0 ? :_separator : :keep}.map{|_,v| v}
  • (1..10).chuck{|n| n%3==0 || nil}.map{|_,v| v}
【解决方案3】:

这里是汇总答案的基准(我不会接受这个答案):

require 'benchmark'
a = *(1..5000); N = 1000
Benchmark.bmbm do |x|
  %w[ split_with_inject split_with_inject_no_tap split_with_each
      split_with_chunk split_with_chunk2 split_with_chunk3 ].each do |method|
    x.report( method ){ N.times{ a.send(method){ |i| i%3==0 || i%5==0 } } }
  end
end
#=>                                user     system      total        real
#=> split_with_inject          1.857000   0.015000   1.872000 (  1.879188)
#=> split_with_inject_no_tap   1.357000   0.000000   1.357000 (  1.353135)
#=> split_with_each            1.123000   0.000000   1.123000 (  1.123113)
#=> split_with_chunk           3.962000   0.000000   3.962000 (  3.984398)
#=> split_with_chunk2          3.682000   0.000000   3.682000 (  3.687369)
#=> split_with_chunk3          2.278000   0.000000   2.278000 (  2.281228)

正在测试的实现(在 Ruby 1.9.2 上):

class Array
  def split_with_inject
    inject([[]]) do |a,v|
      a.tap{ yield(v) ? (a << []) : (a.last << v) }
    end.tap{ |a| a.pop if a.last.empty? }
  end

  def split_with_inject_no_tap
    result = inject([[]]) do |a,v|
      yield(v) ? (a << []) : (a.last << v)
      a
    end
    result.pop if result.last.empty?
    result
  end

  def split_with_each
    result = [a=[]]
    each{ |o| yield(o) ? (result << a=[]) : (a << o) }
    result.pop if a.empty?
    result
  end

  def split_with_chunk
    chunk{ |o| !!yield(o) }.reject{ |b,a| b }.map{ |b,a| a }
  end

  def split_with_chunk2
    chunk{ |o| !!yield(o) }.map{ |b,a| b ? nil : a }.compact
  end

  def split_with_chunk3
    chunk{ |o| yield(o) || nil }.map{ |b,a| b && a }.compact
  end
end

【讨论】:

  • 派对有点晚了,但是:这些方法并不完全可比,因为这些方法的结果并不完全相同。前三个返回类似于String#split 返回的内容(包括找到两个后续分隔符时的空数组),而split_with_chunksplit_with_chunk2 从不返回空数组,而split_with_chunk3 仍然包含块的“分组”值.
【解决方案4】:

这是另一个(将其与最快的split_with_each 此处https://stackoverflow.com/a/4801483/410102 进行比较的基准测试):

require 'benchmark'

class Array
  def split_with_each
    result = [a=[]]
    each{ |o| yield(o) ? (result << a=[]) : (a << o) }
    result.pop if a.empty?
    result
  end

  def split_with_each_2
    u, v = [], []
    each{ |x| (yield x) ? (u << x) : (v << x) }
    [u, v]
  end
end

a = *(1..5000); N = 1000
Benchmark.bmbm do |x|
  %w[ split_with_each split_with_each_2 ].each do |method|
    x.report( method ){ N.times{ a.send(method){ |i| i%3==0 || i%5==0 } } }
  end
end

                        user     system      total        real
split_with_each     2.730000   0.000000   2.730000 (  2.742135)
split_with_each_2   2.270000   0.040000   2.310000 (  2.309600)

【讨论】:

  • 这就像 Array#partition,而不是 String#split。
【解决方案5】:

您可能要考虑的其他Enumerable 方法是each_sliceeach_cons

我不知道你希望它有多笼统,这是一种方法

>> (1..9).each_slice(3) {|a| p a.size>1?a[0..-2]:a}
[1, 2]
[4, 5]
[7, 8]
=> nil
>> (1..10).each_slice(3) {|a| p a.size>1?a[0..-2]:a}
[1, 2]
[4, 5]
[7, 8]
[10]

【讨论】:

  • 仅适用于我的特定 mod 3 示例,但不是一般情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-04
  • 2021-01-27
  • 1970-01-01
相关资源
最近更新 更多