Ruby - 优雅地比较两个枚举器

Posted

技术标签:

【中文标题】Ruby - 优雅地比较两个枚举器【英文标题】:Ruby - Compare two Enumerators elegantly 【发布时间】:2011-06-26 19:10:56 【问题描述】:

在 Ruby (1.9.2) 中,我有来自两个不同来源(二进制数据)的两个长数字流。

这两个来源被封装成两个Enumerators。

我想检查两个流是否完全相等。

我提供了几个解决方案,但似乎都不太优雅。

第一个简单地将两者都转换为数组:

def equal_streams?(s1, s2)
  s1.to_a == s2.to_a
end

这可行,但在内存方面不是很高效,特别是在流包含大量信息的情况下。

另一种选择是……呃。

def equal_streams?(s1, s2)
  s1.each do |e1|
    begin
      e2 = s2.next
      return false unless e1 == e2 # Different element found
    rescue StopIteration
      return false # s2 has run out of items before s1
    end
  end

  begin
    s2.next
  rescue StopIteration
    # s1 and s2 have run out of elements at the same time; they are equal
    return true
  end

  return false

end

那么,有没有更简单、更优雅的方法呢?

【问题讨论】:

Enumerable#zip 会更优雅,但在性能方面很可能不会有任何改进。 看起来在 Ruby 1.9 中处理这个问题的高性能方法是使用纤维/协程。见infoq.com/news/2007/08/ruby-1-9-fibers。 @Steve:zip 的问题在于它在内部创建数组,无论您传递什么 Enumerable。输入参数的长度还有另一个问题。 @Mladen:我知道——这就是为什么我说它很可能不会在性能上有任何改进。 @Mladen: 1.times.zip(1.times) |x,y| puts x, y; 42 打印出 0、0 并返回 nil 【参考方案1】:

假设您的流不包含元素 :eof,只需对您的代码进行轻微重构。

def equal_streams?(s1, s2)
  loop do
    e1 = s1.next rescue :eof
    e2 = s2.next rescue :eof
    return false unless e1 == e2
    return true if e1 == :eof
  end
end

使用像loop 这样的关键字应该比使用像each 这样的方法更快。

【讨论】:

我非常喜欢这个。我没有想到我可以使用“保护”值,例如您示例中的 :eof 符号。谢谢! 哦——我在脑海中想到我们需要一个解决方案可枚举重复调用传递给#each 的块。只要我们可以拉动,这是一个很好的解决方案。如果它必须与枚举一起工作,那么就需要协程。【参考方案2】:

一次比较一个元素可能是你能做到的最好的,但你可以做得比你的“呃”解决方案更好:

def grab_next(h, k, s)
  h[k] = s.next
rescue StopIteration
end

def equal_streams?(s1, s2)
  loop do
    vals =  
    grab_next(vals, :s1, s1)
    grab_next(vals, :s2, s2)
    return true  if(vals.keys.length == 0)  # Both of them ran out.
    return false if(vals.keys.length == 1)  # One of them ran out early.
    return false if(vals[:s1] != vals[:s2]) # Found a mismatch.
  end
end

棘手的部分是区分只有一个流用完和两个流用完。将StopIteration 异常推送到单独的函数中并使用散列中缺少键是一种相当方便的方法。如果您的流包含 falsenil,则仅检查 vals[:s1] 会导致问题,但检查是否存在密钥可以解决该问题。

【讨论】:

感谢您的回答。我更喜欢sawa,但为您的时间+1! @kikito:这很酷,我也赞成sawa 的回答。 Sawa 和我使用几乎相同的逻辑,我只是​​使用没有哈希键来表示结束而不是sentinel value。如果你知道你永远不会在你的流中使用:eof 符号,那么就不需要额外的机制来避免使用哨兵。 你是对的。我的与:eof 有轻微冲突的可能性。 +1 比我的更通用的解决方案。 @sawa:但:eof 可能是哨兵的最佳选择,具体情况涉及“数字流”,如果:eof 符号出现在数字流中,那么 kikito 有更多他手上的问题比保留的哨兵价值。【参考方案3】:

