使用`inject`,`除非`和`next`来确定最小值

我有这个代码:

def test(vertices, distances) until vertices.empty? nearest_vertex = vertices.inject do |a, b| p "a = #{a}: b = #{b}" p "distances[a] = #{distances[a]}, distances[b] = #{distances[b]}" next b unless distances[a] #next b if distances[a] == true next a unless distances[b] #next a if distances[b] == true next a if distances[a]  0, 2 => 3, 3 => 2, 4 => 18, 5 => nil, 6 => 7} test(vertices, distances) 

哪个输出:

 "a = 1: b = 2" "distances[a] = 0, distances[b] = 3" "a = 1: b = 3" "distances[a] = 0, distances[b] = 2" ... "a = 1: b = 6" "distances[a] = 0, distances[b] = 7" "nearest_vertex = 1" 

这里,不打印b = 6 。 这是因为next发出停止迭代命令吗?

 "a = 2: b = 3" "distances[a] = 3, distances[b] = 2" "b = 3" 

为什么迭代不继续a=2: b=4

 "a = 3: b = 4" "distances[a] = 2, distances[b] = 18" "a = 3: b = 5" "distances[a] = 2, distances[b] = " "a = 3: b = 6" "distances[a] = 2, distances[b] = 7" "nearest_vertex = 3" ... 

一旦a设置为3 ,一切都按照我的想法运作。 程序如何知道nearest_vertex是三个?

在确定如何以及何时将顶点声明为nearest_vertex时,我不理解injectnext之间的相互作用。 没有比较运算符时距离如何比较?

让我解释一下纯Ruby中的Enumerable#inject 。 请注意,以下代码不是 Enumerable#inject的原始实现。 为清楚起见,我将在类Array解释它,并专注于最基本的用法ary.inject(&block)

 class Array def inject(&block) return nil if self.empty? enumerator = self.each accumulator = enumerator.next loop do begin accumulator = block.call(accumulator, enumerator.next) rescue StopIteration break end end accumulator end end 

您可以看到,在循环中,前一次迭代的累加器和数组的当前元素被传递给块的参数,并且块的返回值被重新分配给累加器。

然后块中的next x是什么?

您可以将块视为匿名函数,关键字next是其return 。 它终止当前块调用并使块返回x (如果未明确指定返回值,则为nil )。

顺便说一句,块中的break x终止了获取块的方法调用,并使该方法返回x 。 例如:

 [1, 2, 3, 4].inject do |acc, n| break n if n == 2 acc + n end => 2 

n2Array#injectbreak终止,并返回n

块中的return终止方法调用,该方法调用调用获取块的方法。 例如:

 def foo [1, 2, 3].inject do |acc, n| return n end puts 'You will never see this this sentence.' end foo => 2 

并且没有打印句子,因为fooreturn终止。

如何inject工作

传递给inject的块在每次迭代中接收两个参数。 第一个参数( prev_nearest_key here)是一个“累加器”,其值是前一次迭代返回的值。 (对于第一次迭代,它将是inject的参数,或者在缺少的情况下,这里是集合的第一个元素 – vertices[0] 。)第二个参数( key )是集合的当前元素。 迭代完成后,将返回累加器的最终值。

当您在传递给迭代器的块中调用next val时,会立即从块返回val并开始下一次迭代。 为了演示,以下是map外观:

 ["h", "e", "l", "l", "o"].map do |letter| next letter.upcase if "aeoiu".include?(letter) letter end # => ["h", "E", "l", "l", "O"] 

上面,当letter是元音时,将从块返回letter.upcase ,并且永远不会评估下一行。 当letter不是元音时,它从块中返回不变。

这是一个类似的例子,注入:

 ["h", "e", "l", "l", "o"].inject do |accum, letter| next accum + letter.upcase if "aeoiu".include?(letter) accum + letter end # => "hEllO" 

这有什么不同? 当letter是元音时,从块返回accum + letter.upcase (实际上将letter.upcase附加到accum ),并且永远不会评估下一行。 当letter不是元音时,从块返回accum + letter 。 在这两种情况下,从块返回的值在下一次迭代中变为accum

你的代码如何工作

这是您的代码,但具有更易理解的变量名称。

 nearest_vertex = vertices.inject do |prev_nearest_vertex, current_vertex| next current_vertex unless distances[prev_nearest_vertex] next prev_nearest_vertex unless distances[current_vertex] next prev_nearest_vertex if distances[prev_nearest_vertex] < distances[current_vertex] current_vertex end 

我已经将累加器重命名为prev_nearest_vertex ,因为它是前一次迭代返回的值,而bcurrent_vertex

块内的前两行只是检查distances[prev_nearest_vertex]distances[current_vertex]是否nil 。 他们可以(也许应该)这样编写:

 next current_vertex if distances[prev_nearest_vertex].nil? next prev_nearest_vertex if distances[current_vertex].nil? 

第一行基本上说,“如果前一个最近的顶点的距离是nil ,那么它不是最近的,所以在下一次迭代中将prev_nearest_vertex设置为current_vertex 。” 第二行说“如果当前顶点的距离nil ,那么它不是最近的,所以在下一次迭代中保持prev_nearest_vertex相同。

这是第三和第四行:

 next prev_nearest_vertex if distances[prev_nearest_vertex] < distances[current_vertex] current_vertex 

这些可以像这样重写:

 if distances[prev_nearest_vertex] < distances[current_vertex] prev_nearest_vertex else current_vertex end 

它只是说,“如果距离较小,则在下一次迭代prev_nearest_vertex设置为prev_nearest_vertex ;否则将其设置为current_vertex

这是非常好的代码,但你应该......

这样做

Ruby的Enumerable模块有很多非常有用的方法,包括一个名为min_by 。 它为Enumerable中的每个元素计算给定块,并返回返回最低值的元素。 要演示,请考虑以下map

 words = ["lorem", "ipsum", "dolor", "sit", "amet"] words.map {|word| word.size } # => [5, 5, 5, 3, 4] 

这只是将一个单词数组转换为它们大小的数组。 现在假设我们想要得到最短的单词。 使用min_by很容易:

 words = ["lorem", "ipsum", "dolor", "sit", "amet"] words.min_by {|word| word.size } # => "sit" 

这不是返回单词'sizes,而是计算它们的大小,然后返回大小最小的单词。

这直接适用于您的代码。 再次考虑一下map操作:

 vertices = [1, 2, 3, 4, 5, 6] distances = { 1 => 0, 2 => 3, 3 => 2, 4 => 18, 5 => nil, 6 => 7 } vertices.map do |vertex| distances[vertex] || Float::INFINITY end # => [0, 3, 2, 18, Infinity, 7] 

这将生成与vertices的元素对应的距离数组,但是使用Float::INFINITY替换nil距离。 这很有用,因为对于每个数字n n < Float::INFINITY为真。 和以前一样,我们现在可以用min_by替换map来获得与最小距离对应的顶点:

 def test(vertices, distances) vertices.min_by {|vertex| distances[vertex] || Float::INFINITY } end test(vertices, distances) # => 1