惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
Apple Machine Learning Research
Apple Machine Learning Research
云风的 BLOG
云风的 BLOG
量子位
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
人人都是产品经理
人人都是产品经理
V
Visual Studio Blog
Jina AI
Jina AI
L
LangChain Blog
M
MIT News - Artificial intelligence
MongoDB | Blog
MongoDB | Blog
Last Week in AI
Last Week in AI
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学

博客园 - RonChen

康托展开 多源 BFS 抽屉原理 区间合并 距离和的最小值 归并排序与逆序对 快速排序与快速选择 同余分析 差分约束 Treap 点分治 莫队算法 分块 扫描线 错排问题 Sprague-Grundy (SG) 函数及其应用 容斥原理 卢卡斯定理 线性基 高斯消元 勒让德公式 次短路 分层图最短路 01 图最短路 洪水填充 双向搜索 迭代加深搜索 剪枝 最小表示法 表达式计算
Sunday 算法
RonChen · 2026-09-12 · via 博客园 - RonChen

Sunday 算法由 Daniel M. Sunday 于 1990 年提出,是一种极其高效的单模式字符串匹配算法,尤其在处理长文本与较短模式串时表现出色。

Sunday 算法的匹配过程从左向右进行,但它在遇到不匹配时,只关注当前滑动窗口之外紧跟的下一个字符

设文本串为 \(T\),长度为 \(n\),模式串为 \(P\),长度为 \(m\),当前匹配窗口为 \(T_{i \dots i+m-1}\)。如果在窗口内发现字符不匹配,算法会直接查看紧跟在窗口后的字符,即 \(T_{i+m}\)

  • 情况 A:如果 \(T_{i+m}\) 不在模式串 \(P\) 中存在,说明包含该字符的任何窗口都无法匹配。窗口可以直接向后滑动,跳过这个字符,滑动距离为 \(m+1\)
  • 情况 B:如果 \(T_{i+m}\) 模式串 \(P\) 中存在,为了不漏掉可能的匹配,需要将模式串中最靠右的该字符与 \(T_{i+m}\) 对齐。

为了快速决定滑动距离,需要预处理出一个偏移表,记录每个字符对应的滑动步长。

  • 默认值:对于所有不在模式串中的字符,其偏移量为 \(m+1\)
  • 模式串内的字符:遍历模式串,对于字符 \(P_j \ (0 \le j \lt m)\),它距离模式串末尾的距离是 \(m-1-j\)。为了让该字符与窗口外的下一个字符对齐,模式串需要滑动的距离为 \(m-j\)
  • 注意:如果一个字符在模式串中出现多次,以最靠右的出现位置为准。
vector<int> sunday_search(const string &text, const string &pattern) {
    int n = text.length(), m = pattern.length();
    vector<int> results;
    // 边界情况处理
    if (m == 0 || n < m) return results;
    // 1. 初始化偏移表,默认偏移量为 m + 1
    // 假设字符集为 128 个 ASCII 字符
    vector<int> shift(128, m + 1);
    // 2. 计算模式串中每个字符的偏移量
    for (int i = 0; i < m; i++) {
        shift[pattern[i]] = m - i;
    }
    int i = 0;
    // 3. 开始匹配
    while (i <= n - m) {
        // 检查当前窗口是否完全匹配
        bool match = true;
        for (int j = 0; j < m; j++) {
            if (text[i + j] != pattern[j]) {
                match = false;
                break;
            }
        }
        if (match) results.push_back(i);
        // 如果已经到了文本末尾,无需再获取下一个字符
        if (i + m >= n) break;
        // 核心:根据窗口紧跟的下一个字符决定跳跃距离
        char next_char = text[i + m];
        i += shift[next_char];
    }
    return results;
}

如果题目中的字符串是由随机字符组成的(例如普通的英文文章、日志数据),Sunday 算法的速度往往很快。但如果题目刻意构造极端数据,Sunday 算法会退化为 \(O(n \times m)\) 的暴力搜索。

2022 CSP-S1 阅读程序 T1
#include <iostream>
#include <string>
#include <vector>

