如何有效地在字符串集合中找到指定长度的相同子字符串?

时间:2018-09-26 03:08:24

标签: string algorithm

我有一个集合S,通常包含10-50个长字符串。出于说明目的,假设每个字符串的长度在1000到10000个字符之间。

我想找到指定长度k(通常在5到20之间)的字符串,它们是S中每个字符串的子字符串。显然,这可以使用幼稚的方法来完成-枚举S[0]中的每个k长度的子字符串,并检查它们是否存在于S的每个其他元素中。

有没有更有效的方法来解决问题?据我所知,这个问题和最长的公共子序列问题之间有一些相似之处,但是我对LCS的理解是有限的,我不确定它如何适应将所需的公共子串长度限制为k,或者是否可以将子序列技术应用于查找子字符串。

4 个答案:

答案 0 :(得分:2)

这是一个相当简单的算法,应该相当快。

  1. 像在rolling hash中一样使用Rabin-Karp string search algorithm,构造{{1}的所有H0|S0|-k+1子串的哈希表k }}。这大致是S0,因为每个哈希都是从前一个哈希在O(1)中计算的,但是如果存在冲突或重复的子字符串,则将花费更长的时间。使用更好的哈希可以帮助您解决冲突,但是如果O(|S0|)中有很多k长度的重复子字符串,那么您最终可能会使用S0

  2. 现在在O(k|S0|)上使用相同的滚动哈希。这次,在S1中查找每个子字符串,如果找到它,则将其从H0中删除,并将其插入新表H0中。同样,除非您有一些病理情况,例如H1O(|S1|)都是相同字符的长重复,否则应该在S0附近。 (如果S1S0是相同的字符串,或者有很多重叠的部分,那么它也会是次优的。)

  3. 为每个S0重复步骤2,每次创建一个新的哈希表。 (在第2步的每次迭代结束时,您可以从上一步中删除哈希表。)

最后,最后一个哈希表将包含所有公共的Si长度的子字符串。

总运行时间应约为k,但在最坏的情况下可能为O(Σ|Si|)。即使如此,对于上述问题大小,它应该在可接受的时间内运行。

答案 1 :(得分:1)

我会将每个长字符串视为重叠的短字符串的集合,因此ABCDEFGHI变为ABCDE,BCDEF,CDEFG,DEFGH,EFGHI。您可以将每个短字符串表示为一对索引,一个索引指定长字符串,而一个则指定该字符串的起始偏移量(如果这样会使您天真的话,请跳到最后)。

然后我将每个集合按升序排序。

现在,您可以通过合并索引的排序列表来查找前两个集合共有的短字符串,仅保留第一个集合中也存在于第二个集合中的那些。对照第三个集合检查此幸存者,依此类推,最后的幸存者对应于所有长字符串中存在的那些短字符串。

(或者,您可以在每个排序后的列表中维护一组指针,并反复查看每个指针是否指向带有相同文本的短字符串,然后前进指向最小的短字符串的指针)。

时间占主导地位的初始排序是O(n log n)。最坏的情况-例如当每个字符串都是AAAAAAAAA..AA时,在此之上还有一个系数k,因为所有字符串比较都会检查所有字符并花费时间k。希望通过https://en.wikipedia.org/wiki/Suffix_array有一个巧妙的解决方法,它使您可以按时间O(n)而不是O(nk log n)和https://en.wikipedia.org/wiki/LCP_array进行排序,这应该允许您跳过一些字符比较来自不同后缀数组的子字符串时。

再次考虑这个问题,我认为将所有有问题的字符串连接在一起的通常的后缀数组技巧在这里起作用,这些字符串之间没有一个字符分隔。如果查看结果后缀数组的LCP,则可以将其拆分为多个部分,并在后缀之间的差异小于k个字符的点处进行拆分。现在,任何特定部分中的每个偏移量都以相同的k个字符开头。现在查看每个部分中的偏移量,并检查是否与每个可能的起始字符串都至少有一个偏移量。如果是这样,则此k字符序列出现在所有起始字符串中,但并非如此。 (有一些后缀数组结构可以处理任意大的字母,因此,如有必要,您始终可以扩展字母以产生不包含任何字符串的字符)。

答案 2 :(得分:1)

一些想法(N是字符串数,M是平均长度,K是需要的子字符串大小):

方法1:

遍历所有字符串,计算k个长度的字符串的滚动哈希并将这些哈希存储在地图中(存储元组{key: hash; string_num; position}

时间O(NxM),空间O(NxM)

提取具有相同哈希值的组,请逐步检查:
    1)组的大小> =字符串数
    2)所有字符串都在该组3中表示。
    3)彻底检查真实子字符串是否相等(有时不同子字符串的哈希值可能会重合)

方法2:

为每个字符串构建后缀数组

时间O(N x MlogM)空间O(N x M)

使用类合并方法(对后缀进行排序),仅考虑长度为k的部分后缀,找到第一个字符串对的后缀数组的交集,然后继续下一个字符串,依此类推

答案 3 :(得分:1)

我会尝试使用HashSet s的简单方法:

  1. 为S中的所有长字符串及其所有k字符串构建一个HashSet
  2. 按元素数量对集合进行排序。
  3. 扫描第一组。  在其他集合中查找术语。

第一步是处理每个长字符串中的重复项。 第二个确保最小数量的比较。

let getHashSet k (lstr:string) =
    let strs = System.Collections.Generic.HashSet<string>()
    for i in 0..lstr.Length - k do
        strs.Add lstr.[i..i + k - 1] |> ignore
    strs

let getCommons k lstrs =
    let strss = lstrs |> Seq.map (getHashSet k) |> Seq.sortBy (fun strs -> strs.Count)
    match strss |> Seq.tryHead with
    | None   -> [||]
    | Some h ->
    let rest = Seq.tail strss |> Seq.toArray
    [|  for s in h do
            if rest |> Array.forall (fun strs -> strs.Contains s) then yield s
    |]

测试:

let random = System.Random System.DateTime.Now.Millisecond
let generateString n =
    [|  for i in 1..n do
            yield random.Next 20 |> (+) 65 |> System.Convert.ToByte
    |] |> System.Text.Encoding.ASCII.GetString


[ for i in 1..3 do yield generateString 10000 ]
|> getCommons 4
|> fun l -> printfn "found %d\n %A" l.Length l

结果:

found 40
[|"PPTD"; "KLNN"; "FTSR"; "CNBM"; "SSHG"; "SHGO"; "LEHS"; "BBPD"; "LKQP"; "PFPH";
"AMMS"; "BEPC"; "HIPL"; "PGBJ"; "DDMJ"; "MQNO"; "SOBJ"; "GLAG"; "GBOC"; "NSDI";
"JDDL"; "OOJO"; "NETT"; "TAQN"; "DHME"; "AHDR"; "QHTS"; "TRQO"; "DHPM"; "HIMD";
"NHGH"; "EARK"; "ELNF"; "ADKE"; "DQCC"; "GKJA"; "ASME"; "KFGM"; "AMKE"; "JJLJ"|]

这里是小提琴:https://dotnetfiddle.net/ZK8DCT