为所有字谜生成相同的唯一哈希码
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;
【讨论】:
以上是关于为所有字谜生成相同的唯一哈希码的主要内容,如果未能解决你的问题,请参考以下文章