字符串匹配算法 学习笔记 (介绍及模板)

在字符串匹配问题中,目标是找到一个模式串(Pattern)在一个文本串(Text)中的出现位置。这类问题在实际中有着广泛的应用。本文将介绍几种经典的字符串匹配算法,并包括一些并行加速的内容。主要涉及以下几种算法:

  • 朴素算法
  • KMP算法
  • BM算法
  • Sunday算法
  • CUDA加速的朴素算法

1. 朴素字符串匹配算法(Brute Force)

原理:

最简单的字符串匹配方法是朴素算法,遍历文本串的所有可能位置,将每个位置的子串与模式串逐个字符比较。

时间复杂度:

  • 最坏情况:O(n*m)
    (其中n是文本长度,m是模式串长度)

实现:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

vector<int> bruteForce(const string &text, const string &pattern) {
    vector<int> result;
    int n = text.size();
    int m = pattern.size();
    for (int i = 0; i <= n - m; i++) {
        int j;
        for (j = 0; j < m; j++) {
            if (text[i + j] != pattern[j])
                break;
        }
        if (j == m)
            result.push_back(i);
    }
    return result;
}

int main() {
    string text = "ABABABABCABABABCABABABC";
    string pattern = "ABABABC";
    vector<int> result = bruteForce(text, pattern);
    for(auto & i:result)
        cout << "Pattern found at index " << i << endl;
    return 0;
}

2. KMP算法(Knuth-Morris-Pratt)

原理:

KMP算法通过构建next数组,避免在匹配失败后对已经比较过的字符重新比较,从而提升效率。

在求解next数组的过程中,遍历模式串,并使用双指针法维护两个变量:当前字符位置i和已经匹配的最长前缀的长度len。具体步骤如下:

  1. 初始化一个长度为模式串长度的数组next,并将next[0]设为0,因为第一个字符没有前缀和后缀可比。
  2. 使用变量i从第二个字符开始遍历模式串。如果当前字符pattern[i]与pattern[len]相等,表示前缀和后缀相同,此时len++,并将next[i]设置为len,然后继续处理下一个字符(i++)。
  3. 如果当前字符不匹配,并且len不为0,则将next回退为next[len-1],相当于找到前一位置的最长前缀后缀长度,继续比较。
  4. 如果len为0,说明当前字符无法匹配任何前缀,则将next[i]置为0,并移动到下一个字符。

通过这种方式,next数组能高效记录模式串中每个位置的最长前缀后缀匹配信息,确保KMP算法在主串匹配时能快速跳过无效的匹配尝试。

时间复杂度:

  • 最坏情况:O(n + m)

实现:

#include <iostream>
#include <vector>

using namespace std;

vector<int> computeLPSArray(const string &pattern) {
    int m = pattern.size();
    vector<int> next(m, 0);
    int len = 0;
    int i = 1;

    while (i < m) {
        if (pattern[i] == pattern[len]) {
            len++;
            next[i] = len;
            i++;
        } else {
            if (len != 0) {
                next = next[len - 1];
            } else {
                next[i] = 0;
                i++;
            }
        }
    }
    return next;
}

void KMPSearch(const string &text, const string &pattern) {
    int n = text.size();
    int m = pattern.size();
    vector<int> lps = computeLPSArray(pattern);
    int i = 0, j = 0;

    while (i < n) {
        if (pattern[j] == text[i]) {
            i++;
            j++;
        }
        if (j == m) {
            cout << "Pattern found at index " << i - j << endl;
            j = lps[j - 1];
        } else if (i < n && pattern[j] != text[i]) {
            if (j != 0) {
                j = lps[j - 1];
            } else {
                i++;
            }
        }
    }
}

int main() {
    string text = "ABABABABCABABABCABABABC";
    string pattern = "ABABABC";
    KMPSearch(text, pattern);
    return 0;
}

3. BM算法(Boyer-Moore)

原理:

BM算法通过预处理模式串,使用坏字符规则好后缀规则来跳过不必要的字符比较,使得匹配效率极高。

坏字符规则的思想是,当模式中的字符与文本中的字符不匹配时,我们可以直接跳过一些无关的字符,而不是一个字符一个字符地滑动模式。

  1. 当模式中的字符与文本中的字符不匹配时,记录这个“坏字符”。
  2. 在模式中查找该“坏字符”最近的出现位置。
  3. 根据“坏字符”在模式中的位置,决定模式向右滑动多少步,以避免不必要的比较。

好后缀规则通过匹配后缀的部分,来进一步加快模式滑动速度。

  1. 当发生不匹配时,先看看模式中是否有与已匹配部分(即“好后缀”)相匹配的部分。
  2. 如果有,模式可以直接跳到这个匹配部分的位置。
  3. 如果没有,那么根据某些情况决定移动模式到某个合理的位置。

时间复杂度:

  • 平均情况:O(n/m)
  • 最坏情况:O(n * m)

实现:

#include <iostream>
#include <vector>

using namespace std;

