对字谜词进行分组的算法

Posted

技术标签:

【中文标题】对字谜词进行分组的算法【英文标题】:Algorithm for grouping anagram words 【发布时间】:2010-09-28 14:27:13 【问题描述】:

给定一组单词,我们需要找到字谜单词并使用最佳算法单独显示每个类别。

输入:

man car kile arc none like

输出:

man
car arc
kile like
none

我现在正在开发的最佳解决方案是基于哈希表,但我正在考虑将变位词转换为整数值的方程式。

示例:man => 'm'+'a'+'n' 但这不会给出唯一值。

有什么建议吗?


请参阅以下 C# 代码:

string line = Console.ReadLine();
string []words=line.Split(' ');
int[] numbers = GetUniqueInts(words);
for (int i = 0; i < words.Length; i++)

    if (table.ContainsKey(numbers[i]))
    
        table[numbers[i]] = table[numbers[i]].Append(words[i]);
    
    else
    
        table.Add(numbers[i],new StringBuilder(words[i]));
    


问题是如何开发GetUniqueInts(string [])方法。

【问题讨论】:

所以你想要一个散列函数,它为不同顺序的相同字母组合返回相同的散列,每个字母组合都有一个唯一的散列(没有错误匹配)? 【参考方案1】:

根本不用担心自定义哈希函数。在您的平台上使用普通的字符串散列函数。重要的是让哈希表的键成为“排序词”的概念 - 单词按字母排序,因此“car”=>“acr”。所有字谜都有相同的“排序词”。

只需从“排序的单词”到“排序单词的单词列表”的哈希值。在 LINQ 中,这非常简单:

using System;
using System.Collections.Generic;
using System.Linq;

class FindAnagrams

    static void Main(string[] args)
    
        var lookup = args.ToLookup(word => SortLetters(word));

        foreach (var entry in lookup)
        
            foreach (var word in entry)
            
                Console.Write(word);
                Console.Write(" ");
            
            Console.WriteLine();
        
    

    static string SortLetters(string original)
    
        char[] letters = original.ToCharArray();
        Array.Sort(letters);
        return new string(letters);
    

使用示例:

c:\Users\Jon\Test>FindAnagrams.exe man car kile arc none like
man
car arc
kile like
none

【讨论】:

哇,这看起来很性感。比 c++ 版本短得多 '(:) 我不是在考虑自定义散列,而是使关键整数而不是对所有单词进行排序 我有兴趣查看这个与我的方案相比的性能数字,我认为我的方案应该更快地计算哈希值,因为它可以通过 O(N) 中的字符串进行 1 次传递来完成。 The sort if O(n log n) 但是,查找可能会更好我不确定我的哈希函数将如何分配值。 不要忘记这只是对每个单词进行排序 - 而真正字典中的单词非常短。复杂性与较大的 n 相关。 事实上,如果将其限制为 26 个字符,也可以很容易地在 O(n) 中完成排序。老实说,我的路线的主要优势是简单 - 它显然是正确的,无需过多担心数学问题。我喜欢简单的解决方案:)【参考方案2】:

我使用了一个受哥德尔启发的方案:

将素数 P_1 到 P_26 分配给字母(以任何顺序,但要获得较小的哈希值最好给普通字母小素数)。

构建单词中字母的直方图。

那么哈希值是每个字母的相关质数的乘积,乘以它的频率的幂。这为每个字谜赋予了独特的价值。

Python 代码:

primes = [2, 41, 37, 47, 3, 67, 71, 23, 5, 101, 61, 17, 19, 13, 31, 43, 97, 29, 11, 7, 73, 83, 79, 89, 59, 53]


def get_frequency_map(word):
    map = 

    for letter in word:
        map[letter] = map.get(letter, 0) + 1

    return map


def hash(word):
    map = get_frequency_map(word)
    product = 1
    for letter in map.iterkeys():
        product = product * primes[ord(letter)-97] ** map.get(letter, 0)
    return product

这巧妙地将寻找子字谜的棘手问题转化为分解大数的(也被称为棘手的)问题......

【讨论】:

不错!独特的素因数分解 FTW。 unicode输入怎么样?在这种情况下,对字符串进行排序和比较会赢:) 我喜欢这个答案。这很酷。我回答了这个问题,并查看了我工作所在公司的招聘问卷的答案。大多数人只会产生一个单词的字谜。而且我认为除了我之外没有人认真优化它。这个问题有很多可以炫耀的空间。 但是任意大的单词需要任意大的整数。您也可以使用排序后的词(或频率图)作为哈希键。 是的,这是标准也是最好的方法,面试官会期待/或对这个解决方案 +1 印象深刻。但应该注意它的缺点,正如@Roddy 提到的那样【参考方案3】:

一个用于咯咯笑的 Python 版本:

from collections import defaultdict
res = defaultdict(list)
L = "car, acr, bat, tab, get, cat".split(", ")

for w in L:
    res["".join(sorted(w))].append(w)

print(res.values())

【讨论】:

另外,在这里查看 namin 的排列算法:***.com/questions/396421/…【参考方案4】:

我认为您不会找到比具有自定义散列函数的散列表更好的东西(在散列之前对单词的字母进行排序)。

字母的总和永远不会起作用,因为你不能真正让 'ac' 和 'bb' 不同。

【讨论】:

是的,总和不起作用,但让我们看看一种将字谜单词转换为唯一数字的新方法 您没有直接考虑散列和唯一性。您无法使用散列函数保证唯一性,因此无论如何您都需要一种处理表中重复“命中”的方法。字母总和可能不是最佳哈希,但它仍然可以工作。 将素数分配给字母和变位词素数的乘积将帮助您建立哈希表。【参考方案5】:

您将需要大整数(或实际上是位向量),但以下可能有效

每个字母的第一次出现分配该字母的位数,第二次出现该字母的位数+ 26。

例如

#1 = 1 b #1 = 2 c#1 = 4 #2 = 2^26 b #2 = 2 ^ 27

然后您可以将这些加起来,根据单词的字母得到一个唯一的值。

您对单词值的存储要求是:

n * 26 位

其中 n 是任何重复字母的最大出现次数。

【讨论】:

拥有 26 个唯一值(2^0 到 2^25)是否足够,然后通过计算总和和其他一些交换函数(如 XOR)来比较单词?似乎应该足够了,但我无法提出令人信服的论点为什么...... :) 异或是否好取决于字典中单词的分布。不过,这是一个改进的好主意。唯一真正了解的方法是测试和测量两者。【参考方案6】:

我不会使用散列,因为它增加了查找和添加的额外复杂性。散列、排序和乘法都将比具有跟踪唯一性的简单的基于数组的直方图解决方案慢。最坏的情况是 O(2n):

// structured for clarity
static bool isAnagram(String s1, String s2)

    int[] histogram = new int[256];

    int uniques = 0;

    // scan first string
    foreach (int c in s1)
    
        // count occurrence
        int count = ++histogram[c];

        // count uniques
        if (count == 1)
        
            ++uniques;
        
    

    // scan second string
    foreach (int c in s2)
    
        // reverse count occurrence
        int count = --histogram[c];

        // reverse count uniques
        if (count == 0)
        
            --uniques;
        
        else if (count < 0) // trivial reject of longer strings or more occurrences
        
            return false;
        
    

    // final histogram unique count should be 0
    return (uniques == 0);

【讨论】:

O(2n)O(n) 相同。【参考方案7】:

我之前用一个简单的字母计数数组实现了这一点,例如:

unsigned char letter_frequency[26];

然后将其与每个单词一起存储在数据库表中。具有相同字母频率“签名”的单词是字谜,一个简单的 SQL 查询然后直接返回一个单词的所有字谜。

通过对一个非常大的字典进行一些实验,我发现没有任何单词超过任何字母的频率计数 9,因此“签名”可以表示为一串数字 0..9(大小可以很容易通过打包成十六进制字节减半,并通过二进制编码数字进一步减少,但到目前为止我并没有为此烦恼)。

这是一个 ruby​​ 函数,用于计算给定单词的签名并将其存储到 Hash 中,同时丢弃重复项。我后来从哈希中构建了一个 SQL 表:

def processword(word, downcase)
  word.chomp!
  word.squeeze!(" ") 
  word.chomp!(" ")
  if (downcase)
    word.downcase!
  end
  if ($dict[word]==nil) 
    stdword=word.downcase
    signature=$letters.collect |letter| stdword.count(letter)
    signature.each do |cnt|
      if (cnt>9)
        puts "Signature overflow:#word|#signature|#cnt"
      end
    end
    $dict[word]=[$wordid,signature]
    $wordid=$wordid+1
  end
end

【讨论】:

【参考方案8】:

为字母 a-z 分配一个唯一的素数

迭代您的单词数组,根据每个单词中的字母创建质数乘积。 将该产品与相应的单词一起存储在您的单词列表中。

对数组进行排序,按产品升序。

迭代数组,在每次产品更改时执行control break。

【讨论】:

【参考方案9】:

在 C 中,我刚刚实现了以下哈希,它基本上对字典中的单词是否包含特定字母进行 26 位位掩码。因此,所有字谜都具有相同的哈希值。哈希没有考虑重复的字母,所以会有一些额外的重载,但它仍然比我的 perl 实现更快。

