直接跳到内容

字符串算法

什么是字符串算法

字符串算法是专门处理文本数据的一系列计算方法,解决字符串匹配、搜索、比较和转换等问题。字符串作为最常见的数据类型之一,其高效处理直接影响搜索引擎、文本编辑器和数据分析系统的性能。

特点:

  • 文本处理:专注于字符序列的操作和分析
  • 模式匹配:在文本中快速定位特定模式
  • 效率关键:处理大规模文本时性能差异显著
  • 应用广泛:从简单搜索到复杂生物信息学分析

示意图 (字符串基本操作):

文本: "hello world"
模式: "world"
        ↓ 字符串匹配
位置: 6

字符串匹配算法

朴素字符串匹配

逐个比较文本和模式的每个字符,是最简单直接的匹配方法。

特点:

  • 简单直观:算法逻辑易于理解和实现
  • 无预处理:不需要对模式或文本进行预处理
  • 效率低下:最坏情况时间复杂度 O(mn)
javascript
// 朴素字符串匹配算法
export function naiveStringSearch(text, pattern) {
  const n = text.length;
  const m = pattern.length;
  const positions = [];
  let comparisons = 0;

  // 遍历所有可能的起始位置
  for (let i = 0; i <= n - m; i++) {
    let j;
    for (j = 0; j < m; j++) {
      comparisons++;
      if (text[i + j] !== pattern[j]) {
        break;
      }
    }
    
    // 如果整个模式匹配成功
    if (j === m) {
      positions.push(i);
    }
  }

  return {
    positions,
    comparisons,
    patternLength: m,
    textLength: n
  };
}

// 优化版朴素匹配 - 在发现不匹配时立即停止
export function optimizedNaiveSearch(text, pattern) {
  const n = text.length;
  const m = pattern.length;
  const positions = [];
  let comparisons = 0;

  for (let i = 0; i <= n - m; i++) {
    let matched = true;
    
    for (let j = 0; j < m; j++) {
      comparisons++;
      if (text[i + j] !== pattern[j]) {
        matched = false;
        break; // 提前终止内层循环
      }
    }
    
    if (matched) {
      positions.push(i);
    }
  }

  return {
    positions,
    comparisons,
    efficiency: comparisons / (n * m) // 比较效率
  };
}

示意图 (朴素匹配过程):

文本:    "ABABDABACDABABCABAB"
模式:    "ABABC"

i=0: ABABD ≠ ABABC → 不匹配
i=1: BABDA ≠ ABABC → 不匹配  
i=2: ABDAB ≠ ABABC → 不匹配
i=3: DABAC ≠ ABABC → 不匹配
i=4: ABACD ≠ ABABC → 不匹配
i=5: BACDA ≠ ABABC → 不匹配
i=6: ACDAB ≠ ABABC → 不匹配
i=7: CDABA ≠ ABABC → 不匹配
i=8: DABAB ≠ ABABC → 不匹配
i=9: ABABC = ABABC → 匹配位置9
i=10: BABCA ≠ ABABC → 不匹配
...

KMP 算法

利用部分匹配表避免重复比较,实现高效的字符串匹配。

特点:

  • 部分匹配:构建前缀函数避免回溯
  • 线性时间:预处理 O(m),匹配 O(n)
  • 无回溯:文本指针只前进不后退
javascript
// KMP算法实现
export function kmpSearch(text, pattern) {
  const n = text.length;
  const m = pattern.length;
  
  if (m === 0) return { positions: [0], comparisons: 0 };
  
  // 构建部分匹配表
  const lps = computeLPSArray(pattern);
  const positions = [];
  let comparisons = 0;
  
  let i = 0; // 文本索引
  let j = 0; // 模式索引
  
  while (i < n) {
    comparisons++;
    if (pattern[j] === text[i]) {
      i++;
      j++;
    }
    
    if (j === m) {
      // 找到匹配
      positions.push(i - j);
      j = lps[j - 1];
    } else if (i < n && pattern[j] !== text[i]) {
      // 不匹配时利用LPS表跳过
      if (j !== 0) {
        j = lps[j - 1];
      } else {
        i++;
      }
    }
  }
  
  return {
    positions,
    comparisons,
    lpsTable: lps
  };
}

