我有一个简单的数组:
arr = ["apples", "bananas", "coconuts", "watermelons"]
我还有一个函数f
,它会对单个字符串输入执行操作并返回值。这个操作非常耗费时间,所以我想在哈希表中缓存结果。
我知道可以用类似以下的代码生成所需的哈希表:
h = {}
arr.each { |a| h[a] = f(a) }
我想做的是不必初始化h,这样我就可以像这样写:
h = arr.(???) { |a| a => f(a) }
可以这样做吗?
假设你有一个函数,其函数名非常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)] }]
我对给出的答案进行了快速、简单的基准测试。(这些结果可能与你的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 循环方式创建哈希表速度最快。
.collect
仅仅是.map
的别名,我有些困惑arr.map
和arr.collect
之间的区别。 - Matt VanEseltineh = arr.each_with_object({}) { |v,h| h[v] = f(v) }
这是我可能会写的:
h = Hash[arr.zip(arr.map(&method(:f)))]
简单、清晰、明显、声明式。还能要求什么更多的呢?
zip
,但既然我们已经在调用map
了,为什么不就这样呢?h = Hash[ arr.map { |v| [ v, f(v) ] } ]
你的版本有什么优势是我没有看到的吗? - Telemachus我按照这篇很棒的文章描述的方式进行操作 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
merge
操作。合并的时间复杂度是O(n),迭代的时间复杂度也是O(n)。因此,尽管问题本身显然是线性的,但这个程序的时间复杂度是O(n^2)
。就绝对值而言,我刚刚在一个有100k个元素的数组上尝试了一下,它花费了730秒
,而本帖中提到的其他方法则需要0.7
至1.1秒
不等。是的,这是一个700倍的减速! - Matthias Winkelmannarray = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h[fruit]= f(fruit); h }
Hash[*array.reduce([]) { |memo, fruit| memo << fruit << f(fruit) }]
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 >
... { |v| [v, f(v)] }
,但这个也能解决问题! - Wizzlewott*arr.collect
旁边会有一个*
符号? - Jeriko*
可以根据上下文将列表收集到数组中或将数组展开为列表。这里它把数组展开成了一个列表(用作新哈希的项)。 - Telemachus*
和flatten
以获得更简单的版本:h = Hash [arr.collect {|v| [v,f(v)]}]
。但是我不确定是否有我没有注意到的问题。 - TelemachusHash[*key_pairs.flatten]
可以简写为Hash[key_pairs]
。这种写法更加美观,如果你还没有从 1.8.6 更新过来,可以使用require 'backports'
。 - Marc-André Lafortune