【问题标题】:How do I create a custom hash in which the key lookup is based on a function I define?如何创建自定义哈希,其中键查找基于我定义的函数?
【发布时间】:2018-03-01 16:27:58
【问题描述】:

我使用的是 Ruby 2.4。如何创建自定义哈希,其中键的查找函数是我定义的?我有这个功能...

  def str_almost_equal(a, b)
    a.downcase == b.downcase || (a.size == b.size && a.downcase.chars.map.with_index{|c, i| c == b.downcase[i]}.count(false) == 1)
  end

如果键的两个类都是字符串并且它们与上面的方法匹配,我希望我的哈希来查找键。例如,如果我有

hash = []
hash["aaa"] = 1

然后我希望 hash["aab"] 返回 1,因为 "aaa" 和 "aab" 使用我定义的函数计算为真。

【问题讨论】:

  • 如果在您的示例中存在密钥 "aab" 会发生什么?

标签: ruby function hash key


【解决方案1】:

您可以定义自己的继承自Hash 的类,并根据自己的喜好覆盖[](key) 方法。所以例如

class MyHash < Hash
  def [](key)
    # do something to return the appropriate value
  end
end

根据您的调用代码所需的 Hash 接口的其他方法,您可能需要重写其他方法。

但是,鉴于您的用例,我怀疑您是否能够以非常有效的方式实现该方法,因为您必须遍历存储在哈希中的所有候选者(至少具有相同长度 +-1 的候选者)来评估它们是否匹配。当然,如果哈希中没有很多候选者,你可能会侥幸逃脱。

除此之外,我不认为在为哈希中的查找提供键时返回 1 的行为是非常类似于哈希的行为。我宁愿退回比赛。返回 1 将违反 IMO 最小意外原则。


OP评论后的补充

在不知道您的确切用例的情况下,也许我已经离开了,我会牺牲插入时间和空间的速度来提高查找时间的速度。这意味着我会在插入时预先计算一个键的所有变体,并将它们插入到用于存储的对象中。

class VariantsSet < Set 

  PLACEHOLDER = '_' # Use a character that will not be used in your keys

  def add_variants(string)
    merge(all_variants(string))
  end 

  def delete(string)
    all_variants(string).each { |variant| super(variant) }
  end

  def includes_any_variant?(string)
    all_variants(string).any? { |variant| include?(variant) }
  end

  private

  def all_variants(string)
    downcase_string = string.downcase
    string_length = string.length

    variants = [downcase_string]

    string_length.times do |i|
      variants << downcase_string[0, i] + PLACEHOLDER + downcase_string[i + 1 , string_length]
    end

    variants
  end
end

用法如下:

2.4.2 :026 > s = VariantsSet.new
 => #<VariantsSet: {}> 
2.4.2 :027 > s.add_variants('foobar')
 => #<VariantsSet: {"foobar", "_oobar", "f_obar", "fo_bar", "foo_ar", "foob_r", "fooba_"}> 
2.4.2 :028 > s.includes_any_variant?('foobaz')
 => true 
2.4.2 :029 > s.includes_any_variant?('blubs')
 => false 

根据您要存储的密钥数量,这可能需要相当多的 RAM,因此如果它失控,您可能不得不使用例如用于存储值的数据库。但我仍然建议预先计算变体。

如果您更喜欢的话,基本上可以使用 Hash 作为超类来完成相同的操作。


展开显示哈希实现

class VariantsHash < Hash

  PLACEHOLDER = '_' # Use a character that will not be used in your keys

  def []=(string, value)
    all_variants(string).each do |variant|
      super(variant, value)
    end
  end 

  def delete(string)
    all_variants(string).each do |variant|
      super(variant)
    end
  end 

  def [](string)
    match = all_variants(string).detect do |variant|
      super(variant)
    end

    super(match) if match
  end

  private

  def all_variants(string)
    downcase_string = string.downcase
    string_length = string.length

    variants = [downcase_string]

    string_length.times do |i|
      variants << downcase_string[0, i] + PLACEHOLDER + downcase_string[i + 1 , string_length]
    end

    variants
  end