// 计算最长前缀后缀表
function computeLPSArray(pattern) {
  const m = pattern.length;
  const lps = new Array(m).fill(0);
  let length = 0; // 当前最长前缀后缀长度
  let i = 1;
  
  while (i < m) {
    if (pattern[i] === pattern[length]) {
      length++;
      lps[i] = length;
      i++;
    } else {
      if (length !== 0) {
        length = lps[length - 1];
      } else {
        lps[i] = 0;
        i++;
      }
    }
  }
  
  return lps;
}

// 可视化LPS表构建过程
export function visualizeLPS(pattern) {
  const lps = computeLPSArray(pattern);
  const steps = [];
  
  console.log(`构建模式 "${pattern}" 的LPS表:`);
  for (let i = 0; i < pattern.length; i++) {
    let prefix = pattern.slice(0, lps[i]);
    let suffix = pattern.slice(i - lps[i] + 1, i + 1);
    steps.push({
      index: i,
      character: pattern[i],
      lpsValue: lps[i],
      prefix,
      suffix,
      match: prefix === suffix
    });
  }
  
  return steps;
}

示意图 (KMP 算法 LPS 表):

模式: "ABABC"
LPS表构建:
i=0: "A" → LPS[0]=0
i=1: "AB" → 前缀"A"≠后缀"B" → LPS[1]=0  
i=2: "ABA" → 前缀"A"=后缀"A" → LPS[2]=1
i=3: "ABAB" → 前缀"AB"≠后缀"AB"? 检查: 
     前缀"A"=后缀"B"? 不匹配 → LPS[3]=0? 
     但前缀"AB"=后缀"AB"? 实际: LPS[3]=2
i=4: "ABABC" → 前缀"ABC"≠后缀"ABC"? 检查:
     前缀"A"=后缀"C"? 不匹配 → LPS[4]=0

最终LPS: [0,0,1,2,0]

匹配过程:
文本: "ABABDABACDABABCABAB"
模式: "ABABC"
i=0,j=0: A=A → i=1,j=1
i=1,j=1: B=B → i=2,j=2  
i=2,j=2: A=A → i=3,j=3
i=3,j=3: B=B → i=4,j=4
i=4,j=4: D≠C → 不匹配,j=LPS[3]=2
继续比较 i=4,j=2: D≠A → j=LPS[1]=0
继续比较 i=4,j=0: D≠A → i=5
...
直到找到匹配

Boyer-Moore 算法

从模式末尾开始比较,利用坏字符和好后缀规则实现大幅跳跃。

特点:

  • 反向比较:从模式末尾开始匹配
  • 启发跳跃:利用坏字符和好后缀规则跳过多个位置
  • 实践高效:在实际应用中通常比 KMP 更快
javascript
// Boyer-Moore算法实现
export function boyerMooreSearch(text, pattern) {
  const n = text.length;
  const m = pattern.length;
  
  if (m === 0) return { positions: [0], comparisons: 0 };
  
  // 预处理:坏字符表
  const badCharTable = buildBadCharTable(pattern);
  // 预处理:好后缀表
  const goodSuffixTable = buildGoodSuffixTable(pattern);
  
  const positions = [];
  let comparisons = 0;
  let shift = 0;
  
  while (shift <= n - m) {
    let j = m - 1;
    
    // 从右向左比较
    while (j >= 0 && pattern[j] === text[shift + j]) {
      comparisons++;
      j--;
    }
    
    if (j < 0) {
      // 找到匹配
      positions.push(shift);
      
      // 使用好后缀规则决定下一个移位
      shift += (shift + m < n) ? m - badCharTable[text.charCodeAt(shift + m)] : 1;
    } else {
      // 不匹配,计算两个规则建议的移位,取较大值
      comparisons++;
      const badCharShift = j - badCharTable[text.charCodeAt(shift + j)];
      const goodSuffixShift = goodSuffixTable[j];
      
      shift += Math.max(1, Math.max(badCharShift, goodSuffixShift));
    }
  }
  
  return {
    positions,
    comparisons,
    badCharTable,
    goodSuffixTable
  };
}

// 构建坏字符表
function buildBadCharTable(pattern) {
  const table = new Array(256).fill(-1); // ASCII字符表
  const m = pattern.length;
  
  for (let i = 0; i < m; i++) {
    table[pattern.charCodeAt(i)] = i;
  }
  
  return table;
}

