KMP算法,前缀树和AC自动机
基础知识
单模式匹配
KMP算法(Knuth-Morris-Pratt算法)
LC28 Implement strStr()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Solution {
public:
int strStr(string haystack, string needle) {
if(haystack.empty() && needle.empty()) return 0;
if(haystack.empty() && !needle.empty()) return -1;
if(!haystack.empty() && needle.empty()) return 0;
//compute the next array
auto pi = vector<size_t>(needle.length());
pi[0] = 0;
size_t k = 0;
for(size_t q = 1; q < needle.length(); ++q){
while(needle[k] != needle[q] && k > 0) k = pi[k - 1];
if(needle[k] == needle[q]) ++k;
pi[q] = k;
}
k = 0;
for(size_t i = 0; i < haystack.length(); ++i){
while(haystack[i] != needle[k] && k > 0) k = pi[k - 1];
if(haystack[i] == needle[k]) ++k;
if(k == needle.length()) return i - k + 1;
}
return -1;
}
};
BM算法(Boyer-Moore算法)
多模式匹配
Tries(字典树)
原理
TODO
LC208 Implement Trie (Prefix Tree)
看了很多这个题的答案,没有人写析构函数。自己dynamically allocated的内存,用完就给扔那里了。这个类的对象析构之后,所有申请的内存全泄漏了。Leetcode也不检查一下语法(估计也没法检查?)。
下面给出几个没有内存泄漏的解法:
使用原生指针的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60class Trie {
public:
/** Initialize your data structure here. */
Trie()
:root{new TrieNode{}}
{}
~Trie(){
delete root;
}
/** Inserts a word into the trie. */
void insert(string word) {
TrieNode* p = root;
for(auto c : word){
auto i = static_cast<size_t>(c - 'a');
if(p->children[i] == nullptr) p->children[i] = new TrieNode{};
p = p->children[i];
}
p->isWord = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
TrieNode* p = root;
for(auto c : word){
p = p->children[c - 'a'];
if(!p) return false;
}
if(p->isWord) return true;
else return false;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
TrieNode* p = root;
for(auto c : prefix){
p = p->children[c - 'a'];
if(!p) return false;
}
return true;
}
private:
class TrieNode{
public:
bool isWord;
array<TrieNode*, 26> children;
TrieNode()
:isWord{false}
,children{nullptr}
{}
~TrieNode(){
for(auto ptr : children){
delete ptr;
}
}
};
TrieNode* root;
};
使用shared_ptr
的实现,非常的慢。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55class Trie {
class TrieNode;
using TrieNodePtr = shared_ptr<TrieNode>;
public:
/** Initialize your data structure here. */
Trie()
:root{}
{
root = make_shared<TrieNode>();
}
/** Inserts a word into the trie. */
void insert(string word) {
TrieNodePtr p = root;
for(auto c : word){
auto i = static_cast<size_t>(c - 'a');
if(p->children[i] == nullptr)
p->children[i] = make_unique<TrieNode>();
p = p->children[i];
}
p->isWord = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
TrieNodePtr p = root;
for(auto c : word){
p = p->children[c - 'a'];
if(!p) return false;
}
if(p->isWord) return true;
else return false;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
TrieNodePtr p = root;
for(auto c : prefix){
p = p->children[c - 'a'];
if(!p) return false;
}
return true;
}
private:
class TrieNode{
public:
TrieNode()
:isWord{false}
,children{nullptr}
{}
bool isWord;
array<TrieNodePtr, 26> children;
};
TrieNodePtr root;
};
另一个没有内存泄漏的实现:Accepted cpp solution without memory leak - tju_xu
AC算法(Aho-Corasick算法,字符串匹配自动机)
多模字符串匹配算法之AC自动机—原理与实现
AC自动机 - 司徒正美
思路:
- 什么是fail指针?
非常类似KMP算法里的next数组。next数组里存的是$next[q] = max{k : P_k 是 P_q 的 后缀}$。而fail指针所指的是:$P,Q \in {所有模式串}, max{k : Q_k 是 P_q 的后缀}$的$Q_k$. 即【从根节点开始到该节点的字符串的后缀】和【所有模式串的前缀】的最长公共子串。 - 如何构造一个AC自动机?
- 把所有的模式构造成一个Trie. 这个Trie的所有TrieNode要有isWord标志和fail指针(失配指针).
- 之后我们来为所有TrieNode来弄fail指针(失配指针).
- root的fail指针赋值为
nullptr
. - 我们首先把root的所有孩子节点的fail指针赋值为root.因为如果一个字符串第一个字符都无法匹配,那这样的字符串就只能匹配空字符$\varepsilon$,所以回到root.
- 之后将root的所有孩子放入队列,开始对Trie做BFS.
- 从队列中pop出来一个,来为它(root)的所有孩子(child[i])找fail指针
- 设置
toFail
初始为root->fail
. - 如果
toFail == nullptr
这就是回到了root
了,只能匹配空串了。设置root->childs[i]->fail = root
。 - 如果
toFail->childs[i] != nullptr
类似KMP算法,说明此时代表最长公共部分的root->fail
还可以增长。那么直接设置root->childs[i]->fail = toFail>childs[i]
即可 - 如果
toFail->childs[i] == nullptr
类似KMP算法,我们需要找比这个要小的最长公共部分,所以设置toFail = toFail->fail
后,再回到开头。
- 设置
- 把一个TrieNode的所有child的fail指着都设置好后,我们把该TrieNode弹出队列。
- 一直循环直到队列为空(BFS做完)。
- root的fail指针赋值为
- 如何在AC自动机里做查询?
- 初始化当前指针
currPtr = root
- 首先从文本中读入一个字符c
- 循环开始
- 如果c是
currPtr
的孩子,自动机沿着主线进入下一个TrieNode(currPtr = currPtr->child[c - 'a']
)- 如果
currPtr->isWord
那么证明我们找到了一个匹配。 - 如果
!currPtr->isWord
- 如果
currPtr->fail->isWord
我们也找到了一个匹配。当前!isword
不代表没有匹配,有可能其最长的后缀是一个匹配。
- 如果
- 没有匹配任何模式,读入下一个字符串
- 如果
- 如果c不是
currPtr
的孩子,自动机需要沿着fail指针(失配指针)进入下一个TrieNode(currPtr = currPtr->fail
)- 如果
currPtr == nullptr
,那就是第一个字符都无法匹配,没有匹配任何模式。 - 回到循环头部。
- 如果
- 初始化当前指针
LC1032 Stream of Characters1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114class Trie{
class TrieNode;
constexpr static size_t NUM_OF_LETTERS = 26;
public:
Trie()
:root{new TrieNode{}}
,currPtr{root}
{}
Trie(vector<string>& words)
:root{new TrieNode{}}
,currPtr{root}
{
for(auto& word : words) insert(word);
buildAC();
}
~Trie(){
delete root;
}
void insert(const string& word){
auto p = root;
for(auto c : word){
auto i = static_cast<size_t>(c - 'a');
if(!p->childs[i]) p->childs[i] = new TrieNode{};
p = p->childs[i];
}
p->isWord = true;
}
void buildAC(){
auto q = queue<TrieNode*>{};
for(auto ptr : root->childs){
if(ptr){
ptr->fail = root;
q.push(ptr);
}
}
while(!q.empty()){
auto curr = q.front();
for(size_t i = 0; i < NUM_OF_LETTERS; ++i){
if(curr->childs[i]){
auto toFail = curr->fail;
while(true){
if(!toFail){
curr->childs[i]->fail = root;
break;
}
if(toFail->childs[i]){
curr->childs[i]->fail = toFail->childs[i];
break;
}
else toFail = toFail->fail;
}
q.push(curr->childs[i]);
}
}
q.pop();
}
}
inline bool nextState(char c) noexcept{
auto i = static_cast<size_t>(c - 'a');
while(true){
if(currPtr->childs[i]){
currPtr = currPtr->childs[i];
if(currPtr->isWord || currPtr->fail->isWord) return true;
return false;
}else{
currPtr = currPtr->fail;
if(!currPtr) {currPtr = root; return false;}
}
}
}
private:
TrieNode* root;
TrieNode* currPtr;
class TrieNode{
public:
array<TrieNode*, NUM_OF_LETTERS> childs;
TrieNode* fail;
bool isWord;
TrieNode()
:childs{nullptr}
,fail{nullptr}
,isWord{false}
{}
~TrieNode(){
for(auto ptr : childs) delete ptr;
}
};
};
class StreamChecker {
public:
StreamChecker(vector<string>& words)
:trie{new Trie{words}}
{}
~StreamChecker(){
delete trie;
}
bool query(char letter) {
return trie->nextState(letter);
}
private:
Trie* trie;
};
最长回文子串(Manacher算法)
LC5, Longest Palindromic Substring
解法详见:
1 | class Solution { |
正则表达式
元字符 | 含义 |
---|---|
. |
除\n 以外的任意字符 |
\w |
word: 字母、数字、下划线、汉字 |
\s |
space:空格、全角空格、Tab、\n |
\d |
decimal:数字 |
\b |
单词的开头或结尾:前一个字符或后一个字符,不全是\w |
^ |
字符串的开始 |
$ |
字符串的结束 |
限定符 | 含义 |
---|---|
{n} |
出现$r = n$次 |
{n,} |
出现$r \in [n, +\infty]$次 |
{n,m} |
出现$r \in [n, m]$次 |
* | 出现$r \in [0, \infty]$次 |
? | 出现$r \in {0,1}$次 |
+ | 出现$r = [1, +\infty]$次 |
题目
单模式匹配
LC459 Repeated Substring Pattern1
2
3
4
5
6
7
8
9
10
11
12
13
14class Solution {
public:
bool repeatedSubstringPattern(string s) {
auto len = s.size();
auto pi = vector<size_t>(len + 1);
size_t k = 0;
for(size_t q = 1; q < len; ++q){
while(s[q] != s[k] && k > 0) k = pi[k];
if(s[q] == s[k]) ++k;
pi[q+1] = k;
}
return k && (len % (len - k)) == 0;
}
};
多模式匹配
Trie(字典树)
Map Sum
1 | class MapSum { |
Add and Search Word
LC211 Add and Search Word - Data structure design
思路:
- 把所有的word都放到一个Trie里
- 做前序遍历
- 检查string的迭代器是不是到头了,如果到头了,并且该TrieNode还是isWord,就搜到了。
- 如果遇到
'.'
我们就搜索所有子树。 - 如果遇到其他字符我们就只搜索该字符。
1 | class WordDictionary { |
LC720 Longest Word in Dictionary
思路:
- 把所有的word都放进Trie
- 按字典序做DFS前序遍历
- 如果该节点isWord并且这个时候word比max_word(result)长,就用这个word代替result即可。(这么做的原因是我们按【字典序】对Trie做的搜索,每个长度的word只要被搜出来,就一定是字典序第一个。)
- 如果该节点是isWord,继续搜索他的所有子树。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67class Trie{
constexpr static size_t NUM_OF_LETTERS = 26;
constexpr static size_t ASCII_OF_a = 97;
class TrieNode;
public:
Trie()
:root{new TrieNode{}}
{}
~Trie(){
delete root;
}
void insert(const string& word){
auto p = root;
for(auto c : word){
auto i = static_cast<size_t>(c - 'a');
if(!p->childs[i]) p->childs[i] = new TrieNode{};
p = p->childs[i];
}
p->isWord = true;
}
string dfs(){
auto word = string{};
auto result = string{};
dfsImpl(root, word, result);
return result;
}
private:
TrieNode* root;
void dfsImpl(TrieNode* root, string& word, string& result){
if(root->isWord && word.size() > result.size())
result = word;
if(root->isWord || root == this->root){
for(size_t i = 0; i < NUM_OF_LETTERS; ++i){
if(root->childs[i]){
cout<<static_cast<char>(ASCII_OF_a + i)<<endl;
word.push_back(static_cast<char>(ASCII_OF_a + i));
dfsImpl(root->childs[i], word, result);
word.pop_back();
}
}
}
}
class TrieNode{
public:
array<TrieNode*, NUM_OF_LETTERS> childs;
bool isWord;
TrieNode()
:childs{nullptr}
,isWord{false}
{}
~TrieNode(){
for(auto ptr : childs) delete ptr;
}
};
};
class Solution {
public:
string longestWord(vector<string>& words) {
auto trie = Trie{};
for(auto word : words) trie.insert(word);
return trie.dfs();
}
};
正则表达式匹配
由*和.构成的正则表达式匹配
Regular Expression Matching
思路:
不能想着去构造DFA的方法去做这样的题,时间是不可能够的。直接用动态规划的思想。
- 从文本里拿出来一个字符,和模式匹配
- 如果文本字符是$\varepsilon$(空),且模式也是$\varepsilon$,则匹配成功。
- 如果文本字符不是$\varepsilon$,但模式是$\varepsilon$,匹配失败。
- 如果模式字符的下一个字符是
*
- 如果文本字符和模式字符匹配 或 文本字符不是$\varepsilon$的同时,模式字符是
.
- 我们可以忽略这个
a*
,让文本停留在当前字符,模式向后移动两个。 - 我们可以不忽略这个
a*
,让匹配停留在该状态,即文本向后移动一个字符,模式不移动。
- 我们可以忽略这个
- 如果文本字符和模式字符不匹配,我们此时只能忽略
a*
,即让文本停留在当前字符,模式向后移动两个字符。
- 如果文本字符和模式字符匹配 或 文本字符不是$\varepsilon$的同时,模式字符是
- 如果模式字符的下一个字符不是
*
- 如果文本字符和模式字符匹配 或 文本字符不是$\varepsilon$的同时,模式字符是
.
,我们可以让文本及模式都向后移动一个字符。 - 否则匹配失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Solution {
public:
bool isMatch(string a, string b) {
auto t = a.c_str();
auto p = b.c_str();
return isMatchImpl(t,p);
}
bool isMatchImpl(const char* t, const char* p){
if(*t == '\0' && *p == '\0') return true;
if(*t != '\0' && *p == '\0') return false;
if(p + 1 && *(p + 1) == '*'){
if(*t == *p || (*t != '\0' && *p == '.'))
return isMatchImpl(t, p + 2) || isMatchImpl(t + 1, p);
else
return isMatchImpl(t, p + 2);
}
if(*t == *p || (*t != '\0' && *p == '.'))
return isMatchImpl(t + 1, p + 1);
return false;
}
};
- 如果文本字符和模式字符匹配 或 文本字符不是$\varepsilon$的同时,模式字符是
有效的数字表达式
其他
LC425 Word Squares premium