为所有字谜生成相同的唯一哈希码

Posted

技术标签:

【中文标题】为所有字谜生成相同的唯一哈希码【英文标题】:Generate same unique hash code for all anagrams 【发布时间】:2013-09-17 20:00:48 【问题描述】:

最近,我参加了一次面试,遇到了一个关于哈希冲突的好问题。

问题:给定一个字符串列表,一起打印出字谜。

示例:i/p:              行为,上帝,动物,狗,猫 o/p :                 行为,猫,狗,神

我想创建 hashmap 并将单词作为键和值作为字谜列表

为避免冲突,我想为字谜生成唯一的哈希码,而不是排序并使用排序后的单词作为键。

我正在寻找哈希算法来处理冲突而不是使用链接。我希望算法为 act 和 cat 生成相同的哈希码...以便将下一个单词添加到值列表中

谁能推荐一个好的算法?

【问题讨论】:

据我所知,哈希算法应该有冲突来分组“相等”的项目。否则,您最终会以排序后的单词作为键。 您应该将单词视为一组字符,而不是一个列表。我能想到的最简单的事情是将所有字符值以某个素数为模相乘。但当然,这很快就会发生意外碰撞。 对每个单词进行排序并散列排序值是我会做的。散列的副作用是总是会发生冲突。有办法绕过它,但它们不能保证你 O(1) 访问任何 iirc。 我正在寻找哈希算法来处理冲突而不是使用链接。我希望算法为 act 和 cat 生成相同的哈希码...以便将其视为值。 @user1554241 您很可能能够为“act”和“cat”生成相同的哈希码。在对每个单词中的字母进行排序后进行散列是一个很好的建议。你不能确保没有冲突,因为字符串比固定宽度的整数多。 【参考方案1】:

使用排序后的字符串进行散列非常好,我可能会这样做,但它确实可能很慢而且很麻烦。这是另一个想法,不确定它是否有效 - 选择一组素数,如你所愿,与你的字符集大小相同,并建立一个从你的字符到那个的快速映射函数。然后对于给定的单词,将每个字符映射到匹配的素数,然后相乘。最后,使用结果进行哈希处理。

这与 Heuster 建议的非常相似,只是碰撞更少(实际上,鉴于任何数的素数分解的唯一性,我相信不会有错误的碰撞)。

简单例如-

int primes[] = 2, 3, 5, 7, ... // can be auto generated with a simple code

inline int prime_map(char c) 
    // check c is in legal char set bounds
    return primes[c - first_char];


...
char* word = get_next_word();
char* ptr = word;
int key = 1;
while (*ptr != NULL) 
    key *= prime_map(*ptr);
    ptr++;

hash[key].add_to_list(word); 

[编辑]

关于唯一性的几句话 - 任何整数都有一个分解为素数乘法,所以给定哈希中的整数键,您实际上可以重建所有可能的字符串,这些字符串将散列到它,只有这些词。只需分解为素数 p1^n1*p2^n2*... 并将每个素数转换为匹配的字符。 p1 的字符将出现 n1 次,依此类推。 你不能得到任何你没有明确使用过的新素数,成为素数意味着你不能通过任何其他素数的乘法得到它。

这带来了另一个可能的改进——如果你可以构造字符串,你只需要标记你在填充哈希时看到的排列。由于排列可以按字典顺序排序,因此您可以用一个数字替换每个排列。这将节省在散列中存储实际字符串的空间,但需要更多的计算,因此它不一定是一个好的设计选择。尽管如此,它还是使最初的面试问题变得很复杂:)

【讨论】:

我认为这是正确的解决方案。我用谷歌搜索并找到了类似的解决方案。你能给出选择素数和乘法的数据点吗?如果你能指出任何文件,我会很高兴 好吧,我不熟悉“官方”解决方案,所以我不知道这里是否有任何陷阱,但我认为任何素数都可以,所以从这里选择 - @ 987654321@ ,或者使用 sieve 方法创建它们。 请记住,如果您使用常规的 32 位或 64 位数字,您将很快发生溢出。您可能需要考虑另一种存储号码的方法以避免此问题。 好点,因为你有 26 个字母,所以你需要的最高质数是 101,所以如果你不限于 9 个字母的单词,你可能需要一个大数字库 一个 64 位整数只能表示 2^64 个不同的值(这里使用了非常小的一组),但字谜的空间是无限的。【参考方案2】:

哈希函数:为每个字符分配主要数字。在计算哈希码时,获取分配给该字符的素数并乘以现有值。现在所有字谜产生相同的哈希值。

例如: a2, c - 3 t - 7

猫的哈希值 = 3*2*7 = 42 act 的哈希值 = 2*3*7 = 42 打印所有具有相同哈希值的字符串(字谜将具有相同的哈希值)

【讨论】:

如果字符串是 'nc' 这会中断,因为哈希值仍然会产生 42,但这次会是 14('n') * 3('c')。 @deepkataria 你可以有数字的总和和数字的刺激,在这种情况下 7*3*2 =42 和 7+3+2 = 12 。然后这两个值需要与另一个匹配相同长度的字符集..但是如果字符串的长度太大,这种方法会中断,因为这些数字的乘积会导致溢出。【参考方案3】:

其他海报建议将字符转换为质数并将它们相乘。如果你对一个大素数做这个模,你会得到一个不会溢出的好散列函数。我针对大多数英语单词的 Unix 单词列表测试了以下 Ruby 代码,发现不是彼此字谜的单词之间没有哈希冲突。 (在 MAC OS X 上,此文件位于:/usr/share/dict/words。)

我的 word_hash 函数采用每个字符 mod 32 的序数值。这将确保大写和小写字母具有相同的代码。我使用的大素数是 2^58 - 27。只要小于 2^64 / A,任何大素数都可以,其中 A 是我的字母大小。我使用 32 作为我的字母大小,所以这意味着我不能使用大于大约 2^59 - 1 的数字。因为 ruby​​ 使用一位作为符号,另一位表示该值是数字还是对象,我比其他语言输了一点。

def word_hash(w)
  # 32 prime numbers so we can use x.ord % 32. Doing this, 'A' and 'a' get the same hash value, 'B' matches 'b', etc for all the upper and lower cased characters.
  # Punctuation gets assigned values that overlap the letters, but we don't care about that much.
  primes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131]
  # Use a large prime number as modulus. It must be small enough so that it will not overflow if multiplied by 32 (2^5). 2^64 / 2^5 equals 2^59, so we go a little lower.
  prime_modulus = (1 << 58) - 27
  h = w.chars.reduce(1)  |memo,letter| memo * primes[letter.ord % 32] % prime_modulus; 
end

words = (IO.readlines "/usr/share/dict/words").map|word| word.downcase.chomp.uniq
wordcount = words.size
anagramcount = words.map  |w| w.chars.sort.join .uniq.count

whash = 
inverse_hash = 
words.each do |w|
  h = word_hash(w)
  whash[w] = h
  x = inverse_hash[h]
  if x && x.each_char.sort.join != w.each_char.sort.join
    puts "Collision between #w and #x"
  else
    inverse_hash[h] = w
  end
end
hashcount = whash.values.uniq.size
puts "Unique words (ignoring capitalization) = #wordcount. Unique anagrams = #anagramcount. Unique hash values = #hashcount."

【讨论】:

有人能解释一下 prime_modulus 吗?我也不明白为什么我们特别关心乘以 32 时的溢出。 prime_modulus 有两个用途:(1) 它足够小以防止 64 位整数溢出 (2) 它必须与所使用的其他质数互质,这样当我们取模时对于互为字谜的单词,我们不会产生不同的哈希码。这意味着无论字母以何种顺序出现,它们都会给出相同的哈希码。因此,READ、DARE 和 DEAR 都以相同的代码结束。 谢谢。你知道有什么好的资源来了解更多吗?我是个白痴。【参考方案4】:

实用的小优化,对于上面的hash方法我建议是:

将最少的素数分配给元音,然后分配最频繁出现的辅音。 前任 : : 2 一个:3 我:5 Ø : 7 你:11 时间:13 等等……

另外,英语的平均字长是:~ 6

另外,前 26 个素数小于 100 [2,3,5,7, .., 97]

因此,平均而言,您的哈希会产生大约 100^6 = 10^12 的值。

因此,如果您对大于 10^12 的模数取质数,那么碰撞的可能性就会非常小。

【讨论】:

前 26 个素数小于 100 - 第 26 个素数是 101。en.wikipedia.org/wiki/…【参考方案5】:

上面的复杂性似乎很错位!您不需要素数或散列。这只是三个简单的操作:

    映射每个 OriginalWord 到 (SortedWord, OriginalWord) 的元组。 例子:"cat" 变成 ("act", "cat"); "dog" 变成 ("dgo", "dog")。 这是对每个 OriginalWord 字符的简单排序。 按元组的第一个元素排序示例:("dgo", "dog"), ("act, "cat") 排序为 ("act", "cat"), ("dgo", "dog")。是对整个集合的简单排序。 迭代通过元组(按顺序),发出 OriginalWord。 示例:("act", "cat"), ("dgo", "dog") 发出 "cat" "dog"。 这是一个简单的迭代。

只需要两次迭代和两种排序!

在 Scala 中,正是一行代码

val words = List("act", "animal", "dog", "cat", "elvis", "lead", "deal", "lives", "flea", "silent", "leaf", "listen")

words.map(w => (w.toList.sorted.mkString, w)).sorted.map(_._2)
# Returns: List(animal, act, cat, deal, lead, flea, leaf, dog, listen, silent, elvis, lives)

或者,正如最初的问题所暗示的那样,您只需要计数 > 1 的情况,它只是多一点:

scala> words.map(w => (w.toList.sorted.mkString, w)).groupBy(_._1).filter(case (k,v) => v.size > 1).mapValues(_.map(_._2)).values.toList.sortBy(_.head)
res64: List[List[String]] = List(List(act, cat), List(elvis, lives), List(flea, leaf), List(lead, deal), List(silent, listen))

【讨论】:

我喜欢这里的简单性作为一种实用的解决方案,但 OP 提到这是为了采访 - 这是直接来自 CTCI,所以他们很可能是从 Big-O 的角度来看这个。对每个单词最大 W 长度的 S 单词进行排序不是 O(S * WlogW) 操作吗?然后我们需要 O(SlogS) 来对元组进行排序? Vs 将所有单词散列到字典中,然后从字典中打印出来,这将是 O(S * 散列成本),并且散列成本不应该超过 W?如果我在这里遗漏了什么,请告诉我 - ty! @SlugFrisco 否。问题要求对答案进行排序,因此无论如何您都需要最终的O(SlogS) 排序。至于WlogW 排序,如果词是自然词(每个少于几十个字符),则排序是微不足道的(常数因子将使任何O 黯然失色)。而且,如果字符串很长(超过几十个字符),那么数千个素数的乘法将复杂得多:即使是 100 个字符的字符串也需要比 1 googol 大得多的乘积。跨度> 啊,很公平——问题是“一起打印字谜”——我不认为它们必须按排序顺序排列,尽管我看到 OP 的输出确实是按字母顺序排列的,所以这是合理的假设。 WlogW 的公平点对于大多数单词来说通常也是微不足道的。感谢回复!【参考方案6】:

使用素数乘积的解决方案非常棒,这里有一个 Java 实现,以防任何人都需要。

class HashUtility 
    private int n;
    private Map<Character, Integer> primeMap;

    public HashUtility(int n) 
        this.n = n;
        this.primeMap = new HashMap<>();
        constructPrimeMap();
    

    /**
     * Utility to check if the passed @code number is a prime.
     *
     * @param number The number which is checked to be prime.
     * @return @link boolean value representing the prime nature of the number.
     */
    private boolean isPrime(int number) 
        if (number <= 2)
            return number == 2;
        else
            return (number % 2) != 0
                    &&
                    IntStream.rangeClosed(3, (int) Math.sqrt(number))
                            .filter(n -> n % 2 != 0)
                            .noneMatch(n -> (number % n == 0));
    

    /**
     * Maps all first @code n primes to the letters of the given language.
     */
    private void constructPrimeMap() 
        List<Integer> primes = IntStream.range(2, Integer.MAX_VALUE)
                .filter(this::isPrime)
                .limit(this.n)      //Limit the number of primes here
                .boxed()
                .collect(Collectors.toList());

        int curAlphabet = 0;
        for (int i : primes) 
            this.primeMap.put((char) ('a' + curAlphabet++), i);
        
    

    /**
     * We calculate the hashcode of a word by calculating the product of each character mapping prime. This works since
     * the product of 2 primes is unique from the products of any other primes.
     * <p>
     * Since the hashcode can be huge, we return it modulo a large prime.
     *
     * @param word The @link String to be hashed.
     * @return @link int representing the prime hashcode associated with the @code word
     */
    public int hashCode(String word) 
        long primeProduct = 1;
        long mod = 100000007;
        for (char currentCharacter : word.toCharArray()) 
            primeProduct *= this.primeMap.get(currentCharacter) % mod;
        

        return (int) primeProduct;
    

如果/如何改进,请告诉我。

【讨论】:

【参考方案7】:

我们可以使用数组的二进制值表示。此代码 sn-p 假设所有字符都是小拉丁字符。

public int hashCode() 
    //TODO: so that each set of anagram generates same hashCode
    int sLen = s.length();
    int [] ref = new int[26];
    for(int i=0; i< sLen; i++) 
      ref[s.charAt(i) - 'a'] +=1;
    
    int hashCode = 0;
    for(int i= 0; i < ref.length; i++) 
      hashCode += new Double(Math.pow(2, i)).intValue() * ref[i];
    
    return hashCode;
  

【讨论】:

以上是关于为所有字谜生成相同的唯一哈希码的主要内容,如果未能解决你的问题,请参考以下文章

如何为android中的字符串输入生成唯一的哈希码...?

面试题:来,问你几个关于HashMap的问题?

基于对象的某些属性创建唯一的哈希码[重复]

哈希索引

如果两个对象的哈希码相同则他们不一定相同,如果对象一致则哈希码一定相同

当许多键具有相同的哈希码时,Java 8 的 HashMap 如何退化为平衡树?