查找数组中每个字符串的最小唯一子字符串

时间:2012-06-28 13:13:52

标签: arrays string algorithm unique substring

(我在JavaScript的上下文中写这个,但会接受任何语言的算法正确答案)

如何在字符串数组中找到每个元素的最短子字符串,其中子字符串不包含在任何其他元素中,忽略大小写?

假设我有一个输入数组,例如:

var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];

输出应该是这样的:

var uniqueNames = ["ne", "h", "ua", "ka", "i", "r"];

出于我的目的,您可以安全地假设没有元素将完全包含在另一个元素中。

我的想法:
似乎人们可能会蛮力这样做:

var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];
var uniqueNames = [], nameInd, windowSize, substrInd, substr, otherNameInd, foundMatch;
// For each name
for (nameInd = 0; nameInd < names.length; nameInd++)
{
    var name = names[nameInd];
    // For each possible substring length
    windowLoop:
    for (windowSize = 1; windowSize <= name.length; windowSize++)
    {
        // For each starting index of a substring
        for (substrInd = 0; substrInd <= name.length-windowSize; substrInd++)
        {
            substr = name.substring(substrInd,substrInd+windowSize).toLowerCase();
            foundMatch = false;
            // For each other name
            for (otherNameInd = 0; otherNameInd < names.length; otherNameInd++)
            {
                if (nameInd != otherNameInd && names[otherNameInd].toLowerCase().indexOf(substr) > -1)
                {
                    foundMatch = true;
                    break;
                }
            }

            if (!foundMatch)
            {
                // This substr works!
                uniqueNames[nameInd] = substr;
                break windowLoop;
            }
        }
    }
}

但我必须想象使用尝试/前缀树,后缀数组或类似的有趣内容的更优雅的解决方案。

修改: 我相信这是所选答案在JavaScript中以编程方式进行的形式:

var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];
var uniqueNames = [], permutations = {}, permutation, nameInd, windowSize, substrInd, substr;

// For each name
for (nameInd = 0; nameInd < names.length; nameInd++)
{
    var name = names[nameInd];
    // For each possible substring length
    windowLoop:
    for (windowSize = 1; windowSize <= name.length; windowSize++)
    {
        // For each starting index of a substring
        for (substrInd = 0; substrInd <= name.length-windowSize; substrInd++)
        {
            substr = name.substring(substrInd,substrInd+windowSize).toLowerCase();
            permutations[substr] = (typeof permutations[substr] === "undefined")?nameInd:-1;
        }
    }
}

for (substr in permutations)
{
    permutation = permutations[substr];
    if (permutation !== -1 && ((typeof uniqueNames[permutation] === "string" && substr.length < uniqueNames[permutation].length) || typeof uniqueNames[permutation] === "undefined"))
    {
        uniqueNames[permutation] = substr;
    }
}

4 个答案:

答案 0 :(得分:3)

这个问题可以用 O(N * L * L * L)复杂度来解决。该方法将使用后缀尝试。 trie的每个节点也将存储前缀计数,该前缀计数将指向从根遍历到该节点时形成的子字符串出现在所有插入到目前为止的后缀中的次数。

我们将构建 N + 1 次尝试。第一个trie将是全局的,我们将所有 N 字符串的所有后缀插入其中。对于包含相应后缀的每个 N 字符串,下一次 N 次尝试将是本地的。

构建尝试的预处理步骤将在 O(N * L * L)中完成。

现在,一旦构造了尝试,对于每个字符串,我们可以开始查询在全局特里结构中出现子字符串(从最小长度开始)和对应于该字符串的特里结构的次数。如果两者都相同则表示它不包含在除自身之外的任何其他字符串中。这可以用 O(N * L * L * L)来实现。复杂性可以解释为每个字符串的N,用于考虑每个子字符串的L * L和用于在trie中执行查询的L.

答案 1 :(得分:2)

Say N是字符串数,L是字符串的最大长度。您正在进行N*L*L*N次迭代。