using namespace std;

int f(const string &s, const string &t)
{
    int n = s.length(), m = t.length();

    vector<int> shift(128, m + 1);

    int i, j;

    for (j = 0; j < m; j++)
        shift[t[j]] = m - j;
    
    for (i = 0; i <= n - m; i += shift[s[i + m]]) {
        j = 0; 
        while (j < m && s[i + j] == t[j]) j++;
        if (j == m) return i;
    }

    return -1;
}

int main()
{
    string a, b;
    cin >> a >> b;
    cout << f(a, b) << endl;
    return 0;
}

假设输入字符串由 ASCII 可见字符组成:

  1. 当输入为 abcde fg 时,输出为 -1。(正确/错误)
  2. 当输入为 abbababbbab abab 时,输出为 4。(正确/错误)
  3. 当输入为 GoodLuckCsp2022 22 时,第 20 行的 j++ 语句执行次数为 \(2\)。(正确/错误)
  4. 该算法最坏情况下的时间复杂度为?
  5. f(a, b) 与下列哪个语句的功能最类似?
  • A. a.find(b)
  • B. a.rfind(b)
  • C. a.substr(b)
  • D. a.compare(b)
  1. 当输入为 baaabaaabaaabaaaa aaaa,第 20 行的 j++ 语句执行次数为?
答案

判断:正确

计算过程
输入 a = "abcde", b = "fg"\(n=5, m=2\))。shift 数组初始全为 \(3\)
根据 b 计算偏移表:shift['f'] = 2, shift['g'] = 1

  • \(i=0\):当前窗口为 "ab"'a' != 'f',在 \(j=0\) 处失配。查看下一字符 a[0+2] = a[2] = 'c'shift['c'] = 3,更新 \(i = 0 + 3 = 3\)
  • \(i=3\):当前窗口为 "de"'d' != 'f',在 \(j=0\) 处失配。查看下一字符 a[3+2] = a[5] = '\0'(C++中字符串末尾后的字符访问),shift['\0'] = 3,更新 \(i = 3 + 3 = 6\)
    此时 \(i = 6 > n - m = 3\),循环条件不满足,跳出循环并返回 -1

判断:错误

计算过程
输入 a = "abbababbbab", b = "abab"\(n=11, m=4\))。默认 shift\(5\)
根据 b 计算偏移表:shift['a'] = 2, shift['b'] = 1

  • \(i=0\):窗口 "abba",在 \(j=2\) 处失配(a[2]='b' != t[2]='a')。下个字符为 a[0+4] = a[4] = 'b',更新 \(i = 0 + shift['b'] = 1\)
  • \(i=1\):窗口 "bbab",在 \(j=0\) 处失配('b' != 'a')。下个字符为 a[1+4] = a[5] = 'a',更新 \(i = 1 + shift['a'] = 1 + 2 = 3\)
  • \(i=3\):窗口 "abab",逐个比对全部成功(\(j\) 最终递增到 \(4\))。循环结束,直接返回当前的起始位置 \(3\)

判断:正确

计算过程
输入 a = "GoodLuckCsp2022", b = "22"\(n=15, m=2\))。默认 shift\(3\)shift['2'] = 1

  • \(i=0, 3, 6\):窗口分别为 "Go", "dL", "ck"。每次都在首字符发生失配(j++ 执行 \(0\) 次),且下一位字符分别为 'o', 'u', 'C',其 shift 值均为 \(3\),因此 \(i\) 依次递增为 \(3, 6, 9\)
  • \(i=9\):窗口 "sp",在 \(j=0\) 失配(j++ 执行 \(0\) 次)。下一位字符为 a[11]='2'shift['2']=1,更新 \(i = 9+1=10\)
  • \(i=10\):窗口 "p2",首字符 'p' != '2' 再次在 \(j=0\) 失配(j++ 执行 \(0\) 次)。下一位字符为 a[12]='0'shift['0']=3,更新 \(i = 10+3=13\)
  • \(i=13\):窗口 "22",与目标串 "22" 逐位比对。首字符相等(执行第 \(1\)j++),第二个字符也相等(执行第 \(2\)j++)。匹配成功,返回 \(13\)
    全局累计下来,j++ 语句仅在最后一次完全匹配时执行了 \(2\) 次。

