ruby 使用`inject`、`unless`和`next`确定最小值

cwxwcias  于 2023-06-22  发布在  Ruby
关注(0)|答案(2)|浏览(131)

我有这个代码:

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] < distances[b]
      p "b = #{b}"
      b
    end
    p "nearest_vertex = #{nearest_vertex}"
    vertices.delete nearest_vertex
  end
end

vertices = [1, 2, 3, 4, 5, 6]
distances = {1 => 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是3?
我不理解injectnext在确定如何以及何时将顶点声明为nearest_vertex时的相互作用。当没有比较运算符时,如何比较距离?

jk9hmnmh

jk9hmnmh1#

让我来解释一下纯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

n2时,Array#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结尾。

xqnpmsa8

xqnpmsa82#

inject工作原理

传递给inject的块在每次迭代中接收两个参数。第一个参数(这里是prev_nearest_key)是一个“累加器”,其值是上一次迭代返回的任何值。(对于第一次迭代,它将是给定给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不是元音时,它将从块中返回,不作任何更改。
下面是一个类似的inject示例:

["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

我将累加器a重命名为prev_nearest_vertex,因为它是上一次迭代返回的值,并将b重命名为current_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"

它不返回单词的大小,而是计算它们的大小,然后返回大小最小的单词。
这直接适用于您的代码。再次考虑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中元素的距离数组,但nil距离被Float::INFINITY替换。这很有用,因为n < Float::INFINITY对每个数字n都为真。和前面一样,我们现在可以用min_by替换map,以获得对应于最小距离的顶点:

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

test(vertices, distances)
# => 1

相关问题