问题描述
有人问我一个面试问题:写一个函数 match(s,t)
来判断一个字符串 s
是否是另一个字符串 t
的广义子字符串。更具体地说,当且仅当删除 match
中的某些字符可以使其等于 t
时,s
应该返回 True。例如,match("abc","abbbc")
为 True,因为我们可以删除 b
中的两个额外的 t
。
当然面试官期待某种递归解决方案,但我感觉很冒险并写了
def match(s,t):
return re.search('.*?'.join(s),t) is not None
这似乎满足了所有的测试用例,但后来我开始怀疑是否存在任何对抗性输入可以占用此操作(并可能使我们的服务容易受到 DoS 攻击)。有关扩展讨论,请参阅 this great article,但作为一个快速示例,请尝试
re.match("a?"*29+"a"*29,"a"*29)
并且需要几秒钟才能找到匹配项。原因是 re
实现了回溯正则表达式引擎:
考虑正则表达式 a?nan。它匹配字符串 an 当 a?选择不匹配任何字母,留下整个字符串由 an 匹配。回溯正则表达式实现实现零或一?先尝试一个,然后再尝试零。有 n 个这样的选择,总共有 2n 种可能性。只有最后一种可能性——为所有 ? 选择零——会导致匹配。因此回溯方法需要 O(2n) 时间,所以它不会超过 n=25。
回到面试问题,从技术上讲,'.*?'.join(s)
至少为我们提供了 len(s) - 1
个选择,这意味着可能存在一些类似于 "a?"*29+"a"*29
和 "a"*29
的对抗性输入以上,但经过反复试验,我没能找到它们。
问题:什么 s
和 t
可以让 match
慢得离谱?
解决方法
懒惰的量词通常对性能非常好,但 AFAIK 它们并不能阻止病态的强调行为。
当正则表达式的开头与文本的开头匹配时尤其如此,但匹配很早,并且会在文本结尾失败,需要大量回溯来“修复”错误的早期延迟匹配正则表达式的开头。
在您的情况下,这是一个需要指数级步骤的病理输入示例:
# This should take at least few minutes to compute
n = 12
match('ab'*(n+1),'abbbbb'*n+'a')
以下是基于 match
的值所需的步骤数和 Python n
时间:
n | steps | time
1 | 44 | 2.4 µs
2 | 374 | 6.4 µs
3 | 2621 | 22.1 µs
4 | 18353 | 131.3 µs
5 | 126211 | 925.8 µs
6 | - | 6.2 ms
7 | - | 42.2 ms
8 | - | 288.7 ms
9 | - | 1.97 s
您可以理解和分析 this great website 上产生的正则表达式匹配行为(在这种情况下更具体地说是回溯)。
,您的解决方案由于回溯而受到影响,这就是为什么接受的答案能够提供会大大减慢速度的输入。
这是一个正则表达式解决方案,它与我之前的答案非常相似,而且似乎还避免了困扰您当前解决方案的问题。
def match(s,t):
return re.search("".join(f'[^{re.escape(c)}]*{re.escape(c)}' for c in s),t) is not None
上一个回答
这并不能直接回答您的问题,但是
当然面试官期待某种递归解决方案......
我不这么认为。像这样的东西也可以工作
def match(s,t):
i = -1
return all ((i := t.find(c,i + 1)) != -1 for c in s)
我只是想提供一种不需要递归或正则表达式的替代解决方案,因此会导致您提到的漏洞。