【问题标题】:Regex to find 'good enough' sequences正则表达式查找“​​足够好”的序列
【发布时间】:2013-06-27 13:08:21
【问题描述】:

我正在寻求实现一些算法来帮助我匹配不完美的序列。

假设我有一个存储的 ABBABABBA 序列,我想在大量字符流中找到“看起来像”的东西。

如果我允许我的算法有 2 个通配符(差异),我如何使用正则表达式匹配如下内容: where ( 和 ) 标记差异:

A(A)BABAB(A)A 
or 
(B)BBA(A)ABBA

我的困境是,我希望在一大串字符中找到这些潜在的目标匹配(有缺陷)。 所以像这样:

ABBDBABDBCBDBABDB(A(A)BABAB(A)A)DBDBABDBCBDBAB
ADBDBABDBDBDBCBDBABCBDBABCBDBABCBDBABABBBDBABABBCD
DBABCBDABDBABCBCBDBABABDABDBABCBDBABABDDABCBDBABAB

我必须能够搜索这些“足够接近”的匹配项。
其中括号表示:(The Good enough Match with the (Differences))

编辑:为了在这个例子中更正式,如果 N-2 个字符与原始字符相同(2 个差异),则可以接受长度为 N 的匹配

我以前使用过正则表达式,但只是为了找到完美的序列——而不是为了“看起来像”的东西。

希望这足够清楚,可以得到一些建议。 感谢您的阅读和任何帮助!

【问题讨论】:

  • 我认为要问的第一个问题是“什么是匹配?”它必须有一定的长度,包含特定的字符,包含特定的序列吗?
  • @Doc 在此示例中获取这些匹配项的情况下 - 匹配项与原始匹配项非常相似 - 但允许 2 个不正确的字符(通配符)。
  • 能否请您更正式和准确一些?更正式定义的一个示例是:如果至少有 N-2 个字符匹配,则匹配大小为 N 的目标序列。另一个正式的定义是:大小从 N-2 到 N+2 的目标序列匹配,如果它可以减少到不超过 2 次编辑操作(添加/删除/替换)的模式序列。
  • @Doc 这只是一个非常简单的例子,实际上我使用了 4 个唯一字符,为此 (ABCD)。我目前录制了一个签名,ABCDBD。然后我搜索看起来可能的字符样本(允许差异(通配符)),因此如果允许 1 个通配符,AB(B)DBD 是匹配的,ABCDB(B) 也是如此。带有 2 个通配符,A(A)(B)DBD 等。
  • 抱歉,我已经更新了原始帖子以显示我面临的问题的严重程度,感谢大家的帮助!

标签: c# regex


【解决方案1】:

您可以使用 LINQ 变得美观且富有表现力。

为了使用它,请确保您的代码顶部有一个using System.Linq

假设

  • source 是存储的目标模式
  • test 是要测试的字符串。

那你就可以了

public static bool IsValid(string source, string test) 
{
  return test != null  
         && source != null 
         && test.Length == source.Length 
         && test.Where((x,i) => source[i] != x).Count() <=2
}

还有一个快捷版本,在失败时退出 false,节省了对字符串其余部分的迭代。

public static bool IsValid(string source, string test) 
{
   return test != null  
          && source != null 
          && test.Length == source.Length 
          && !test.Where((x,i) => source[i] != x).Skip(2).Any();
}

根据 cmets 的要求,稍微解释一下这是如何工作的

在 C# 中,字符串可以被视为字符数组,这意味着可以在其上使用 Linq 方法。

test.Where((x,i) => source[i] != x)

这对测试中的每个字符使用 Where 的重载,x 被分配给字符,i 被分配给索引。如果 source 中位置 i 的条件字符不等于 x 则输出到结果中。

Skip(2)

这会跳过前 2 个结果。

Any()

如果还有任何结果,则返回 true,否则返回 false。因为 linq 在 this 为 false 的那一刻推迟执行,所以函数退出而不是评估字符串的其余部分。

然后通过前缀“!”来否定整个测试表示我们想知道哪里没有更多结果。

现在为了匹配为子字符串,您需要表现得类似于正则表达式回溯...

public static IEnumerable<int> GetMatches(string source, string test)
{
   return from i in Enumerable.Range(0,test.Length - source.Length)
      where IsValid(source, !test.Skip(i).Take(source.Length))
          select i;
}

public static bool IsValid(string source, IEnumerable<char> test) 
{
   return test.Where((x,i) => source[i] != x).Skip(2).Any();
}

更新解释

Enumerable.Range(0,test.Length - source.Length)

这会创建一个从 0 到 test.Length - source.Length 的数字序列,不需要从 test 中的每个字符开始检查,因为一旦长度较短,答案就无效。

从 i 在 ....

基本上遍历集合,每次都将 i 分配为当前值

其中 IsValid(source, !test.Skip(i).Take(source.Length))

过滤结果以仅包括在测试中从索引 i 开始(因此跳过)并继续进行 source.Length 字符(因此采取)的匹配项。