// 构建好后缀表
function buildGoodSuffixTable(pattern) {
  const m = pattern.length;
  const table = new Array(m).fill(0);
  const suffixes = new Array(m).fill(0);
  
  // 计算后缀数组
  suffixes[m - 1] = m;
  let g = m - 1;
  let f = 0;
  
  for (let i = m - 2; i >= 0; i--) {
    if (i > g && suffixes[i + m - 1 - f] < i - g) {
      suffixes[i] = suffixes[i + m - 1 - f];
    } else {
      if (i < g) g = i;
      f = i;
      
      while (g >= 0 && pattern[g] === pattern[g + m - 1 - f]) {
        g--;
      }
      suffixes[i] = f - g;
    }
  }
  
  // 构建好后缀表
  for (let i = 0; i < m; i++) {
    table[i] = m;
  }
  
  for (let i = m - 1; i >= 0; i--) {
    if (suffixes[i] === i + 1) {
      for (let j = 0; j < m - 1 - i; j++) {
        if (table[j] === m) {
          table[j] = m - 1 - i;
        }
      }
    }
  }
  
  for (let i = 0; i <= m - 2; i++) {
    table[m - 1 - suffixes[i]] = m - 1 - i;
  }
  
  return table;
}

示意图 (Boyer-Moore 算法):

文本:    "ABAAABCDBBABCDEF"
模式:    "ABCD"
坏字符表: A:0, B:1, C:2, D:3, 其他:-1

第一次比较(shift=0):
文本: ABAAABCD...
模式: ABCD
      ↑ 从右比较: D≠A → 坏字符'A'在模式位置0
     移位 = 坏字符位置3 - 表中位置0 = 3 → shift=3

第二次比较(shift=3):
文本: ABAAABCD...
模式:    ABCD
            ↑ 从右比较: D=D, C=C, B=B, A=A → 完全匹配
找到匹配位置3

第三次比较(shift=7):
文本: ABAAABCDBBABCDEF
模式:        ABCD
                 ↑ 从右比较: D≠B → 坏字符'B'在模式位置1
     移位 = 坏字符位置3 - 表中位置1 = 2 → shift=9

继续比较直到结束...

字符串搜索数据结构

Trie 树

前缀树,用于高效存储和检索字符串集合。

特点:

  • 前缀共享:相同前缀的字符串共享节点
  • 快速检索:搜索时间复杂度 O(m),m 为模式长度
  • 空间优化:压缩版本可减少内存使用
javascript
// Trie节点类
export class TrieNode {
  constructor() {
    this.children = new Map();
    this.isEndOfWord = false;
    this.frequency = 0; // 词频统计
  }
}

// Trie树实现
export class Trie {
  constructor() {
    this.root = new TrieNode();
    this.size = 0; // 存储的单词数量
  }

  // 插入单词
  insert(word) {
    let currentNode = this.root;
    
    for (const char of word) {
      if (!currentNode.children.has(char)) {
        currentNode.children.set(char, new TrieNode());
      }
      currentNode = currentNode.children.get(char);
    }
    
    if (!currentNode.isEndOfWord) {
      this.size++;
    }
    currentNode.isEndOfWord = true;
    currentNode.frequency++;
  }

  // 搜索单词
  search(word) {
    let currentNode = this.root;
    
    for (const char of word) {
      if (!currentNode.children.has(char)) {
        return { found: false, frequency: 0 };
      }
      currentNode = currentNode.children.get(char);
    }
    
    return {
      found: currentNode.isEndOfWord,
      frequency: currentNode.frequency
    };
  }

  // 前缀搜索
  startsWith(prefix) {
    let currentNode = this.root;
    
    for (const char of prefix) {
      if (!currentNode.children.has(char)) {
        return false;
      }
      currentNode = currentNode.children.get(char);
    }
    
    return true;
  }

  // 获取所有以给定前缀开头的单词
  getWordsWithPrefix(prefix) {
    const results = [];
    let currentNode = this.root;
    
    // 导航到前缀的最后一个节点
    for (const char of prefix) {
      if (!currentNode.children.has(char)) {
        return results; // 前缀不存在
      }
      currentNode = currentNode.children.get(char);
    }
    
    // 收集所有单词
    this.collectWords(currentNode, prefix, results);
    return results;
  }