end 

【讨论】:

  • 我返回“1”,因为这就是我的示例。如果我输入“55”,那么该示例将返回“55”。无论如何,当您说“遍历存储在哈希中的所有候选人(至少具有相同长度 +-1 的候选人)以评估它们是否匹配”时,您实际上是在重申我的问题。我寻求如何做到这一点的答案。
  • 感谢您的扩展回答。当它适应所有这些时我有点慢,所以当你说“基本上可以使用哈希作为超类来完成同样的事情”时,我重写了哪些方法来使哈希查找以我描述的方式工作?
  • 我添加了 Hash 实现
  • 谢谢,但这并不完全奏效。如果我设置 "hash["abc"] = 1" 然后运行 ​​"hash["abb"]",我得到了 "ab"。但是我应该得到“1”,因为根据我在问题中列出的函数—— str_almost_equal("abc", "abb") 的计算结果为 true。
  • @Natalia,我更改了VariantsHash 以正确返回为任何变体设置的值。 hash["abb"] 现在将按照指定返回 1。感谢您指出错误。
【解决方案2】:

这可能不是最好的答案,但我认为它可以帮助你

使用新类

class NewHash < Hash
  def [](key)
    if keys.include?(key)
      super
    else
      found = keys.find { |k| compare_almost(k, key) }
      self[found] if found
    end
  end

  private

  def compare_almost(a, b)
    a.downcase == b.downcase ||
      (a.size == b.size && a.downcase.chars.map.with_index{|c, i| c == b.downcase[i]}.count(false) == 1)
  end
end

hash = NewHash.new
hash['aaa'] = 1
p hash['aab'] # => 1
p hash['acs'] # => nil

为每个对象使用模块

module CompareAlmost
  def [](key)
    if keys.include?(key)
      super
    else
      found = keys.find { |k| compare_almost(k, key) }
      self[found] if found
    end
  end

  private

  def compare_almost(a, b)
    a.downcase == b.downcase ||
      (a.size == b.size && a.downcase.chars.map.with_index{|c, i| c == b.downcase[i]}.count(false) == 1)
  end
end

hash = {'aaa' => 2}
hash.extend CompareAlmost
p hash['aab'] # => 2

替换原始哈希类

class Hash
  def [](key)
    if keys.include?(key)
      fetch key
    else
      found = keys.find { |k| compare_almost(k, key) }
      self[found] if found
    end
  end

  private

  def compare_almost(a, b)
    a.downcase == b.downcase ||
      (a.size == b.size && a.downcase.chars.map.with_index{|c, i| c == b.downcase[i]}.count(false) == 1)
  end
end

hash = { 'aaa' => 3 }
p hash['aab'] # => 3

【讨论】:

  • 为什么你同时拥有“Hash”和“NewHash”类?我不应该只需要你的“NewHash”类吗?我不想改变所有哈希的底层功能,只需创建一个具有不同功能的新哈希。
  • 我刚刚为您列出了 3 个解决方案。因此,如果您不想更改Hash,只需使用NewHash
【解决方案3】:

从您的问题中不清楚您的用例是什么,特别是如果密钥已经存在应该发生什么。假设您只想在键不存在时使用自定义函数查找值,那么它只是为哈希设置自定义默认值。如果是这样的话,解决方案就很简单了:

## your custom function, changed to a Proc so it can be passed to a method
str_almost_equal = Proc.new do |a,b|
    a.downcase == b.downcase || (a.size == b.size && a.downcase.chars.map.with_index{|c, i| c == b.downcase[i]}.count(false) == 1)
  end

## iterator to run all values through your function
def check_all(a, array, func)
  array.each do |n|
    return n if func.call(a, n)
  end
end

## Hash with custom default value
h = Hash.new do |h,k| 
  if k.is_a? String
    key = check_all(k, h.keys, str_almost_equal) 
    h[key] if key
  end