这是通过为Enumerable#zip 创建一个替代方案来实现它的一个镜头,该替代方案运行缓慢并且不会创建整个数组。它结合了 Closure 的 interleave 的 my implementation 和其他两个答案(使用标记值来指示已到达 Enumerable 的结尾 - 导致问题的事实是 next 一旦到达结束)。

此方案支持多个参数,因此您可以一次比较 n 个结构。

module Enumerable
  # this should be just a unique sentinel value (any ideas for more elegant solution?)
  END_REACHED = Object.new

  def lazy_zip *others
    sources = ([self] + others).map(&:to_enum)
    Enumerator.new do |yielder|
      loop do
        sources, values = sources.map|s|
          [s, s.next] rescue [nil, END_REACHED]
        .transpose
        raise StopIteration if values.all?|v| v == END_REACHED
        yielder.yield values.map|v| v == END_REACHED ? nil : v
      end
    end
  end
end

因此,当您有 zip 的变体,它可以延迟工作并且在第一个可枚举到达末尾时不会停止迭代,您可以使用 all?any? 来实际检查相应元素的相等性。

# zip would fail here, as it would return just [[1,1],[2,2],[3,3]]:
p [1,2,3].lazy_zip([1,2,3,4]).all?|l,r| l == r
#=> false

# this is ok
p [1,2,3,4].lazy_zip([1,2,3,4]).all?|l,r| l == r
#=> true

# comparing more than two input streams:
p [1,2,3,4].lazy_zip([1,2,3,4],[1,2,3]).all?|vals|
  # check for equality by checking length of the uniqued array
  vals.uniq.length == 1

#=> false

【讨论】:

“延迟工作并且不会创建整个数组” - zip 通常也不会创建整个数组:请参阅 ***.com/questions/6486107/… 和 ***.com/questions/6487747/… 我的评论链接到这个问题。对不起,我完全失败了!【参考方案4】:

根据 cmets 中的讨论,这里是基于 zip 的解决方案,首先将 zip 的块版本包装在 Enumerator 中,然后使用它来比较相应的元素。

它有效,但是已经提到了边缘情况:如果第一个流比另一个短,则另一个流的剩余元素将被丢弃(参见下面的示例)。

我已将此答案标记为社区 wiki,因为其他成员可以改进它。

def zip_lazy *enums
  Enumerator.new do |yielder|
    head, *tail = enums
    head.zip(*tail) do |values|
      yielder.yield values
    end
  end
end

p zip_lazy(1..3, 1..4).all?|l,r| l == r
#=> true
p zip_lazy(1..3, 1..3).all?|l,r| l == r
#=> true
p zip_lazy(1..4, 1..3).all?|l,r| l == r
#=> false

【讨论】:

【参考方案5】:

这是一个使用纤维/协同例程的 2 源示例。它有点啰嗦,但它的行为非常明确,这很好。

def zip_verbose(enum1, enum2)
  e2_fiber = Fiber.new do
    enum2.each|e2| Fiber.yield true, e2 
    Fiber.yield false, nil
  end
  e2_has_value, e2_val = true, nil
  enum1.each do |e1_val|
    e2_has_value, e2_val = e2_fiber.resume if e2_has_value
    yield [true, e1_val], [e2_has_value, e2_val]
  end
  return unless e2_has_value
  loop do
    e2_has_value, e2_val = e2_fiber.resume
    break unless e2_has_value
    yield [false, nil], [e2_has_value, e2_val]
  end
end

def zip(enum1, enum2)
  zip_verbose(enum1, enum2) |e1, e2| yield e1[1], e2[1] 
end

def self.equal?(enum1, enum2)
  zip_verbose(enum1, enum2) do |e1,e2|
    return false unless e1 == e2
  end
  return true
end

【讨论】:

以上是关于Ruby - 优雅地比较两个枚举器的主要内容,如果未能解决你的问题,请参考以下文章

这 2 个 Ruby 枚举器之间的区别:[1,2,3].map 与 [1,2,3].group_by

怎么比较两个枚举类型是不是相等

如何优雅地重命名 Ruby 中哈希中的所有键? [复制]

ruby 创建一个接收N个枚举器的枚举器然后排序

如何在 Ruby 中“关闭”枚举器?

ruby Ruby函数使用枚举器来获取数字的倍数