  // 递归收集单词
  collectWords(node, currentWord, results) {
    if (node.isEndOfWord) {
      results.push({
        word: currentWord,
        frequency: node.frequency
      });
    }
    
    for (const [char, childNode] of node.children) {
      this.collectWords(childNode, currentWord + char, results);
    }
  }

  // 删除单词
  delete(word) {
    this.deleteRecursive(this.root, word, 0);
  }

  deleteRecursive(node, word, depth) {
    if (!node) return false;

    if (depth === word.length) {
      if (node.isEndOfWord) {
        node.isEndOfWord = false;
        this.size--;
        return node.children.size === 0;
      }
      return false;
    }

    const char = word[depth];
    const childNode = node.children.get(char);
    
    if (!childNode) return false;

    const shouldDeleteChild = this.deleteRecursive(childNode, word, depth + 1);

    if (shouldDeleteChild) {
      node.children.delete(char);
      return node.children.size === 0 && !node.isEndOfWord;
    }

    return false;
  }
}

示意图 (Trie 树结构):

插入单词: "cat", "car", "card", "dog"

Trie结构:
    root
    /  \
   c    d
  /      \
 a        o
/ \        \
t* r*       g*
   |
   d*

* 表示单词结束节点

搜索过程:
搜索 "car": root → c → a → r* → 找到
搜索 "cat": root → c → a → t* → 找到  
搜索 "ca": root → c → a → 前缀存在
搜索 "cab": root → c → a → 无b子节点 → 未找到

Aho-Corasick 算法

多模式匹配算法,在 Trie 基础上添加失败指针实现高效多模式搜索。

特点:

  • 多模式匹配:同时搜索多个模式串
  • 自动机转换:将 Trie 扩展为有限状态自动机
  • 线性时间:预处理后可在 O(n + m + z) 时间内完成搜索
javascript
// Aho-Corasick算法实现
export class AhoCorasick {
  constructor() {
    this.root = new TrieNode();
    this.isBuilt = false;
  }

  // 添加模式
  addPattern(pattern) {
    let currentNode = this.root;
    
    for (const char of pattern) {
      if (!currentNode.children.has(char)) {
        currentNode.children.set(char, new TrieNode());
      }
      currentNode = currentNode.children.get(char);
    }
    
    currentNode.isEndOfWord = true;
    currentNode.output = pattern; // 存储完整模式
    this.isBuilt = false;
  }

  // 构建失败指针
  buildFailureLinks() {
    const queue = [];
    
    // 第一层节点的失败指针指向root
    for (const [char, child] of this.root.children) {
      child.fail = this.root;
      queue.push(child);
    }
    
    // 广度优先构建失败指针
    while (queue.length > 0) {
      const currentNode = queue.shift();
      
      for (const [char, child] of currentNode.children) {
        let failNode = currentNode.fail;
        
        // 沿着失败指针链找到有char子节点的节点
        while (failNode !== this.root && !failNode.children.has(char)) {
          failNode = failNode.fail;
        }
        
        if (failNode.children.has(char)) {
          child.fail = failNode.children.get(char);
        } else {
          child.fail = this.root;
        }
        
        // 如果失败指针指向的节点是输出节点,继承输出
        if (child.fail.isEndOfWord && !child.output) {
          child.output = child.fail.output;
        }
        
        queue.push(child);
      }
    }
    
    this.isBuilt = true;
  }

  // 搜索文本中的所有模式
  search(text) {
    if (!this.isBuilt) {
      this.buildFailureLinks();
    }
    
    const results = [];
    let currentNode = this.root;
    
    for (let i = 0; i < text.length; i++) {
      const char = text[i];
      
      // 沿着失败指针链找到有当前字符子节点的节点
      while (currentNode !== this.root && !currentNode.children.has(char)) {
        currentNode = currentNode.fail;
      }
      
      if (currentNode.children.has(char)) {
        currentNode = currentNode.children.get(char);
      }
      
      // 检查当前节点及其失败指针链上的输出
      let outputNode = currentNode;
      while (outputNode !== this.root) {
        if (outputNode.isEndOfWord) {
          results.push({
            pattern: outputNode.output,
            position: i - outputNode.output.length + 1,
            index: i
          });
        }
        outputNode = outputNode.fail;
      }
    }
    
    return results;
  }
}

示意图 (Aho-Corasick 自动机):

模式: "he", "she", "his", "hers"