h["aaa"]    ## nil
h["aaa"] = 3
h["aab"]    ## 3
h["bab"]    ## nil
h["aab"] = 4
h["aab"]    ## 4

【讨论】:

    【解决方案4】:

    对于用户创建的类型,哈希查找使用两种方法#eql?#hash。这是覆盖哈希行为的规范方法。

    class StringWrapper
      attr_reader :string
    
      def initialize(string)
        @string = string
      end
    
      def eql?(other)
        string.downcase == other.string.downcase ||
        (string.size == other.string.size && 
         string.downcase.chars.map.with_index{|c, i| c == 
         other.string.downcase[i]}.count(false) == 1
        )
      end
    
      def hash
        # never actually do this part
        0
      end
    end
    
    hash = {}
    hash[StringWrapper.new("aaa")] = 1
    hash[StringWrapper.new("aab")] # => 1
    

    但是使用 Hash 而不是其他数据类型的原因是,无论您有多少数据,查找键所需的时间通常都不会增长。但在我们上面的实现中,我们打破了这一点。让我们看看哈希是如何工作的。在后台,您的数据大致如下存储1

    |0  |1  |2  |3  |4  |5  |6  |7  |8  |9  |10 |
    |   |   |   |aaa|   |   |aad|   |   |   |aab|
    |   |   |   |aae|   |   |   |   |   |   |   |
    

    当您在哈希中查找一个键时,Ruby 不会将它与所有已经存在的键进行比较。如果这样做了,那么您添加的键越多,查找速度就会越来越慢。相反,它会为密钥生成一个散列——通常是一个大的伪随机数。例如,在我的计算机上"aaa".hash 返回 2707793439826082248。 2707793439826082248 % 113,所以它被放入 3 号桶中。然后当我们需要查找它时,该过程再次发生,Ruby 知道要检查哪个桶。现在,无需检查每个键,只需检查存储在 bucked 3 中的那些。这是我们定义 #hash 始终返回 0 时所做的。

    |0  |1  |2  |3  |4  |5  |6  |7  |8  |9  |10 |
    |aaa|   |   |   |   |   |   |   |   |   |   |
    |aab|   |   |   |   |   |   |   |   |   |   |
    |aac|   |   |   |   |   |   |   |   |   |   |
    |aad|   |   |   |   |   |   |   |   |   |   |
    

    每个对象都存储在同一个存储桶中,因此我们破坏了哈希查找性能。在定义自定义哈希方法时,您需要每次计算时都相同且随机出现的东西,以便模运算将您的数据均匀地分布在存储桶中。但是哈希方法还必须尊重您对#eql? 的实现。如果不是这样,您最终会在大多数情况下寻找错误的存储桶。例如,如果您想要一个哈希处理以相同字母开头的对象,这很容易。

     def hash
       string[0].hash
     end
    

    您所要求的问题是,可以构建一系列查找,其中每个字符串都彼此相等。 “富” === “嘘”。 “嘘” == “宝”。 “宝” == “酒吧”。为了让程序尊重您对相等性的定义,所有这些字符串都需要位于同一个哈希桶中。这意味着为了让您的哈希按照您描述的方式运行,每个字符串都需要具有相同的哈希桶。根据定义,这不是真正的哈希。所以我认为你应该退后一步,考虑一下你要解决的问题是什么,以及哈希是否真的是正确的方法。

    你当然可以做 ulferts 建议的事情,但要以使用更多内存为代价,但是会发生什么

    hash = { "aaa" => 1 }
    hash["cab"] = 2
    hash["aab"]
    

    1:具有少量项目的 Ruby 哈希实际上是作为数组实现的,至少在 MRI 中是这样。希望这些图有助于解释哈希表在高层次上是如何工作的,而不是实际显示给定的 Ruby 是如何实现它们的。我敢肯定它们有很多不准确之处。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-10-22
      • 1970-01-01
      • 1970-01-01
      • 2018-06-07
      • 1970-01-01
      • 1970-01-01
      • 2021-04-06
      • 1970-01-01
      相关资源
      最近更新 更多