我只能通过交换一次迭代以获得额外的内存来改善它。对于每个可能的子字符串长度(L次迭代),

  • 枚举每个名称(N*L)中该长度的所有子串,并将其与名称的索引一起存储到哈希表(1)中。如果已存在此子字符串的索引,您知道它将不起作用,那么您将index替换为某些特殊值,例如-1

  • 遍历哈希表,获取索引不是-1的子字符串 - 这是其相应索引的答案,但只有在名称尚未得到更短答案时才使用它们上一次迭代

通过将引用存储回现有字符串而不是复制子字符串,可以大大减少内存使用量。

答案 2 :(得分:2)

如果你构建一个通用后缀树,你只需要找到每个字符串的中缀从其他字符串的中缀分支的最浅点,并将标签带到该分支点加上一个“区别”字符。踢球者必须有这样一个额外的角色(它可能只是在每个字符串末端的元字符处分支),并且分支点可能不会导致叶子,它可能会导致子树所有来自相同字符串的叶子(因此必须考虑内部节点)。

对于每个字符串S,找到最浅(通过父标签深度)节点N,该节点N仅包含来自S的叶子,并且其边缘标签包含至少一个字符。从根到N的父节点的路径标签加上通向N的边缘标签中的一个字符,是在其他字符串中找不到的最短的中缀。

我认为在构造期间或通过GST的O(N)扫描可以完成仅包含来自一个字符串的叶子的节点的标记;那么扫描最终树并为每个字符串保持最小运行是一件简单的事情。所以这都是O(N)。

(编辑 - 我无法回复评论)

为了澄清,后缀树中的每个后缀都有一个节点,它与其他后缀分开;这里的目标是找到每个字符串的/ a后缀,该字符串从最小深度的所有其他字符串的后缀分支出来,由该节点的路径标签测量。我们需要的只是在该点之后的一个额外字符,以使子字符串不出现在任何其他字符串中。

示例:

字符串:abbc,abc

使用Ukonnen的算法,在第一个字符串之后,我们有一个后缀树,后缀只有该字符串的后缀;我在这里用[1]标记它们:

abbc[1]
b
 bc[1]
 c[1]
c[1]

接下来我们插入字符串2的后缀:

ab
  bc[1]
  c[2]
b
 bc[1]
 c
  [1]
  [2]
c
 [1]
 [2]

现在我们想找到最短的字符串,它导致一个只有[1]的分支;我们可以通过扫描所有[1]并查看他们的直接父母来做到这一点,我将在此列出路径标签,加上一个字符(我将在下面使用):

abbc:  abb
bbc: bb
bc: bc[1]
c: c[1]

请注意,我已经包含[1],因为它是区分[1]和[2]的其他相同后缀的元字符。这在识别在多个字符串中重复的子串时很方便,但它对我们的问题没有用,因为如果我们删除[1],我们最终会得到[2]中出现的字符串,即它不是候选字符。

现在,右边没有任何标签出现在任何其他字符串中,所以我们选择最短的标签,不包括元字符,即bb。

同样,第二个字符串有这些候选字符:

abc: abc
bc: bc[2]
c: c[2]

最后只有一个没有元字符,所以我们必须使用abc。

我的最后一点是,每个字符串的最小发现不必一次一个地发生;可以扫描GST一次,将节点标记为包含来自一个字符串([1],[2],.. [n])或“混合”的叶子,然后是每个字符串最小的非共享字符串(我会称这些“区分中缀”)也可以一次性计算。

答案 3 :(得分:-1)

   for(String s : strArr) { //O(n)
      //Assume the given string as shortest and override with shortest
       result.put(s, s);   
       for(int i = 0; i < s.length(); i++) { // O(m)              
          for (int j = i + 1; j <=s.length(); j++) {
               String subStr = s.substring(i, j);
               boolean isValid = true;
               for(String str2: strArr) { // O(n)
                   if(str2.equals(s)) // Same string cannot be a substring
                     continue;
                     
                    if(str2.contains(subStr)) {
                        isValid = false;
                        break;
                    }
               }

               if(isValid && subStr.length() < result.get(s).length()) 
                   result.put(s, subStr);
           }
        } 
   } 
    
   return result;