构建的自动机:
状态0 (root) --h--> 状态1 --e--> 状态2 (输出: "he")
          --s--> 状态3 --h--> 状态4 --e--> 状态5 (输出: "she")
状态1 --i--> 状态6 --s--> 状态7 (输出: "his")  
状态1 --e--> 状态2
状态4 --r--> 状态8 --s--> 状态9 (输出: "hers")

失败指针:
状态2.fail → 状态8 (因为"e"在root无子节点,但状态8有"e"? 实际构建更复杂)
状态5.fail → 状态2 (最长后缀匹配)

搜索文本 "shers":
s: 状态0→3
h: 状态3→4
e: 状态4→5 → 输出"she"位置2
r: 状态5→状态2.fail=状态8 → 状态8
s: 状态8→9 → 输出"hers"位置3
找到 "she"和"hers"

字符串相似度算法

编辑距离

衡量两个字符串的相似度,通过计算从一个字符串转换到另一个字符串所需的最少操作次数。

特点:

  • 操作计数:插入、删除、替换操作的最小次数
  • 动态规划:使用表格存储子问题解
  • 应用广泛:拼写检查、生物信息学、自然语言处理
javascript
// 编辑距离算法
export function editDistance(str1, str2) {
  const m = str1.length;
  const n = str2.length;
  
  // 创建DP表格
  const dp = Array.from({ length: m + 1 }, () => 
    new Array(n + 1).fill(0)
  );
  
  // 初始化边界条件
  for (let i = 0; i <= m; i++) {
    dp[i][0] = i; // 从str1[0..i]到空字符串需要i次删除
  }
  
  for (let j = 0; j <= n; j++) {
    dp[0][j] = j; // 从空字符串到str2[0..j]需要j次插入
  }
  
  // 填充DP表格
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        // 字符相同,不需要操作
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        // 取插入、删除、替换操作的最小值
        dp[i][j] = 1 + Math.min(
          dp[i][j - 1],    // 插入
          dp[i - 1][j],    // 删除  
          dp[i - 1][j - 1] // 替换
        );
      }
    }
  }
  
  return {
    distance: dp[m][n],
    dpTable: dp,
    operations: reconstructOperations(dp, str1, str2)
  };
}

// 重建操作序列
function reconstructOperations(dp, str1, str2) {
  const operations = [];
  let i = dp.length - 1;
  let j = dp[0].length - 1;
  
  while (i > 0 || j > 0) {
    if (i > 0 && j > 0 && str1[i - 1] === str2[j - 1]) {
      // 字符相同,无操作
      operations.unshift({ type: 'match', char: str1[i - 1] });
      i--;
      j--;
    } else {
      const current = dp[i][j];
      
      if (i > 0 && j > 0 && dp[i - 1][j - 1] + 1 === current) {
        // 替换操作
        operations.unshift({ 
          type: 'replace', 
          from: str1[i - 1], 
          to: str2[j - 1] 
        });
        i--;
        j--;
      } else if (j > 0 && dp[i][j - 1] + 1 === current) {
        // 插入操作
        operations.unshift({ 
          type: 'insert', 
          char: str2[j - 1] 
        });
        j--;
      } else if (i > 0 && dp[i - 1][j] + 1 === current) {
        // 删除操作
        operations.unshift({ 
          type: 'delete', 
          char: str1[i - 1] 
        });
        i--;
      }
    }
  }
  
  return operations;
}

// 带权编辑距离(不同的操作有不同的代价)
export function weightedEditDistance(str1, str2, costs = {}) {
  const { insert = 1, delete: del = 1, replace = 1 } = costs;
  const m = str1.length;
  const n = str2.length;
  
  const dp = Array.from({ length: m + 1 }, () => 
    new Array(n + 1).fill(0)
  );
  
  for (let i = 0; i <= m; i++) {
    dp[i][0] = i * del;
  }
  
  for (let j = 0; j <= n; j++) {
    dp[0][j] = j * insert;
  }
  
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        dp[i][j] = Math.min(
          dp[i][j - 1] + insert,
          dp[i - 1][j] + del,
          dp[i - 1][j - 1] + replace
        );
      }
    }
  }
  
  return dp[m][n];
}

示意图 (编辑距离 DP 表格):

字符串1: "kitten"
字符串2: "sitting"

