【问题标题】:clustering words based on their char set根据字符集对单词进行聚类
【发布时间】:2013-08-12 15:20:10
【问题描述】:

假设有一个词集,我想根据它们的字符包(多集)对它们进行聚类。例如

{茶,吃,阿爸,阿布,你好}

会聚成

{{茶,吃},{abba,aabb},{你好}}。

abbaaabb 聚集在一起,因为它们具有相同的字符包,即两个a 和两个b

为了提高效率,我能想到的一个幼稚的方法是将每个单词转换成一个char-cnt系列,例如abbaaabb都会被转换成a2b2,tea/eat会转换为a1e1t1。这样我就可以构建字典并使用相同的键对单词进行分组。

这里有两个问题:首先我必须对字符进行排序以构建密钥;其次,字符串键看起来很别扭,性能不如char/int键。

有没有更有效的方法来解决问题?

【问题讨论】:

  • 我会考虑中间的:对关键字符串的字符进行排序,但不要对其进行 RLE 压缩,所以 abbaaabb 都会出现为 aabb .很容易做到,没有太多尴尬或“压缩”爆炸并使绳子变长的机会。
  • 您是否也需要能够检索原始字符串?
  • 我想知道这里有没有人听说过“字谜”这个词。
  • 正确的集合不包括计数;那么“meet”和“met”是由同一组字符组成的。
  • 技术上与Group together all the anagrams 重复,但是该问题被标记为与它没有重复的内容的重复。

标签: algorithm anagram


【解决方案1】:

为了检测字谜,您可以使用基于素数乘积的散列方案A->2, B->3, C->5 等将给出 "abba" == "aabb" == 36(但不同的字母到素数映射会更好) 看我的回答here

【讨论】:

  • 不要在素数列表中使用 2。当您遇到溢出情况时,将一个数字乘以 2 会将两个值映射到一个结果(一个值从顶部移出 0,另一个值从顶部移出)。如果你乘以任何奇数,那么你总是会得到一个 1:1 的映射模式 2**n,这样你就不会丢失信息。看我的回答here
  • 哦,我看到你已经在你的另一篇文章中提出了这个观点。我使用剩余的位来区分已经溢出的哈希值和没有溢出的哈希值,因为后者给出了明确的正面和负面结果(尽管许多溢出的哈希值可能仍然是唯一的,但我无法轻易证明它们中的任何一个)。
  • 正如我在原始答案中所述,我没有观察到使用 >> 100K 荷兰语单词列表(包括拼写错误等)的溢出;最大的单词长约 15 个字符,乘积为 64 位 unsigned long long int。使用最常见的字母 (e,t) 映射到最低素数的映射似乎足以使乘积保持在范围内。
  • 算术基本定理:每个整数都可以唯一地表示为素数的乘积(整数分解)。由于2 是第一个质数,因此它也适用。
  • @rook,但在modular arithmetic 中并非如此。
【解决方案2】:

由于您要对单词进行排序,我假设所有字符的 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]}

