在Ruby中,我如何从一个数组创建一个哈希表?

91

我有一个简单的数组:

arr = ["apples", "bananas", "coconuts", "watermelons"]

我还有一个函数f,它会对单个字符串输入执行操作并返回值。这个操作非常耗费时间,所以我想在哈希表中缓存结果。

我知道可以用类似以下的代码生成所需的哈希表:

h = {}
arr.each { |a| h[a] = f(a) }

我想做的是不必初始化h,这样我就可以像这样写:

h = arr.(???) { |a| a => f(a) }

可以这样做吗?

8个回答

142

假设你有一个函数,其函数名非常fantastic: "f"

def f(fruit)
   fruit + "!"
end

arr = ["apples", "bananas", "coconuts", "watermelons"]
h = Hash[ *arr.collect { |v| [ v, f(v) ] }.flatten ]

会给你:

{"watermelons"=>"watermelons!", "bananas"=>"bananas!", "apples"=>"apples!", "coconuts"=>"coconuts!"}

更新:

如评论中所提到的,Ruby 1.8.7引入了一个更好的语法来表示这个:

h = Hash[arr.collect { |v| [v, f(v)] }]

我认为你的意思是 ... { |v| [v, f(v)] },但这个也能解决问题! - Wizzlewott
4
只有一个问题 - 为什么 *arr.collect 旁边会有一个 * 符号? - Jeriko
4
@splat运算符*可以根据上下文将列表收集到数组中或将数组展开为列表。这里它把数组展开成了一个列表(用作新哈希的项)。 - Telemachus
2
在查看了Jörg的答案并进一步思考后,请注意您可以删除*flatten以获得更简单的版本:h = Hash [arr.collect {|v| [v,f(v)]}]。但是我不确定是否有我没有注意到的问题。 - Telemachus
3
在 Ruby 1.8.7 版本中,丑陋的 Hash[*key_pairs.flatten] 可以简写为 Hash[key_pairs]。这种写法更加美观,如果你还没有从 1.8.6 更新过来,可以使用 require 'backports' - Marc-André Lafortune
显示剩余3条评论

61

我对给出的答案进行了快速、简单的基准测试。(这些结果可能与你的Ruby版本、奇怪的缓存等不完全相同,但总体结果将是类似的。)

arr 是一组 ActiveRecord 对象。

Benchmark.measure {
    100000.times {
        Hash[arr.map{ |a| [a.id, a] }]
    }
}

基准测试 @real=0.860651,@cstime=0.0,@cutime=0.0,@stime=0.0,@utime=0.8500000000000005,@total=0.8500000000000005

Benchmark.measure { 
    100000.times {
        h = Hash[arr.collect { |v| [v.id, v] }]
    }
}

基准测试 @real=0.74612,@cstime=0.0,@cutime=0.0,@stime=0.010000000000000009,@utime=0.740000000000002,@total=0.750000000000002

Benchmark.measure {
    100000.times {
        hash = {}
        arr.each { |a| hash[a.id] = a }
    }
}

基准测试 @real=0.627355,@cstime=0.0,@cutime=0.0,@stime=0.010000000000000009,@utime=0.6199999999999974,@total=0.6299999999999975

Benchmark.measure {
    100000.times {
        arr.each_with_object({}) { |v, h| h[v.id] = v }
    }
}

基准测试结果 @real=1.650568, @cstime=0.0, @cutime=0.0, @stime=0.12999999999999998, @utime=1.51, @total=1.64

总结

仅仅因为 Ruby 具有表现力和动态性,并不意味着您应该总是选择最美观的解决方案。使用基本的 each 循环方式创建哈希表速度最快。


10
你做完作业并且发表了它,这太棒了,我的朋友 :) - Alexander Bird
使用手动递增的循环变量会稍微快一些:我没有你的数据集 - 我只是用一个带有@id访问器的微不足道的对象,并且大致匹配了你的数字 - 但是直接迭代可以节省几个百分点。从风格上讲,我更喜欢{}.tap { |h| .... }来分配哈希,因为我喜欢封装好的代码块。 - android.weasel
3
考虑到.collect仅仅是.map的别名,我有些困惑arr.maparr.collect之间的区别。 - Matt VanEseltine

41

34
h = arr.each_with_object({}) { |v,h| h[v] = f(v) }

2
这种写法比使用 Hash[arr.collect{...}] 更加简洁易懂。 - kaichanvong
1
这个非常慢,请查看我下面的帖子:https://dev59.com/_HA85IYBdhLWcg3wF_i0#27962063 - dmastylo

11

这是我可能会写的:

h = Hash[arr.zip(arr.map(&method(:f)))]

简单、清晰、明显、声明式。还能要求什么更多的呢?


2
我和其他人一样喜欢使用zip,但既然我们已经在调用map了,为什么不就这样呢?h = Hash[ arr.map { |v| [ v, f(v) ] } ] 你的版本有什么优势是我没有看到的吗? - Telemachus
@Telemachus:由于我一直在阅读Haskell代码,所以我只是习惯了无参编程。 - Jörg W Mittag

5

我按照这篇很棒的文章描述的方式进行操作 http://robots.thoughtbot.com/iteration-as-an-anti-pattern#build-a-hash-from-an-array

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h.merge(fruit => f(fruit)) }

关于inject方法的更多信息:http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-inject

(该方法是Ruby编程语言中的一种迭代器方法,它可以将一个集合中的所有元素通过指定的操作进行累加或者合并,并返回最终结果。)

1
这个程序在每次迭代时都执行merge操作。合并的时间复杂度是O(n),迭代的时间复杂度也是O(n)。因此,尽管问题本身显然是线性的,但这个程序的时间复杂度是O(n^2)。就绝对值而言,我刚刚在一个有100k个元素的数组上尝试了一下,它花费了730秒,而本帖中提到的其他方法则需要0.71.1秒不等。是的,这是一个700倍的减速 - Matthias Winkelmann

3
除了Vlado Cingel的回答(我还不能添加评论,所以我添加了一个回答),Inject也可以这样使用:块必须返回累加器。只有块中的赋值返回赋值的值,并报告错误。
array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h[fruit]= f(fruit); h }

我对这两个版本进行了基准测试:使用合并操作会使执行时间增加一倍。上述的注入版本与Microspino的收集版本相当。 - ruud

1
另一个,依我之见更加清晰的 -
Hash[*array.reduce([]) { |memo, fruit| memo << fruit << f(fruit) }]

使用length作为f() -
2.1.5 :026 > array = ["apples", "bananas", "coconuts", "watermelons"]
 => ["apples", "bananas", "coconuts", "watermelons"] 
2.1.5 :027 > Hash[*array.reduce([]) { |memo, fruit| memo << fruit << fruit.length }]
 => {"apples"=>6, "bananas"=>7, "coconuts"=>8, "watermelons"=>11} 
2.1.5 :028 >

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接