选择我

返回我

这将返回一个可枚举的测试中存在匹配的索引,您可以使用提取它们

GetMatches(source,test).Select(i => 
                      new string(test.Skip(i).Take(source.Length).ToArray()));

【讨论】:

  • 感谢您的回答 - 不幸的是,我对 LINQ 并没有那么了解,尽管我已经看到了它的许多很好的用途。你能详细说明一下这意味着什么吗?它类似于我刚刚在白板上模拟的一个更简洁的想法
【解决方案2】:

我认为这不能用正则表达式来完成(如果可以的话,我不熟悉语法)。但是,您可以对Levenshtein distance 使用动态规划算法。

编辑:如果您不需要处理已切换位置的字母,更简单的方法是只比较两个字符串中的每对字符,然后计算差异的数量。

【讨论】:

    【解决方案3】:

    我想不出你会如何使用正则表达式,但它的代码应该很简单。

    我可能只是将字符串拆分并逐个字符地比较它们。如果您有差异,请计算并移至下一个字符。如果超过 2 个差异,则继续下一个完整字符串。

    【讨论】:

    • 提出了一个很好的观点,我没有在我的帖子中提到这一点:如果字符串很长并且时间很重要,你可以在循环中编写代码,在差异之后退出检查达到 2。
    【解决方案4】:

    我认为没有一个好的正则表达式可以处理这种情况。 (或者至少,没有一个不会占用足够多的三行文本并导致您脚上多颗子弹。)但是,这并不意味着您无法解决这个问题。

    取决于你的字符串有多大(我假设它们每个不会有数百万个字符)我看不出有什么阻止你使用单个循环来按顺序比较个人字符,同时保持一个计数区别:

    int differences = 0;    // Count of discrepancies you've detected
    int tolerance = 7;    // Limit of discrepancies you'll allow
    
    CheckStrings(int differences, int tolerance) {
        for (i = 0; i < StringA.Length; i++)
        {
            if (StringA[i] != StringB[i]) {
                differences++;
                if (differences > tolerance) {
                    return false;
                }
            }
        }
        return true;
    }
    

    大多数时候,不要担心字符串太长而无法进入循环。在幕后,任何评估字符串每个字符的代码都会以某种形式循环。在您字面上处理数百万个字符之前,循环应该可以解决问题。

    【讨论】:

    • 是的,基本上就是这个。我快了 4 秒,但你带来了代码。我会删除我的答案。
    【解决方案5】:

    我将绕过“正则表达式”部分并专注于:

    有没有比对每个位置进行通配符嵌套循环更好的方法?

    听起来有一种程序化方式可能会对您有所帮助。 See this post 关于迭代两个 IEnumerables。通过同时迭代两个字符串,您可以在 O(n) 时间内完成任务。更好的是,如果你知道你的容忍度(最多 2 个错误),你有时可以比 O(n) 更快地完成。

    这是我写的一个简单的例子。它可能需要针对您自己的情况进行调整,但这可能是一个很好的起点。

    static void imperfectMatch(String original, String testCase, int tolerance)
    {
        int mistakes = 0;
    
        if (original.Length == testCase.Length)
        {
            using (CharEnumerator enumerator1 = original.GetEnumerator())
            using (CharEnumerator enumerator2 = testCase.GetEnumerator())
            {
                while (enumerator1.MoveNext() && enumerator2.MoveNext())
                {
                    if (mistakes >= tolerance)
                        break;
                    if (enumerator1.Current != enumerator2.Current)
                        mistakes++;
                }
            }
        }
        else
            mistakes = -1;
    
        Console.WriteLine(String.Format("Original String: {0}", original));
        Console.WriteLine(String.Format("Test Case String: {0}", testCase));
        Console.WriteLine(String.Format("Number of errors: {0}", mistakes));
        Console.WriteLine();
    }
    

    【讨论】:

    • 谢谢,这实际上可以帮助解决部分问题。从那以后我已经详细说明了。
    • @Rodney 我能想到的一件事是制作一个布尔数组来表示允许哪些字符不同。由于该数组将等于原始字符串和 testCase 字符串的长度,因此您也可以同时对其进行迭代。事实上,您可以根据需要迭代尽可能多的等长数组/集合。只需确保增加循环结构中的每一项即可。
    【解决方案6】:

    AB() 的任意组合是否有效?

    bool isMatch = Regex.IsMatch(inputString, "^[AB()]+$")
    

    【讨论】:

      【解决方案7】:

      对于足够小的模式 (ABCD),您可以生成一个正则表达式:

      ..CD|.B.D|.BC.|A..D|A.C.|AB..
      

      您还可以编写自定义比较循环

      【讨论】:

        猜你喜欢
        • 2012-03-31
        • 2018-12-21
        • 1970-01-01
        • 1970-01-01
        • 2011-06-03
        • 2011-08-03
        • 2016-07-29
        相关资源
        最近更新 更多