我不知道这是否是“更多的 Ruby 方式”。至少是一种更“高阶”的方式,FWIW。
# first line is number of inputs (Don't need it), thus drop the first line
# read inputs as ints
h = ARGF.drop(1).reduce(Hash.new(0)) {|h, n| h.tap {|h| h[n.to_i] += 1 }}
这里不多说。我们不是简单地循环 ARGF 并设置哈希键,而是使用 reduce 让它为我们完成工作。我们使用默认值为0 的哈希,而不是手动检查键是否存在。
我们使用Enumerable#drop 来简单地删除第一行。
ARGF 是从 Perl 中窃取的一个非常酷的功能(就像大多数脚本功能一样):如果您只是将脚本称为 script.rb 而不带参数,那么 ARGF 是标准输入。但是,如果您将脚本称为script.rb a.txt b.txt,那么Ruby 会将所有参数解释为文件名,打开所有文件进行读取,ARGF 将是它们内容的串联。这使您可以非常快速地编写可以通过标准输入或文件获取其输入的脚本。
# find smallest mode
modes = h.group_by(&:last).sort.last.last.map(&:first).sort
puts "Mode is: #{modes.first}"
Ruby 没有明确的键值对类型,相反,大多数对哈希的循环操作都使用二元素数组。这允许我们使用Array#first 和Array#last 来引用键和值。
在这种特殊情况下,我们使用Enumerable#group_by 将哈希分组到不同的桶中,我们使用的分组标准是last 方法,即我们哈希中的值是频率。换句话说,我们按频率分组。
如果我们现在对结果哈希进行排序,最后一个元素是频率最高的元素(即模式)。我们取它的最后一个元素(键值对的值),然后是它的最后一个元素,这是一个键值对数组(number => frequency),我们从中提取键(数字)并对其进行排序。
[注意:只需将每个中间阶段的结果打印出来,就更容易理解了。只需将上面的 modes = ... 行替换为以下内容:
p modes = h.tap(&method(:p)).
group_by(&:last).tap(&method(:p)).
sort.tap(&method(:p)).
last.tap(&method(:p)).
last.tap(&method(:p)).
map(&:first).tap(&method(:p)).
sort
]
modes 现在是一个排序数组,其中包含具有该特定频率的所有数字。如果我们取第一个元素,我们有最小的模式。
# mode unique?
puts "Mode is #{unless modes.size == 1 then '*not* ' end}unique."
如果数组的大小不是1,那么模式不是唯一的。
# print number of singleton odds,
# odd elems repeated odd number times in desc order
# even singletons in desc order
odds, evens = h.select {|_,f|f==1}.map(&:first).sort.reverse.partition(&:odd?)
看起来这里发生了很多事情,但实际上很简单。您从等号后开始阅读,然后从左到右阅读。
- 我们选择哈希值(即频率)为
1的所有项目。 IOW,我们选择所有的单例。
- 我们将所有生成的键值对映射到它们的第一个元素,即数字 - 我们丢弃频率的 IOW。
- 我们对列表进行排序
- 然后反转它(对于较大的列表,我们应该从反转开始,因为这非常浪费 CPU 周期和内存)
- 最后,我们将数组分成两个数组,一个包含所有奇数,另一个包含所有偶数
-
现在我们终于看到等号的左侧:Enumerable#partition 返回一个二元素数组,其中包含两个具有分区元素的数组,我们使用 Ruby 的解构赋值将这两个数组分配给两个变量
puts "奇数值只出现一次的元素个数:#{odds.size}"
现在我们有了一个奇数单例列表,它们的数量就是列表的大小。
puts "Elements repeated an odd number of times: #{
h.select {|_, f| f.odd?}.map(&:first).sort.reverse.join(', ')
}"
这与上面的非常相似:选择所有奇数频率的数字,映射出键(即数字),排序,反转,然后通过将它们连接在一起并用逗号和空格将它们转换为字符串。
puts "Elements with an even value that appear exactly once: #{evens.join(', ')}"
同样,既然我们有一个偶数单例列表,打印它们只需用逗号连接列表元素即可。
# print fib numbers in the hash
我不想重构这个算法以提高效率并专门用于记忆。我只是做了一些小的调整。
class Integer
算法中没有任何东西依赖于特定大小的数字,所以我将方法拉到Integer 类中。
def fib?
我删除了is_ 前缀。这是一个布尔方法这一事实已经在问号中明确了。
l, h = 0, 1
while h <= self
return true if h == self
l, h = h, l+h
end
end
end
puts "Fibonacci numbers: #{h.keys.sort.select(&:fib?).join(', ')}"
这可能不需要太多解释:取出键,对它们进行排序,选择所有斐波那契数并用逗号将它们连接在一起。
这里有一个关于如何重构这个算法的想法。 a very interesting implementation of Fibonacci 使用带有默认值的 Hash 用于记忆:
fibs = {0 => 0, 1 => 1}.tap do |fibs|
fibs.default_proc = ->(fibs, n) { fibs[n] = fibs[n-1] + fibs[n-2] }
end
看起来有点像这样:
class Integer
@@fibs = {0 => 0, 1 => 1}.tap do |fibs|
fibs.default_proc = ->(fibs, n) { fibs[n] = fibs[n-1] + fibs[n-2] }
end
def fib?
i = 0
until @@fibs[i += 1] > self
break true if @@fibs[i] == self
end
end
end
puts "Fibonacci numbers: #{h.keys.sort.select(&:fib?).join(', ')}"
如果有人能想出一种优雅的方式来摆脱i = 0、i += 1 和整个until 循环,我将不胜感激。