void badCharHeuristic(const string &pattern, int size, int badChar[256]) {
    for (int i = 0; i < 256; i++)
        badChar[i] = -1;

    for (int i = 0; i < size; i++)
        badChar[(int)pattern[i]] = i;
}

void BMSearch(const string &text, const string &pattern) {
    int n = text.size();
    int m = pattern.size();

    int badChar[256];
    badCharHeuristic(pattern, m, badChar);

    int s = 0;
    while (s <= (n - m)) {
        int j = m - 1;

        while (j >= 0 && pattern[j] == text[s + j])
            j--;

        if (j < 0) {
            cout << "Pattern found at index " << s << endl;
            s += (s + m < n) ? m - badChar[text[s + m]] : 1;
        } else {
            s += max(1, j - badChar[text[s + j]]);
        }
    }
}

int main() {
    string text = "ABABABABCABABABCABABABC";
    string pattern = "ABABABC";
    BMSearch(text, pattern);
    return 0;
}

4. Sunday算法

原理:

Sunday算法是BM算法的一种变体,它预处理的是模式串紧随文本中当前窗口的字符,以此决定滑动步长。
一旦发现某一位置的字符不匹配,Sunday算法并不会逐个字符地往后滑动模式串,而是根据主串当前不匹配位置之后的一个字符来决定滑动的步长(这就是与BM算法不同的地方)
如果模式串后一个字符在模式串中出现,则将模式串移动到该字符在模式串中的最后一次出现位置的对齐处。
如果模式串后一个字符在模式串中没有出现,则直接将模式串向后滑动整个模式串长度+1的位置。

时间复杂度:

  • 最坏情况:O(n * m)
  • 平均情况:O(n)

实现:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

void SundaySearch(const string &text, const string &pattern) {
    int n = text.size();
    int m = pattern.size();
    vector<int> shift(256, m + 1);

    for (int i = 0; i < m; i++) {
        shift[pattern[i]] = m - i;
    }

    int i = 0;
    while (i <= n - m) {
        int j = 0;
        while (j < m && pattern[j] == text[i + j]) {
            j++;
        }
        if (j == m) {
            cout << "Pattern found at index " << i << endl;
        }
        i += shift[text[i + m]];
    }
}

int main() {
    string text = "ABABABABCABABABCABABABC";
    string pattern = "ABABABC";
    SundaySearch(text, pattern);
    return 0;
}

5. CUDA加速字符串匹配

原理:

CUDA可以利用GPU的并行计算能力对大规模数据进行加速处理。在字符串匹配中,可以将每个字符的比较任务分发到不同的线程,从而并行完成匹配操作。

CUDA实现朴素算法:

#include <iostream>
#include <cuda_runtime.h>

using namespace std;

__global__ void bruteForceCUDA(char* d_text, char* d_pattern, int n, int m, int* d_result) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx <= n - m) {
        bool match = true;
        for (int j = 0; j < m; j++) {
            if (d_text[idx + j] != d_pattern[j]) {
                match = false;
                break;
            }
        }
        if (match) {
            d_result[idx] = 1;
        }
    }
}

int main() {
    string text = "ABABABABCABABABCABABABC";
    string pattern = "ABABABC";
    int n = text.size();
    int m = pattern.size();

    char* d_text;
    char* d_pattern;
    int* d_result;
    int* result = new int[n - m + 1]();

    cudaMalloc(&d_text, n * sizeof(char));
    cudaMalloc(&d_pattern, m * sizeof(char));
    cudaMalloc(&d_result, (n - m + 1) * sizeof(int));

    cudaMemcpy(d_text, text.c_str(), n * sizeof(char), cudaMemcpyHostToDevice);
    cudaMemcpy(d_pattern, pattern.c_str(), m * sizeof(char), cudaMemcpyHostToDevice);

    bruteForceCUDA<<<(n - m + 1) / 256 + 1, 256>>>(d_text, d_pattern, n, m, d_result);

    cudaMemcpy(result, d_result, (n - m + 1) * sizeof(int), cudaMemcpyDeviceToHost);

    for (int i = 0; i <= n - m; i++) {
        if (result[i] == 1) {
            cout << "Pattern found at index " << i << endl;
        }
    }

    cudaFree(d_text);
    cudaFree(d_pattern);
    cudaFree(d_result);
    delete[] result;

    return 0;
}

简单的实验对比

数据:大肠杆菌基因组序列的一个片段
Brute Force: 10 microseconds, Matches: 921
KMP: 15 microseconds, Matches: 921
BM: 4 microseconds, Matches: 921
Sunday: 4 microseconds, Matches: 921

总结:

不同的字符串匹配算法在不同场景下各有优势:

  • 朴素算法简单易懂,适合小规模数据。
  • KMP算法适合需要频繁回退匹配的场景。
  • BM和Sunday算法在大规模文本匹配中效率较高。
  • CUDA加速算法则适用于大规模并行计算环境,在硬件支持的情况下能够极大提升性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值