DP表格:
    "" s i t t i n g
""   0 1 2 3 4 5 6 7
k    1 1 2 3 4 5 6 7  
i    2 2 1 2 3 4 5 6
t    3 3 2 1 2 3 4 5
t    4 4 3 2 1 2 3 4
e    5 5 4 3 2 2 3 4
n    6 6 5 4 3 3 2 3

编辑距离 = 3
操作序列:
k → s (替换)
- → i (插入)
e → i (替换)

最长公共子序列

找到两个序列的最长公共子序列,不要求连续但保持相对顺序。

特点:

  • 相对顺序:子序列元素保持原有顺序
  • 动态规划:经典的二维动态规划问题
  • 应用广泛:版本控制、生物序列比对
javascript
// 最长公共子序列
export function longestCommonSubsequence(str1, str2) {
  const m = str1.length;
  const n = str2.length;
  
  // DP表格,dp[i][j]表示str1[0..i-1]和str2[0..j-1]的LCS长度
  const dp = Array.from({ length: m + 1 }, () => 
    new Array(n + 1).fill(0)
  );
  
  // 填充DP表格
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
  
  // 重建LCS
  const lcs = reconstructLCS(dp, str1, str2);
  
  return {
    length: dp[m][n],
    sequence: lcs,
    dpTable: dp
  };
}

// 重建LCS序列
function reconstructLCS(dp, str1, str2) {
  let i = dp.length - 1;
  let j = dp[0].length - 1;
  const lcs = [];
  
  while (i > 0 && j > 0) {
    if (str1[i - 1] === str2[j - 1]) {
      lcs.unshift(str1[i - 1]);
      i--;
      j--;
    } else if (dp[i - 1][j] > dp[i][j - 1]) {
      i--;
    } else {
      j--;
    }
  }
  
  return lcs.join('');
}

// 多序列LCS
export function multipleLCS(sequences) {
  if (sequences.length === 0) return '';
  if (sequences.length === 1) return sequences[0];
  
  let lcs = sequences[0];
  
  for (let i = 1; i < sequences.length; i++) {
    const result = longestCommonSubsequence(lcs, sequences[i]);
    lcs = result.sequence;
    
    if (lcs.length === 0) break; // 没有公共子序列
  }
  
  return lcs;
}

示意图 (LCS DP 表格):

字符串1: "ABCDGH"
字符串2: "AEDFHR"

DP表格:
    "" A E D F H R
""   0 0 0 0 0 0 0
A    0 1 1 1 1 1 1  
B    0 1 1 1 1 1 1
C    0 1 1 1 1 1 1
D    0 1 1 2 2 2 2
G    0 1 1 2 2 2 2
H    0 1 1 2 2 3 3

LCS长度 = 3
LCS序列: "ADH"

重建过程:
从(6,6)=3开始
H=H → 加入H → 移动到(5,5)
D≠F → 比较(4,5)=2和(5,4)=2 → 选择(4,5)
D=D → 加入D → 移动到(3,3)  
A=A → 加入A → 移动到(2,2)
完成,LCS="ADH"

高级字符串算法

Rabin-Karp 算法

使用哈希函数进行字符串匹配,通过滚动哈希实现高效搜索。

特点:

  • 哈希匹配:使用哈希值快速比较
  • 滚动哈希:常数时间更新哈希值
  • 多模式支持:天然支持多模式搜索
javascript
// Rabin-Karp算法
export function rabinKarpSearch(text, pattern, prime = 101) {
  const n = text.length;
  const m = pattern.length;
  const positions = [];
  let comparisons = 0;
  
  if (m === 0 || n < m) return { positions, comparisons };
  
  // 计算pattern哈希和第一个窗口哈希
  let patternHash = 0;
  let textHash = 0;
  let h = 1;
  
  // h = (d^(m-1)) % prime
  for (let i = 0; i < m - 1; i++) {
    h = (h * 256) % prime;
  }
  
  // 计算初始哈希值
  for (let i = 0; i < m; i++) {
    patternHash = (256 * patternHash + pattern.charCodeAt(i)) % prime;
    textHash = (256 * textHash + text.charCodeAt(i)) % prime;
  }
  
  // 滑动窗口
  for (let i = 0; i <= n - m; i++) {
    comparisons++;
    
    // 哈希值匹配时进行精确比较
    if (patternHash === textHash) {
      let match = true;
      
      for (let j = 0; j < m; j++) {
        comparisons++;
        if (text[i + j] !== pattern[j]) {
          match = false;
          break;
        }
      }
      
      if (match) {
        positions.push(i);
      }
    }
    
    // 计算下一个窗口的哈希值
    if (i < n - m) {
      textHash = (256 * (textHash - text.charCodeAt(i) * h) + 
                 text.charCodeAt(i + m)) % prime;
      
      // 处理负哈希值
      if (textHash < 0) {
        textHash += prime;
      }
    }
  }
  
  return {
    positions,
    comparisons,
    hashCollisions: comparisons - positions.length * m
  };
}

