字符串匹配算法之Sunday算法
什么是Sunday算法
Sunday算法是字符串匹配算法的一种, 比KMP的代码简单. 平均情况下执行效率一样.
-
讨论前提: 假设文本串为
str, 长度为n, 用i指针扫描; 匹配串为pat, 长度为n, 用j指针来扫描; 字符串下标从0开始; -
算法流程
-
str: a c b c d ? f d k f pat: a c b c e // ? == c pat: a c b c e // ? 不在pat中 pat: a b c d e pat: a b c d e -
pat向右边移动多少位取决于
?这个位置上的字符是否有在pat中出现过- 出现过: 我们将
?和pat中对应的字符(最靠右边的, 比如上面c在1和3的位置上都出现过, 我们选3这个位置的c) 对齐, 值得关注的是我们这里对齐的步数是怎么算的.- 比如上面
pat中第3个位置上的c, 首先它要走到字符串末尾-->再往前跨一步-->它就到了?这个位置了. 我们也就对齐成功了. 用公式来描述就是m - last_occ[c]. 其中last_occ[c]表示c这个字符在pat中最后出现的位置 - ❓为什么选择靠右的位置, 道理很简单, 如果选了靠左的位置, 那么夹在中间的可能匹配就被我们跳过了
- 比如上面
- 没出现过: 这个很好理解, 如果
?这个位置的字符在pat中从来没有出现过, 那么可以直接跨过它. 可以想象一下如果不跨过的话, 每一次肯定有?和pat中某个字符的无意义比较, 可以看看上面我列出来的浪费的情况. 这是很大的浪费. 那么直接跨过的步数就很简单了, 肯定是m + 1步. 为了和出现过的情况的表达式统一. 可以表示为m - (-1)
- 出现过: 我们将
-
指针的更新
- 每次
pat移动后,j = 0从头开始,i则指向移动后的pat的起始位置在str中的索引. - 在上面的讨论中我们已经得出了
pat移动的规律, 那么我们的指针i更新也是一个道理. 就是i + m - last_occ[i + m]
- 每次
-
-
举个🌰
-
表格显示不完整的可以缩小页面显示比例
-
index 0 1 2 3 4 5 6 7 8 9 10 str a b c d c a o b x c d pat a x c d move1️⃣ a x c d move2️⃣ a x c d -
1️⃣在
index[1]的位置b和x不匹配, 此时我们检查index[4]位置上的c有没有在pat里面出现过, 显然pat[1] = c, 根据上面提到的规则, 此时pat向右边移动4 - 2 = 2格.i的更新为i = 0 + 4 - 2 = 2 -
2️⃣在
index[2]的a位置和c不匹配, 此时我们检查index[6]位置上的o有没有在pat里面出现过, 显然, 我们在pat中找不到o, 根据上面提到的规则, 此时pat向右边移动4-(-1) = 5格.i的更新为i = 2 + 4 - (-1) = 7
-
-
复杂度分析
- 最好的时间复杂度就是\(T-O(m + n/m)≈O(n)\), \(m\)是每次0位置上的比较. 即每一次比较, 如果每次将
pat[0]和str[0]比较都是不匹配, 而且?的字符没有出现在pat中, 那么一次就能 - 最差时间复杂度就是\(T-O(nm)\), 可以想象一下每次都是
pat[m-1]才匹配失败, 而且每次只能动一格的情况 - 平均时间复杂度就是\(T-O(n+m)\)
- 最好的时间复杂度就是\(T-O(m + n/m)≈O(n)\), \(m\)是每次0位置上的比较. 即每一次比较, 如果每次将
-
特点
-
并不规定
pat中字符匹配的顺序, 像Boyer-Moore算法就规定了是从右向左比, 所以Sunday算法会更加灵活. 你完全可以从一个最可能不匹配的位置开始比较. 以下面为例, 如果从index[1]开始比较, 我们第一次就发现不匹配了. 此时就可以马上看t, 发现不在pat中, 那么我们可以直接跨过去 -
index 0 1 2 3 4 5 6 7 8 str a b c d t w x y b pat a x c d move a x c d
-
写出代码
- 以Leetcode 28. Implement strStr()为例, 可以在上面提交代码测试. 此处
haystack即主串,needle为子串. 经过我的实际测试, Sunday算法的执行效率和KMP持平, 代码还比KMP简单👍 - 要点
last_occ的初始化, 默认为-1, 对应2️⃣. 其他情况更新为在str中的索引就好了.
class Solution {
public:
vector<int> get_last_occ(string pat)
{
vector<int> occ;
for(int i=0;i<256;i++) //ASCII码表的上限为256
occ.push_back(-1);
for(int i=0;i<pat.size();i++) //从左向右遍历, 所以相同字符总是能更新为最右边的字符
occ[pat[i]] = i;
return occ;
}
int strStr(string haystack, string needle) {
int n = haystack.size();
int m = needle.size();
auto last_occ = get_last_occ(needle);
int i = 0;
while(i <= n-m)
{
int cur;
for(cur=i;cur<i+m;cur++) // 主串从i开始, 模式串都从0开始比较
if(haystack[cur] != needle[cur - i])
break;
if(cur == i+m) // cur == i + m说明已经找到了符合的子串
return i;
else{
i += m; // 定位到?的位置
if(i < n)
i -= last_occ[haystack[i]]; // 查看?字符的最后出现位置
else
break;
}
}
return -1;
}
};
本文来自博客园,作者:MartinLwx,转载请注明原文链接:https://www.cnblogs.com/MartinLwx/p/13218934.html
🚀🚀🚀博客园已不再更新文章,最新文章请查看👉Github pages

浙公网安备 33010602011771号