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

Posted

技术标签:

【中文标题】使用 `inject`、`unless` 和 `next` 来确定最小值【英文标题】:Using `inject`, `unless`, and `next` to determine the minimum value 【发布时间】:2016-03-22 14:31:22 【问题描述】:

我有这个代码:

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"

这里为什么不继续迭代@​​987654326@?

"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是三?

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

【问题讨论】:

我相信您将块中的累加器和元素混合到inject 上。根据您的 cmets,您希望它是 |element, accum|,而恰恰相反:|accum, element|。尝试将|a, b| 交换为|b, a| “最接近”什么? 感谢铁皮人编辑我的原帖。这是我在 *** 上发布的第一个问题,原始格式不太理想。 Nearest 只是将“a”与所有其他“b”进行比较并选择最小的数字。这是从 Ruby 中 Dijkstra 算法的实现中脱离上下文的一小段代码。 【参考方案1】:

让我用纯 Ruby 解释Enumerable#inject。请注意,以下代码不是 Enumerable#inject 的原始实现。为清楚起见,我会在课堂上讲解Array,重点讲解最基本的用法ary.inject(&amp;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

可以看到在循环中,前一次迭代的累加器和数组的当前元素被传递给了block的params,block的返回值被重新赋值给了累加器。

那么块中的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终止。

【讨论】:

感谢您抽出宝贵时间帮助我更好地了解注入方法。将它与 unless 结合使用确实令人困惑,但我想我现在对它的理解要好得多。【参考方案2】:

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 不是元音时,它原封不动地从块中返回。

这是一个类似的注入示例:

["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 &lt; Float::INFINITY 对每个数字n 都是正确的。和之前一样,我们现在可以将map替换为min_by,得到最小距离对应的顶点:

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

test(vertices, distances)
# => 1

【讨论】:

以上是关于使用 `inject`、`unless` 和 `next` 来确定最小值的主要内容,如果未能解决你的问题,请参考以下文章

unless|until|LABEL|{}|last|next|redo| || |//|i++|++i

理解saltstack 里cmd.run 配合onlyif和unless使用

php 加入 unless 语法

Perl 学习笔记-高级控制结构.unless控制结构

low security dvwa--SQL Injection

带有 express-unless 的 Node.js express-jwt 给出警告“对象可能是'未定义'”