// 多模式Rabin-Karp
export function rabinKarpMultiple(text, patterns, prime = 101) {
  const results = [];
  const patternHashes = new Map();
  
  // 预计算所有模式的哈希值
  for (const pattern of patterns) {
    let hash = 0;
    for (let i = 0; i < pattern.length; i++) {
      hash = (256 * hash + pattern.charCodeAt(i)) % prime;
    }
    patternHashes.set(pattern, hash);
  }
  
  const m = Math.min(...patterns.map(p => p.length));
  const n = text.length;
  
  if (n < m) return results;
  
  let textHash = 0;
  let h = 1;
  
  for (let i = 0; i < m - 1; i++) {
    h = (h * 256) % prime;
  }
  
  for (let i = 0; i < m; i++) {
    textHash = (256 * textHash + text.charCodeAt(i)) % prime;
  }
  
  // 检查第一个窗口
  checkWindow(text, 0, textHash, patternHashes, results);
  
  // 滑动窗口
  for (let i = 1; i <= n - m; i++) {
    textHash = (256 * (textHash - text.charCodeAt(i - 1) * h) + 
               text.charCodeAt(i + m - 1)) % prime;
    
    if (textHash < 0) {
      textHash += prime;
    }
    
    checkWindow(text, i, textHash, patternHashes, results);
  }
  
  return results;
}

function checkWindow(text, position, textHash, patternHashes, results) {
  for (const [pattern, patternHash] of patternHashes) {
    if (textHash === patternHash) {
      // 哈希匹配,进行精确比较
      let match = true;
      for (let j = 0; j < pattern.length; j++) {
        if (text[position + j] !== pattern[j]) {
          match = false;
          break;
        }
      }
      
      if (match) {
        results.push({
          pattern,
          position
        });
      }
    }
  }
}

示意图 (Rabin-Karp 滚动哈希):

文本: "ABCDEFG"
模式: "CDE"
素数: 101

计算模式哈希:
C:67, D:68, E:69
hash = (256*(256*67 + 68) + 69) % 101
     = (256*(17152 + 68) + 69) % 101
     = (256*17220 + 69) % 101
     = (4408320 + 69) % 101 = 4408389 % 101 = 35

文本窗口哈希:
窗口"ABC": (256*(256*65 + 66) + 67) % 101 = 33
窗口"BCD": 
  新哈希 = (256*(33 - 65*h) + 68) % 101
  h = (256^(2)) % 101 = 65536 % 101 = 88
  新哈希 = (256*(33 - 65*88) + 68) % 101
        = (256*(33 - 5720) + 68) % 101
        = (256*(-5687) + 68) % 101
        = (-1455360 + 68) % 101 = -1455292 % 101
        = -1455292 + 14405*101 = 35 (调整后)

哈希匹配35=35,进行精确比较"CDE"="CDE" → 匹配位置2

后缀数组

存储字符串所有后缀的排序数组,支持高效字符串搜索和分析。

特点:

  • 后缀排序:所有后缀按字典序排列
  • 高效搜索:支持 O(m log n) 时间复杂度的模式搜索
  • 空间优化:相比后缀树更节省空间
javascript
// 后缀数组实现
export class SuffixArray {
  constructor(text) {
    this.text = text;
    this.suffixArray = this.buildSuffixArray();
    this.lcpArray = this.buildLCPArray();
  }

  // 构建后缀数组
  buildSuffixArray() {
    const n = this.text.length;
    const suffixes = [];
    
    // 存储所有后缀及其起始索引
    for (let i = 0; i < n; i++) {
      suffixes.push({
        index: i,
        suffix: this.text.substring(i)
      });
    }
    
    // 按后缀字典序排序
    suffixes.sort((a, b) => a.suffix.localeCompare(b.suffix));
    
    // 提取索引数组
    return suffixes.map(s => s.index);
  }

