【问题标题】:Convert an array into an index hash in Ruby在 Ruby 中将数组转换为索引哈希
【发布时间】:2010-09-29 11:09:45
【问题描述】:

我有一个数组,我想做一个散列,这样我就可以快速询问“X 在数组中吗?”。

在 perl 中,有一种简单(快速)的方法:

my @array = qw( 1 2 3 );
my %hash;
@hash{@array} = undef;

这会生成一个如下所示的哈希:

{
    1 => undef,
    2 => undef,
    3 => undef,
}

我在 Ruby 中想出的最好的方法是:

array = [1, 2, 3]
hash = Hash[array.map {|x| [x, nil]}]

给出:

{1=>nil, 2=>nil, 3=>nil}

有没有更好的 Ruby 方法?

编辑 1

不,Array.include?这不是一个好主意。它的。它在 O(n) 而不是 O(1) 中进行查询。为简洁起见,我的示例数组包含三个元素;假设实际有一百万个元素。让我们做一些基准测试:

#!/usr/bin/ruby -w
require 'benchmark'

array = (1..1_000_000).to_a
hash = Hash[array.map {|x| [x, nil]}]

Benchmark.bm(15) do |x|
    x.report("Array.include?") { 1000.times { array.include?(500_000) } }
    x.report("Hash.include?") { 1000.times { hash.include?(500_000) } }
end

生产:

                     user     system      total        real
Array.include?  46.190000   0.160000  46.350000 ( 46.593477)
Hash.include?    0.000000   0.000000   0.000000 (  0.000523)

【问题讨论】:

  • 不要忘记考虑转换所需的时间。当然,如果您的情况允许,使用一组开头(如@Zach Langley 建议的那样)可以避免此成本。
  • 公平地说,上面的基准应该包括从数组转换为哈希
  • @drhenner 在理论上,当然。在实践中,不是真的——它基本上是无关紧要的。转换完成一次,查找很多很多次。当我问这个问题时,我忘记了我在做什么,但在实际程序中,在转换一次之后,查找可能已经完成了数百万次。

标签: arrays ruby hash set


【解决方案1】:

如果你想快速询问“X 在数组中吗?”你应该使用Array#include?

编辑(响应 OP 中的添加):

如果您想要快速查找时间,请使用 Set。拥有一个指向所有 nils 的哈希是愚蠢的。使用Array#to_set,转换也很简单。

require 'benchmark'
require 'set'

array = (1..1_000_000).to_a
set = array.to_set

Benchmark.bm(15) do |x|
    x.report("Array.include?") { 1000.times { array.include?(500_000) } }
    x.report("Set.include?") { 1000.times { set.include?(500_000) } }
end

我的机器上的结果:

                     user     system      total        real
Array.include?  36.200000   0.140000  36.340000 ( 36.740605)
Set.include?     0.000000   0.000000   0.000000 (  0.000515)

您应该考虑只使用一个集合,而不是一个数组,这样就不需要转换了。

【讨论】:

  • 请参阅我的(新添加的)部分,了解为什么不使用 Array#include?在问题中。
【解决方案2】:

也许我误解了这里的目标;如果你想知道 X 是否在数组中,为什么不做 array.include?("X") ?

【讨论】:

  • 请参阅我的(新添加的)部分,了解为什么不使用 Array#include?在问题中。
【解决方案3】:

如果您正在寻找与此 Perl 代码等效的代码:

grep {$_ eq $element} @array

您可以只使用简单的 Ruby 代码:

array.include?(element)

【讨论】:

  • 请参阅我的(新添加的)部分,了解为什么不包含 Array#include?在问题中。
【解决方案4】:

您创建哈希的方式看起来不错。我在 irb 有一个烂摊子,这是另一种方式

>> [1,2,3,4].inject(Hash.new) { |h,i| {i => nil}.merge(h) }
=> {1=>nil, 2=>nil, 3=>nil, 4=>nil}

【讨论】:

  • 简洁的方法,但是非常慢;在 1..1_000_000:1.4 秒(我的地图代码)与 3 分钟后无聊的等待。
  • 根据经验,这似乎在 O(n^2) 中运行
  • 呵呵。我没说它快!您始终可以使用您的 Hash[map] 方式为类 Array 创建一个 to_h 方法。
  • 使用合并!应该更快
  • array.inject(Hash.new) { |h,i| h.merge!({i => nil}) } 结果非常快,但 array.each{|e| hash[e] = nil} 仍然快 3.5 倍
【解决方案5】:

我相当肯定,没有一种一劳永逸的聪明方法来构造这个散列。我的倾向是明确说明我在做什么:

hash = {}
array.each{|x| hash[x] = nil}

它看起来不是特别优雅,但它很清晰,并且可以完成工作。

FWIW,您最初的建议(至少在 Ruby 1.8.6 下)似乎不起作用。我收到“ArgumentError:Hash 的奇数个参数”错误。 Hash.[] 需要一个字面的、偶数长度的值列表:

Hash[a, 1, b, 2] # => {a => 1, b => 2}

所以我尝试将您的代码更改为:

hash = Hash[*array.map {|x| [x, nil]}.flatten]

但表现很糟糕:

#!/usr/bin/ruby -w
require 'benchmark'

array = (1..100_000).to_a

Benchmark.bm(15) do |x|
  x.report("assignment loop") {hash = {}; array.each{|e| hash[e] = nil}}
  x.report("hash constructor") {hash = Hash[*array.map {|e| [e, nil]}.flatten]}
end

给予

                     user     system      total        real
assignment loop  0.440000   0.200000   0.640000 (  0.657287)
hash constructor  4.440000   0.250000   4.690000 (  4.758663)

除非我在这里遗漏了什么,否则简单的赋值循环似乎是构造此哈希的最清晰、最有效的方法。

【讨论】:

  • 我意识到我在代码中省略了结束 ]... 已修复。它适用于此处的 ruby​​ 1.8.7,也适用于 1.9.0
  • 对于 Ruby 1.8.6,您需要 splat,即 hash = Hash[*array.map { |x| [x, nil] }.flatten]
【解决方案6】:

我认为chrismear 关于使用分配而不是创建的观点很棒。不过,为了使整个事情更像 Ruby 风格,我可能会建议为每个元素分配除 nil 之外的 other 的东西:

hash = {}
array.each { |x| hash[x] = 1 } # or true or something else "truthy"
...
if hash[376]                   # instead of if hash.has_key?(376)
  ...
end

分配给nil 的问题是您必须使用has_key? 而不是[],因为如果Hash 没有指定的键。您可以通过使用不同的默认值来解决这个问题,但为什么还要进行额外的工作呢?

# much less elegant than above:
hash = Hash.new(42)
array.each { |x| hash[x] = nil }
...
unless hash[376]
  ...
end

