【问题标题】:Recursive solutions for glob pattern matchingglob 模式匹配的递归解决方案
【发布时间】:2015-08-29 16:12:27
【问题描述】:

我目前正在研究 UNIX 风格的 glob 模式匹配的实现。通常,fnmatch 库是此功能的一个很好的参考实现。

看了一些the implementations,以及阅读各种关于这个的博客/教程,似乎这个算法通常是递归实现的。

通常,任何需要“回溯”或需要返回到较早状态的算法都非常适合递归解决方案。这包括诸如树遍历或解析嵌套结构之类的事情。

但是我很难理解为什么特别是 glob 模式匹配经常递归地实现。我的想法是有时需要回溯,例如,如果我们有一个字符串aabaabxbaab 和一个模式a*baab,则* 之后的字符将匹配第一个“baab”子字符串,例如aa(baab)xbaab ,然后 fail 以匹配字符串的其余部分。所以算法需要回溯,以便字符匹配计数器重新开始,我们可以匹配第二次出现的baab,例如:aabaabx(baab)

好的,但是当我们可能需要多个嵌套级别的回溯时,通常会使用递归,我不知道在这种情况下有什么必要。当模式上的迭代器和输入字符串上的迭代器无法匹配时,似乎我们一次只需要回溯一个级别。此时,模式上的迭代器需要移回最后一个“保存点”,这可能是字符串的开头,或者是最后一个成功匹配的*。这不需要堆栈 - 只需一个保存点。

我能想到的唯一复杂情况是发生“重叠”匹配。例如,如果我们有输入字符串aabaabaab 和模式a*baab,我们将无法匹配,因为最后一个baab 中的“b”可能是第一个匹配项或第二个匹配项的一部分。但是,如果最后一个模式迭代器保存点和模式结束点之间的距离大于输入迭代器位置和输入字符串结束点之间的距离,这似乎可以通过简单地回溯输入迭代器来解决。

所以,据我所见,迭代地实现这个 glob 匹配算法应该不是什么大问题(假设一个非常简单的 glob 匹配器,它只使用 * 字符来表示“匹配零个或多个字符”。此外,匹配策略将是贪婪的。)


所以,我认为我对此肯定是错误的,因为其他人都是递归地这样做的——所以我一定错过了一些东西。只是我看不到我在这里缺少什么。所以我在 C++ 中实现了一个简单的 glob 匹配器(只支持 * 运算符),看看我是否能弄清楚我错过了什么。这是一个非常直接、简单的迭代解决方案,它只使用一个内部循环来进行通配符匹配。它还记录了* 字符在对向量中匹配的索引:

bool match_pattern(const std::string& pattern, const std::string& input, 
    std::vector<std::pair<std::size_t, std::size_t>>& matches)
{
    const char wildcard = '*';

    auto pat = std::begin(pattern);
    auto pat_end = std::end(pattern);

    auto it = std::begin(input);
    auto end = std::end(input);

    while (it != end && pat != pat_end)
    {
        const char c = *pat;
        if (*it == c)
        {
            ++it;
            ++pat;
        }
        else if (c == wildcard)
        {
            matches.push_back(std::make_pair(std::distance(std::begin(input), it), 0));
            ++pat;
            if (pat == pat_end) 
            {  
                matches.back().second = input.size();
                return true; 
            }

            auto save = pat;
            std::size_t matched_chars = 0;

            while (it != end && pat != pat_end)
            {
                if (*it == *pat)
                {
                    ++it;
                    ++pat;
                    ++matched_chars;

                    if (pat == pat_end && it != end) 
                    {
                        pat = save;
                        matched_chars = 0;

                        // Check for an overlap and back up the input iterator if necessary
                        //
                        std::size_t d1 = std::distance(it, end);
                        std::size_t d2 = std::distance(pat, pat_end);
                        if (d2 > d1) it -= (d2 - d1);
                    }
                }
                else if (*pat == wildcard)
                {
                    break;
                }
                else
                {
                    if (pat == save) ++it;
                    pat = save;
                    matched_chars = 0;
                }
            }

            matches.back().second = std::distance(std::begin(input), it) - matched_chars;
        }
        else break;
    }

    return it == end && pat == pat_end;
}

然后我写了一系列测试,看看我是否能找到一些需要多级回溯(因此需要递归或堆栈)的模式或输入字符串,但我似乎想不出任何东西。

这是我的测试函数:

void test(const std::string& input, const std::string& pattern)
{
    std::vector<std::pair<std::size_t, std::size_t>> matches;
    bool result = match_pattern(pattern, input, matches);
    auto match_iter = matches.begin();

    std::cout << "INPUT:   " << input << std::endl;
    std::cout << "PATTERN: " << pattern << std::endl;
    std::cout << "INDICES: ";
    for (auto& p : matches)
    {
        std::cout << "(" << p.first << "," << p.second << ") ";
    }
    std::cout << std::endl;

    if (result)
    {
        std::cout << "MATCH:   ";

        for (std::size_t idx = 0; idx < input.size(); ++idx)
        {
            if (match_iter != matches.end())
            {
                if (idx == match_iter->first) std::cout << '(';
                else if (idx == match_iter->second)
                {
                    std::cout << ')';
                    ++match_iter;
                }
            }

            std::cout << input[idx];
        }

        if (!matches.empty() && matches.back().second == input.size()) std::cout << ")";

        std::cout << std::endl;
    }
    else
    {
        std::cout << "NO MATCH!" << std::endl;
    }

    std::cout << std::endl;
}