【讨论】:

    【解决方案3】:

    使用由 x 个字符组成的字母表和 y 的最大字长,您可以创建 (x + y) 位的散列,这样每个字谜都有一个唯一的散列。位的值为 1 表示当前有另一个字母,值为 0 表示移动到下一个字母。这是一个展示其工作原理的示例:

    假设我们有一个 7 个字母的字母表 (abcdefg),最大字长为 4。每个字哈希都是 11 位。让我们对“fade”这个词进行哈希处理:10001010100

    第一位是1,表示有礼物。第二位表示没有更多的a。第三位表示没有更多的 b,依此类推。另一种思考方式是,一行中的个数表示该字母的数量,而该字符串之前的总零表示它是哪个字母。

    这里是“dada”的哈希值:11000110000

    值得注意的是,由于可能的哈希值和可能的字谜之间存在一一对应关系,因此这是保证为任何输入提供唯一哈希值的最小可能哈希值,这样就无需检查存储桶中的所有内容你已经完成了哈希。

    我很清楚,使用大字母和长单词会导致散列大小变大。该解决方案旨在保证唯一的哈希值,以避免比较字符串。如果您可以设计一种算法来在恒定时间内计算此哈希(假设您知道 x 和 y 的值),那么您将能够在 O(n) 中解决整个分组问题。

    【讨论】:

      【解决方案4】:

      我会分两步完成,首先根据单词的长度对所有单词进行排序,然后分别处理每个子集(这是为了避免以后出现大量重叠。)

      下一步更难,有很多方法可以做到。最简单的一种方法是为每个字母分配一个数字(例如 a = 1、b = 2 等)并将每个单词的所有值相加,从而将每个单词分配给一个整数。然后你可以根据这个整数值对单词进行排序,这大大减少了你必须比较的数字。

      根据您的数据集,您可能仍然有很多重叠(“bad”和“cac”会生成相同的整数哈希),因此您可能需要设置一个阈值,如果您在一个桶中有太多单词,您可以用另一个哈希重复上一步(只是为字母分配不同的数字)除非有人查看了您的代码并设计了一个单词列表来搞砸您,否则这应该将重叠减少到几乎没有。

      请记住,当您期望少量单词在同一个字符包中时,这种方法会很有效。如果您的数据是很多只进入几个字符包的长词,那么您在最后一步中进行的比较次数将是天文数字,在这种情况下,您最好使用您描述的方法- 一个不可能重叠的。

      【讨论】:

      • 谢谢。正如我所说,这是一个面试问题,面试官建议使用 O(n) 算法。计算每个单词的字符似乎是在正确的轨道上,但我不知道他心中的最终解决方案。
      • 是否有已知的最大字长?如果不是 O(n) 听起来就像是白日梦。
      • @flancor,假设你有一个任意精度的算术库,我已经看到了很多声称是 O(n) 的 SO 问题的解决方案。
      • 如果您更仔细地选择您的字母值映射,那么您可能会在很长一段时间内回答my related question关于如何优化与您的密切相关的解决方案。
      • @sh1 在解决这个问题后,我发布了一个您可能感兴趣的新解决方案。我相信它更接近 OP 正在寻找的东西,但它并未针对实际数据进行优化。
      【解决方案5】:

      我做过的一件事与此类似,但允许发生冲突,即对字母进行排序,然后删除重复项。因此,在您的示例中,您将拥有“aet”、“ab”和“ehlo”的存储桶。

      现在,正如我所说,这允许碰撞。所以“杆”和“门”最终都在同一个桶中,这可能不是你想要的。但是,碰撞将是一个小的集合,可以轻松快速地搜索。

      因此,一旦您有了存储桶的字符串,您就会发现可以将其转换为 32 位整数(至少对于 ASCII 而言)。字符串中的每个字母都变成 32 位整数中的一个位。所以“a”是第一位,“b”是第二位,依此类推。所有(英文)单词构成一个具有 26 位标识符的存储桶。然后,您可以进行非常快速的整数比较,以找到新词进入的桶,或找到现有词所在的桶。

      【讨论】:

      • 谢谢。这是一个面试问题,我实际上提出了你提到的解决方案。但是,面试官说它是通用字符集(可能是unicode),所以你不能将它转换为位向量并将其放入int/long中。
      • 我认为他只是想带你走另一条路。似乎他希望优化运行时复杂性,而不是像使用位向量这样的次要速度优化。至少已经发布了两个答案,它们都提出了相同的O(n) 解决方案(可以概括为“使用桶排序和哈希表”)。如果您不理解,您应该接受或提出问题。
      【解决方案6】:

      计算每个字符串中字符的频率,然后根据频率表构建一个哈希表。例如,对于字符串aczdaaacdz,我们得到20110000000000000000000001。使用哈希表,我们可以在 O(N) 的时间内将所有这些字符串分区。

      【讨论】:

        【解决方案7】:

        26 位整数作为哈希函数

        如果您的字母表不是太大,例如,只有小写英文字母,您可以为每个单词定义这个特定的哈希函数:一个 26 位整数,其中每个位表示该英文字母是否存在于单词中。请注意,具有相同字符集的两个单词将具有相同的哈希值。

        然后将它们添加到哈希表中。它将自动通过哈希冲突进行聚类。

        计算哈希需要O(max length of the word),插入哈希表是常数时间。所以整体复杂度是O(max length of a word * number of words)

        【讨论】:

          猜你喜欢
          • 2017-02-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-06-09
          • 2014-07-31
          • 2013-03-07
          • 2013-08-27
          • 1970-01-01
          相关资源
          最近更新 更多