假设有一个单词集,我想根据他们的char包(multiset)对它们进行聚类。例如
{tea,eat,abba,aabb,hello}
将聚集到
{{tea,eat},{abba,aabb},{hello}}。
abba
和aabb
聚集在一起,因为它们具有相同的char包,即两个a
和两个b
。
为了提高效率,我能想到的一种天真的方法是将每个单词转换为char-cnt系列,例如,abba
和aabb
将转换为{{1} },tea / eat将转换为a2b2
。这样我就可以构建一个字典并用相同的键组合单词。
这里有两个问题:首先我必须对字符进行排序以构建密钥;第二,字符串键看起来很笨拙,性能不如char / int键。
有没有更有效的方法来解决问题?
答案 0 :(得分:2)
为了检测字谜,您可以使用基于素数A->2, B->3, C->5
等产品的散列方案。将给出“abba”==“aabb”== 36(但是对于primenumber映射的不同字母会更好)
请参阅我的回答here。
答案 1 :(得分:1)
由于您要对单词进行排序,我假设所有字符ascii值都在0-255范围内。然后你可以对单词进行Counting Sort。
计数排序将花费与输入单词大小相同的时间。从计数排序中获得的字符串的重建将需要O(wordlen)。你不能使这个步骤小于O(wordLen),因为你必须至少迭代一次字符串,即O(wordLen)。没有预定义的订单。如果不迭代该单词中的所有字符,则不能对该单词做任何假设。传统的排序实现(即基于比较的实现)将为您提供O(n * lg n)。但是非比较的给你O(n)。
迭代列表中的所有单词,并使用我们的计数排序对它们进行排序。保留一张地图 将单词排序到他们映射的已知单词列表中。向列表添加元素需要恒定的时间。总的来说,算法的复杂性是O(n * avgWordLength)。
以下是一个示例实现
import java.util.ArrayList;
public class ClusterGen {
static String sortWord(String w) {
int freq[] = new int[256];
for (char c : w.toCharArray()) {
freq[c]++;
}
StringBuilder sortedWord = new StringBuilder();
//It is at most O(n)
for (int i = 0; i < freq.length; ++i) {
for (int j = 0; j < freq[i]; ++j) {
sortedWord.append((char)i);
}
}
return sortedWord.toString();
}
static Map<String, List<String>> cluster(List<String> words) {
Map<String, List<String>> allClusters = new HashMap<String, List<String>>();
for (String word : words) {
String sortedWord = sortWord(word);
List<String> cluster = allClusters.get(sortedWord);
if (cluster == null) {
cluster = new ArrayList<String>();
}
cluster.add(word);
allClusters.put(sortedWord, cluster);
}
return allClusters;
}
public static void main(String[] args) {
System.out.println(cluster(Arrays.asList("tea", "eat", "abba", "aabb", "hello")));
System.out.println(cluster(Arrays.asList("moon", "bat", "meal", "tab", "male")));
}
}
返回
{aabb=[abba, aabb], ehllo=[hello], aet=[tea, eat]}
{abt=[bat, tab], aelm=[meal, male], mnoo=[moon]}
答案 2 :(得分:1)
使用x个字符的字母和y的最大字长,您可以创建(x + y)位的散列,以便每个anagram都有一个唯一的散列。某个值为1表示当前字母为另一个,值为0表示继续下一个字母。这是一个显示其工作原理的示例:
假设我们有一个7个字母的字母(abcdefg),最大字长为4.每个字哈希都是11位。让我们散列“淡入淡出”这个词:10001010100
第一位是1,表示存在一个。第二位表示没有更多的。第三位表示没有更多的b,依此类推。考虑这个问题的另一种方法是连续的数字表示该字母的数量,并且该字符串之前的总零数表示它是哪个字母。
这是“dada”的哈希值:11000110000
值得注意的是,因为可能的哈希值和可能的字谜之间存在一对一的对应关系,所以这是可保证为任何输入提供唯一哈希值的最小可能哈希值,这样就无需检查桶中的所有内容。你完成了哈希。
我很清楚使用大字母和长字会导致大的哈希值。此解决方案旨在保证唯一的哈希值,以避免比较字符串。如果你可以设计一个算法来在恒定时间内计算这个哈希值(假设你知道x和y的值)那么你将能够在O(n)中解决整个分组问题。
答案 3 :(得分:0)
我会分两步完成,首先根据长度对所有单词进行排序,然后分别处理每个子集(这是为了避免以后出现大量重叠)。
下一步更难,有很多方法可以做到。最简单的一种方法是为每个字母分配一个数字(例如a = 1,b = 2等)并将每个字的所有值相加,从而将每个字分配给一个整数。然后,您可以根据此整数值对单词进行排序,从而大大减少您需要比较的数字。
根据您的数据集,您可能仍然有很多重叠(“坏”和“cac”将生成相同的整数哈希),因此您可能需要设置一个阈值,如果您在一个存储桶中有太多单词,用另一个哈希重复上一步(只是为字母分配不同的数字)除非有人查看了你的代码并设计了一个词表来搞砸你,否则这应该将重叠减少到几乎没有。
请记住,当您希望将少量单词放在同一个char包中时,这种方法会很有效。如果你的数据是很多长字只能进入几个字符包,那么你在最后一步中进行的比较次数就会是天文数字,在这种情况下你最好使用像你描述的方法一样的方法。 - 没有可能重叠的那个。
答案 4 :(得分:0)
我做过的一件事与此类似,但允许碰撞,就是对字母进行排序,然后去除重复。所以在你的例子中,你有“aet”,“ab”和“ehlo”的桶。
现在,正如我所说,这允许碰撞。所以“杆”和“门”都落在同一个桶里,这可能不是你想要的。但是,碰撞将是一个很容易快速搜索的小集合。
因此,一旦你有一个桶的字符串,你会发现你可以将它转换为32位整数(至少对于ASCII)。字符串中的每个字母都变成32位整数。因此“a”是第一位,“b”是第二位,等等。所有(英语)单词构成一个具有26位标识符的存储桶。然后,您可以执行非常快速的整数比较来查找新单词进入的存储桶,或者查找现有单词所在的存储桶。
答案 5 :(得分:0)
计算每个字符串中字符的频率,然后根据频率表构建一个哈希表。举例来说,对于字符串aczda
和aacdz
,我们得到20110000000000000000000001
。使用哈希表,我们可以在O(N)中的桶中对所有这些字符串进行分区。
答案 6 :(得分:0)
26位整数作为哈希函数
如果您的字母表不是太大,例如,只是小写英文字母,您可以为每个单词定义这个特定的散列函数:26位整数,其中每个位表示该单词中是否存在该英文字母。请注意,具有相同char集的两个单词将具有相同的散列。
然后将它们添加到哈希表中。它将通过哈希冲突自动聚类。
计算哈希需要O(max length of the word)
,并且插入哈希表是恒定时间。因此整体复杂性为O(max length of a word * number of words)