#define BUCKETS 49999

struct bucket 
    char *word;
    struct bucket *next;
;

static struct bucket hash_table[BUCKETS];

static unsigned int hash_word(char *word)

    char *p = word;
    unsigned int hash = 0;

    while (*p) 
        if (*p < 97 || *p > 122) 
            return 0;
        
        hash |= 2 << (*p - 97);
        *p++;
    

    return hash % BUCKETS;

重载的桶创建并添加为链表等。然后只需编写一个函数,确保与哈希值匹配的单词长度相同,并且每个单词中的字母为 1 到 1 并将其作为匹配返回.

【讨论】:

【参考方案10】:

我将根据示例单词和其他我不关心的字母生成 hasmap。

例如,如果单词是“汽车” 我的哈希表将是这样的: 一,0 b,最大值 c,1 d,最大值 e,最大值 ... .. r,2 . 因此任何大于 3 的都将被视为不匹配

(更多调整...) 我的比较方法将比较哈希计算本身的哈希总数。一旦识别出单词不相等就不会继续。

public static HashMap<String, Integer> getHashMap(String word) 
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        String[] chars = word.split("");
        int index = 0;
        for (String c : chars) 
            map.put(c, index);
            index++;
        
        return map;
    

    public static int alphaHash(String word, int base,
            HashMap<String, Integer> map) 
        String[] chars = word.split("");
        int result = 0;
        for (String c : chars) 
            if (c.length() <= 0 || c.equals(null)) 
                continue;
            
            int index = 0;
            if (map.containsKey(c)) 
                index = map.get(c);
             else 
                index = Integer.MAX_VALUE;
            
            result += index;
            if (result > base) 
                return result;
            
        
        return result;
    

主要方法

  HashMap<String, Integer> map = getHashMap(sample);
        int sampleHash = alphaHash(sample, Integer.MAX_VALUE, map);
        for (String s : args) 
                if (sampleHash == alphaHash(s, sampleHash, map)) 
                    System.out.print(s + " ");
                
            

【讨论】:

【参考方案11】:

字谜可以通过以下方式找到:

    单词的长度应该匹配。 按照整数值执行每个字符的相加。如果您对 anagram 执行相同的操作,此总和将匹配。 根据整数值执行每个字符的乘法运算。如果您对 anagram 执行相同的操作,则评估值将匹配。

所以我想通过以上三个验证,我们可以找到字谜。如果我错了,请纠正我。


例如:abc cba

两个单词的长度都是3。

两个单词的单个字符之和为 294。

两个单词的单个字符的 Prod 是 941094。

【讨论】:

如果我的词是“zzzzzzzzzz”怎么办?那么产品将是7.3046314e+20。存储和计算这个值可能是一种压力。如果我们有更长的单词怎么办?考虑到这一点,这个解决方案是否有效?【参考方案12】:

除了其他有用的答案之外,只想添加简单的python解决方案:

def check_permutation_group(word_list):
    result = 

    for word in word_list:
        hash_arr_for_word = [0] * 128  # assuming standard ascii

        for char in word:
            char_int = ord(char)
            hash_arr_for_word[char_int] += 1

        hash_for_word = ''.join(str(item) for item in hash_arr_for_word)

        if not result.get(hash_for_word, None):
            result[str(hash_for_word)] = [word]
        else:
            result[str(hash_for_word)] += [word]

return list(result.values())

【讨论】:

【参考方案13】:

python 代码:

line = "man car kile arc none like"
hmap = 
for w in line.split():
  ws = ''.join(sorted(w))
  try:
    hmap[ws].append(w)
  except KeyError:
    hmap[ws] = [w]

for i in hmap:
   print hmap[i]

输出:

['car', 'arc']
['kile', 'like']
['none']
['man']

【讨论】:

【参考方案14】:

javascript 版本。使用散列。

时间复杂度:0(nm) ,其中 n 为单词数,m 为单词长度

var words = 'cat act mac tac ten cam net'.split(' '),
    hashMap = ;

words.forEach(function(w)
    w = w.split('').sort().join('');
    hashMap[w] = (hashMap[w]|0) + 1;
);

function print(obj,key) 
    console.log(key, obj[key]);


Object.keys(hashMap).forEach(print.bind(null,hashMap))

【讨论】:

不是 O(n),因为排序需要的时间不是固定的

以上是关于对字谜词进行分组的算法的主要内容,如果未能解决你的问题,请参考以下文章

Java 8 Stream 函数将字谜列表分组为列表映射

无法正确按字谜分组

模糊分组依据,对相似词进行分组

做字谜组的更好方法

通过 Python 查找和分组字谜

在列表中匹配和分组彼此相关(相关)的相似词