  // 构建LCP(最长公共前缀)数组
  buildLCPArray() {
    const n = this.text.length;
    const lcp = new Array(n).fill(0);
    const inverseSuffix = new Array(n);
    
    // 构建逆后缀数组
    for (let i = 0; i < n; i++) {
      inverseSuffix[this.suffixArray[i]] = i;
    }
    
    let k = 0;
    for (let i = 0; i < n; i++) {
      if (inverseSuffix[i] === n - 1) {
        k = 0;
        continue;
      }
      
      const j = this.suffixArray[inverseSuffix[i] + 1];
      
      // 计算LCP
      while (i + k < n && j + k < n && 
             this.text[i + k] === this.text[j + k]) {
        k++;
      }
      
      lcp[inverseSuffix[i]] = k;
      
      if (k > 0) k--;
    }
    
    return lcp;
  }

  // 在后缀数组中搜索模式
  search(pattern) {
    const n = this.text.length;
    const m = pattern.length;
    const positions = [];
    
    // 二分搜索找到模式的范围
    let left = 0;
    let right = n - 1;
    
    // 找到左边界
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const suffix = this.text.substring(this.suffixArray[mid]);
      const comparison = suffix.substring(0, m).localeCompare(pattern);
      
      if (comparison >= 0) {
        right = mid - 1;
      } else {
        left = mid + 1;
      }
    }
    
    const start = left;
    right = n - 1;
    
    // 找到右边界
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const suffix = this.text.substring(this.suffixArray[mid]);
      const comparison = suffix.substring(0, m).localeCompare(pattern);
      
      if (comparison <= 0) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
    
    const end = right;
    
    // 收集所有匹配位置
    for (let i = start; i <= end; i++) {
      positions.push(this.suffixArray[i]);
    }
    
    return {
      positions: positions.sort((a, b) => a - b),
      count: positions.length,
      range: [start, end]
    };
  }

  // 获取最长重复子串
  getLongestRepeatedSubstring() {
    let maxLength = 0;
    let maxIndex = -1;
    
    for (let i = 0; i < this.lcpArray.length; i++) {
      if (this.lcpArray[i] > maxLength) {
        maxLength = this.lcpArray[i];
        maxIndex = i;
      }
    }
    
    if (maxLength > 0) {
      const startIndex = this.suffixArray[maxIndex];
      return this.text.substring(startIndex, startIndex + maxLength);
    }
    
    return '';
  }

  // 获取所有重复子串
  getAllRepeatedSubstrings(minLength = 2) {
    const substrings = new Map();
    
    for (let i = 0; i < this.lcpArray.length; i++) {
      if (this.lcpArray[i] >= minLength) {
        const startIndex = this.suffixArray[i];
        const substring = this.text.substring(startIndex, startIndex + this.lcpArray[i]);
        
        if (!substrings.has(substring)) {
          substrings.set(substring, []);
        }
        substrings.get(substring).push(startIndex);
      }
    }
    
    return Array.from(substrings.entries()).map(([substring, positions]) => ({
      substring,
      positions,
      length: substring.length
    }));
  }
}

示意图 (后缀数组构建):

文本: "BANANA"

所有后缀:
0: "BANANA"
1: "ANANA" 
2: "NANA"
3: "ANA"
4: "NA"
5: "A"

排序后后缀数组:
索引 后缀
5    "A"
3    "ANA"
1    "ANANA"
0    "BANANA"
4    "NA"
2    "NANA"

后缀数组: [5, 3, 1, 0, 4, 2]

LCP数组: [0, 1, 3, 0, 0, 2]
解释: 
SA[0]和SA[1]: "A"和"ANA" → LCP=1 ("A")
SA[1]和SA[2]: "ANA"和"ANANA" → LCP=3 ("ANA")
SA[2]和SA[3]: "ANANA"和"BANANA" → LCP=0
SA[3]和SA[4]: "BANANA"和"NA" → LCP=0  
SA[4]和SA[5]: "NA"和"NANA" → LCP=2 ("NA")

搜索"ANA":
二分查找找到范围[1,2] → 位置3,1
字符串算法已经加载完毕