还有我的实际测试:

test("aabaabaab", "a*b*ab");
test("aabaabaab", "a*");
test("aabaabaab", "aa*");
test("aabaabaab", "aaba*");
test("/foo/bar/baz/xlig/fig/blig", "/foo/*/blig");
test("/foo/bar/baz/blig/fig/blig", "/foo/*/blig");
test("abcdd", "*d");
test("abcdd", "*d*");
test("aabaabqqbaab", "a*baab");
test("aabaabaab", "a*baab");

所以这个输出:

INPUT:   aabaabaab
PATTERN: a*b*ab
INDICES: (1,2) (3,7) 
MATCH:   a(a)b(aaba)ab

INPUT:   aabaabaab
PATTERN: a*
INDICES: (1,9) 
MATCH:   a(abaabaab)

INPUT:   aabaabaab
PATTERN: aa*
INDICES: (2,9) 
MATCH:   aa(baabaab)

INPUT:   aabaabaab
PATTERN: aaba*
INDICES: (4,9) 
MATCH:   aaba(abaab)

INPUT:   /foo/bar/baz/xlig/fig/blig
PATTERN: /foo/*/blig
INDICES: (5,21) 
MATCH:   /foo/(bar/baz/xlig/fig)/blig

INPUT:   /foo/bar/baz/blig/fig/blig
PATTERN: /foo/*/blig
INDICES: (5,21) 
MATCH:   /foo/(bar/baz/blig/fig)/blig

INPUT:   abcdd
PATTERN: *d
INDICES: (0,4) 
MATCH:   (abcd)d

INPUT:   abcdd
PATTERN: *d*
INDICES: (0,3) (4,5) 
MATCH:   (abc)d(d)

INPUT:   aabaabqqbaab
PATTERN: a*baab
INDICES: (1,8) 
MATCH:   a(abaabqq)baab

INPUT:   aabaabaab
PATTERN: a*baab
INDICES: (1,5) 
MATCH:   a(abaa)baab

"MATCH: " 之后出现在输出中的括号显示每个 * 字符匹配/使用的子字符串。所以,这似乎工作正常,我似乎不明白为什么有必要在这里回溯多个级别 - 至少如果我们将模式限制为仅允许 * 字符,并且我们假设贪婪匹配。

所以我认为我对此肯定是错误的,并且可能忽略了一些简单的事情 - 有人可以帮我看看为什么这个算法可能需要多级回溯(因此需要递归或堆栈)?

【问题讨论】:

  • 这似乎是一种优雅的方法,出于分析原因,如果您可以共享一个不记录索引(可能不备份迭代器)并因此进行优化的版本,这将很有帮助。针对 AI 工作的递归版本的性能测试将我带到这里,在此先感谢您。
  • 您的算法似乎声称daaadabadmandada*da*da* 模式不匹配。有时我们选择递归仅仅是因为它更容易使算法正确。 youtube.com/watch?v=lNYcviXK4rg
  • 我得到NO MATCH! for test("mississippi", "m*issip*"),但它应该是匹配的。

标签: c++ recursion pattern-matching glob


【解决方案1】:

我没有检查你的实现的所有细节,但你可以在没有递归回溯的情况下进行匹配。

您实际上可以通过构建一个简单的有限状态机来进行全局匹配而无需回溯。您可以将 glob 转换为正则表达式并以正常方式构建 DFA,或者您可以使用非常类似于 Aho-Corasick 机器的东西;如果你稍微调整一下你的算法,你会得到同样的结果。 (关键是您实际上不需要备份输入扫描;您只需要找出正确的扫描状态,即可预先计算。)

经典的 fnmatch 实现并未针对速度进行优化;它们基于模式和目标字符串很短的假设。这种假设通常是合理的,但如果您允许不受信任的模式,您将面临 DoS 攻击。并且基于该假设,与您提出的算法类似,不需要预先计算,在绝大多数用例中可能比任何需要预先计算状态转换表同时避免病态模式的指数爆炸的算法更快。

【讨论】:

  • 任何指向优化的速度实现的指针是迭代的并且不使用预计算?
  • @rama-jkatoti:你可以看看 Gustavo Navarro 的 nrgrep、解释它的论文和他的书。 (前两个可通过dcc.uchile.cl/~gnavarro/software 在线获取)。 (IIRC,nrgrep 使用预计算,但本书对模式匹配算法进行了大量调查。)不过,这只是我的想法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-07-22
  • 2015-10-25
  • 1970-01-01
  • 2021-11-03
  • 2018-12-30
  • 2018-11-30
  • 1970-01-01
相关资源
最近更新 更多