【讨论】:

    【解决方案7】:

    如果您只需要成员资格,请考虑使用Set

    设置

    Set 实现了一个无序值的集合,没有 重复。这是 Array 直观的互操作的混合体 设施和哈希的快速查找。

    Set 易于与Enumerable 对象一起使用(实现 each)。大多数初始化方法和二元运算符都接受 除了集合和数组之外的通用 Enumerable 对象。一个 Enumerable 对象可以转换为 Set 使用 to_set 方法。

    Set使用Hash作为存储,所以一定要注意以下几点:

    • 元素的相等性根据Object#eql?Object#hash确定。
    • Set 假定每个元素的标识在存储时不会改变。修改集合的元素会将集合呈现为 不可靠的状态。
    • 当要存储字符串时,将存储该字符串的冻结副本,除非原始字符串已被冻结。

    比较

    比较运算符<><=>= 实现为 {proper_,}{subset?,superset?} 方法的简写。然而 <=> 运算符被故意遗漏,因为不是每一对 集是可比的。 (例如 {x,y} 与 {x,z})

    示例

    require 'set'
    s1 = Set.new [1, 2]                   # -> #<Set: {1, 2}>
    s2 = [1, 2].to_set                    # -> #<Set: {1, 2}>
    s1 == s2                              # -> true
    s1.add("foo")                         # -> #<Set: {1, 2, "foo"}>
    s1.merge([2, 6])                      # -> #<Set: {1, 2, "foo", 6}>
    s1.subset? s2                         # -> false
    s2.subset? s1                         # -> true
    

    [...]

    公共类方法

    新(枚举 = nil)

    创建一个包含给定可枚举元素的新集合 对象。

    如果给定一个块,枚举的元素由 给定块。

    【讨论】:

      【解决方案8】:

      这是一种使用哈希缓存查找的巧妙方法:

      a = (1..1000000).to_a
      h = Hash.new{|hash,key| hash[key] = true if a.include? key}
      

      它所做的几乎就是为新的哈希值创建一个默认构造函数,然后如果它在数组中,则将“true”存储在缓存中(否则为 nil)。这允许延迟加载到缓存中,以防万一您不使用每个元素。

      【讨论】:

      • 哈希加载是如此之快,这几乎没有任何意义,除非你真的内存不足。整个Hash负载是O(n),单个a.include?调用也是 O(n)。
      • 在这种情况下,是的,但这可以概括为记忆您选择的任何功能。
      【解决方案9】:

      Rampion 打败了我。设置可能是答案。

      你可以这样做:

      require 'set'
      set = array.to_set
      set.include?(x)
      

      【讨论】:

        【解决方案10】:

        到目前为止,对建议进行一些基准测试表明,chrismear 和 Gaius 的基于赋值的哈希创建比我的 map 方法稍快(并且赋值 nil 比赋值 true 稍快)。 mtyaka 和 rampion 的 Set 建议的创建速度大约慢了 35%。

        就查找而言,hash.include?(x)hash[x] 快一点点;两者的速度都是set.include?(x) 的两倍。

                        user     system      total        real
        chrismear   6.050000   0.850000   6.900000 (  6.959355)
        derobert    6.010000   1.060000   7.070000 (  7.113237)
        Gaius       6.210000   0.810000   7.020000 (  7.049815)
        mtyaka      8.750000   1.190000   9.940000 (  9.967548)
        rampion     8.700000   1.210000   9.910000 (  9.962281)
        
                        user     system      total        real
        times      10.880000   0.000000  10.880000 ( 10.921315)
        set        93.030000  17.490000 110.520000 (110.817044)
        hash-i     45.820000   8.040000  53.860000 ( 53.981141)
        hash-e     47.070000   8.280000  55.350000 ( 55.487760)
        

        基准代码是:

        #!/usr/bin/ruby -w
        require 'benchmark'
        require 'set'
        
        array = (1..5_000_000).to_a
        
        Benchmark.bmbm(10) do |bm|
            bm.report('chrismear') { hash = {}; array.each{|x| hash[x] = nil} }
            bm.report('derobert')  { hash = Hash[array.map {|x| [x, nil]}] }
            bm.report('Gaius')     { hash = {}; array.each{|x| hash[x] = true} }
            bm.report('mtyaka')    { set = array.to_set }
            bm.report('rampion')   { set = Set.new(array) }
        end
        
        hash = Hash[array.map {|x| [x, true]}]
        set = array.to_set
        array = nil
        GC.start
        
        GC.disable
        Benchmark.bmbm(10) do |bm|
            bm.report('times')  { 100_000_000.times { } }
            bm.report('set')    { 100_000_000.times { set.include?(500_000) } }
            bm.report('hash-i') { 100_000_000.times { hash.include?(500_000) } }
            bm.report('hash-e') { 100_000_000.times { hash[500_000] } }
        end
        GC.enable
        

        【讨论】:

          【解决方案11】:

          如果您不关心哈希值是什么

          irb(main):031:0> a=(1..1_000_000).to_a ; a.length
          => 1000000
          irb(main):032:0> h=Hash[a.zip a] ; h.keys.length
          => 1000000
          

          在我的桌面上花费一秒钟左右的时间。

          【讨论】:

            【解决方案12】:

            你可以做这个非常方便的技巧:

            Hash[*[1, 2, 3, 4].map {|k| [k, nil]}.flatten]
            => {1=>nil, 2=>nil, 3=>nil, 4=>nil}
            

            【讨论】:

            • 这对我很有用。我将其更改为 [k,k] 而不是 [k, nil]。感谢这篇文章
            • 为什么不直接做 Hash[ [1, 2, 3, 4].map {|k| [k, nil]} ] 呢?无需额外的 splat 和 flatten。
            【解决方案13】:

            试试这个:

            a=[1,2,3]
            Hash[a.zip]
            

            【讨论】:

            • 完美!最优雅的方式。
            • Hash[a.zip] 也返回相同的响应。
            • "这当然有效......但我认为它不是最有效的......在这种情况下你要迭代两次。当然不是长度为 2 的数组的问题,但是仍然值得注意。” @brad 来自stackoverflow.com/a/9434078/380607
            【解决方案14】:

            如果您的哈希是 [0,0,0,1,0],这将保留 0

              hash = {}
              arr.each_with_index{|el, idx| hash.merge!({(idx + 1 )=> el }) }
            

            返回:

              # {1=>0, 2=>0, 3=>0, 4=>1, 5=>0}
            

            【讨论】:

              猜你喜欢
              • 2010-12-11
              • 1970-01-01
              • 2018-03-20
              • 1970-01-01
              • 2021-01-17
              • 1970-01-01
              • 1970-01-01
              • 2021-10-16
              • 2017-07-06
              相关资源
              最近更新 更多