\(O(n \times m)\)

要让 Sunday 算法退化到最坏情况,必须同时满足两个极其苛刻的条件:

  1. 单趟比较代价极大:每次窗口匹配时,都要比对到模式串的末尾或接近末尾才发生失配。
  2. 滑动步长极小:每次失配后,根据主串参与匹配窗口的下一个字符计算出的偏移量(shift)尽可能小(最好每次只滑动 1 步)。

极端数据构造示例解析:
假设构造这样的文本串(主串)和模式串:

  • 主串(\(a\)"aaaaa...aaaaa" (长度为 \(n\),全为 'a'
  • 模式串(\(b\)"aaa...aba" (长度为 \(m\),前 \(m-2\) 个全为 'a',倒数第 2 个为 'b',最后 1 个为 'a'

执行过程推演:

  1. 构建 shift 表
    对于模式串 "aaa...aba",字符 'a' 最后一次出现的位置是下标 \(m-1\)(即最后一位)。
    根据 Sunday 算法的偏移规则:shift['a'] = m - (m - 1) = 1
  2. 窗口匹配与失配
    在任何一个起始位置 \(i\),由于主串全为 'a',前 \(m-2\) 次比对('a' == 'a')都会成功。
    直到比对到模式串的倒数第 2 位(即 \(j = m-2\) 时),主串的 'a' 与模式串的 'b' 才会发生失配。
    此时,算法已经执行了 \(m-1\) 次比对(包含了 \(m-2\) 次成功和 \(1\) 次失败)。
  3. 计算偏移并滑动
    失配后,算法查看主串该窗口后的下一个字符(即 \(a[i+m]\))。
    因为主串全为 'a',该字符必然是 'a'
    查表得到 shift['a'] = 1,因此窗口只能向右滑动 1 步,即 \(i \gets i + 1\)
  4. 循环往复
    滑动 1 步后,新的窗口再次重复上述过程:执行 \(m-1\) 次比较后失配,然后再次只能滑动 1 步。

复杂度计算:
由于每次只前进 1 步,外层循环大约会执行 \(n - m\) 次;而内层每次匹配都会进行 \(m-1\) 次字符比较。
总比较次数约为 \((n - m) \times (m - 1)\)
\(n \gg m\) 时,时间复杂度退化为标准的 \(O(n \times m)\)


A. a.find(b)
f(a, b) 的核心逻辑是寻找字符串 ba 中首次出现的起始下标位置,若未找到则返回 -1。这与 C++ 标准库中 std::string::find 的行为高度一致(找不到时返回 string::npos,强转为 int 时值即为 -1)。


10
输入 a = "baaabaaabaaabaaaa", b = "aaaa"\(n=17, m=4\))。shift['a']=1shift['b']=5

  • \(i=0\):窗口 "baaa",首字符失配(j++ \(0\) 次)。下个字符为 a[4]='b'\(i \gets 0+5=5\)
  • \(i=5\):窗口 "aaab",前 \(3\) 位匹配(j++ \(3\) 次)。下个字符为 a[9]='a'\(i \gets 5+1=6\)
  • \(i=6\):窗口 "aaba",前 \(2\) 位匹配(j++ \(2\) 次)。下个字符为 a[10]='a'\(i \gets 6+1=7\)
  • \(i=7\):窗口 "abaa",首位匹配(j++ \(1\) 次)。下个字符为 a[11]='a'\(i \gets 7+1=8\)
  • \(i=8\):窗口 "baaa",首字符失配(j++ \(0\) 次)。下个字符为 a[12]='b'\(i \gets 8+5=13\)
  • \(i=13\):窗口 "aaaa",全部匹配(j++ \(4\) 次)。返回结果。
    累计执行次数为:\(0 + 3 + 2 + 1 + 0 + 4 = 10\)