2026/1/24 15:18:02
网站建设
项目流程
商业网站建设所用软件,网站建设捌金手指花总二六,中国建设银行网站官网网址,wordpress 后台栏目希函数将模式串和文本串中的子串转换为数值进行比较#xff0c;避免大量不必要的字符比较。这个算法特别适合多模式串匹配场景#xff0c;时间复杂度平均为O(nm)#xff0c;n是文本串长度#xff0c;m是模式串长度。Rabin-Karp算法的关键在于使用滚动哈希函数#xff08;R…希函数将模式串和文本串中的子串转换为数值进行比较避免大量不必要的字符比较。这个算法特别适合多模式串匹配场景时间复杂度平均为O(nm)n是文本串长度m是模式串长度。Rabin-Karp算法的关键在于使用滚动哈希函数Rolling Hash它可以在常数时间内计算出滑动窗口的新哈希值保证算法在大多数情况下的高效性。算法步骤计算模式串的哈希值计算文本串中长度为m的第一个子串的哈希值m为模式串长度在文本串上滑动窗口对于每个位置使用滚动哈希技术高效计算当前窗口的哈希值如果哈希值与模式串相等则进行字符逐一比较以避免哈希冲突如果完全匹配则找到一个匹配位置重复步骤3直到处理完整个文本串核心特性基于哈希比较通过哈希值比较代替直接字符比较滚动哈希O(1)时间复杂度计算下一窗口的哈希值时间复杂度平均情况O(nm)最坏情况O(n*m)空间复杂度O(1)只需常数额外空间适用范围单模式和多模式串匹配场景特别是多模式匹配基础实现接下来大家一起看下Rabin-Karp算法的部分主流语言实现javapublic class RabinKarp {private final static int PRIME 101; // 哈希计算使用的质数public static int search(String text, String pattern) {int m pattern.length();int n text.length();if (m n) return -1;if (m 0) return 0;// 计算哈希乘数等于d^(m-1) % PRIME用于滚动哈希计算int h 1;for (int i 0; i m - 1; i) {h (h * 256) % PRIME;}// 计算模式串和第一个窗口的哈希值int patternHash 0;int textHash 0;for (int i 0; i m; i) {patternHash (256 * patternHash pattern.charAt(i)) % PRIME;textHash (256 * textHash text.charAt(i)) % PRIME;}// 滑动窗口比较哈希值for (int i 0; i n - m; i) {// 哈希值相等时检查是否真正匹配if (patternHash textHash) {boolean match true;for (int j 0; j m; j) {if (text.charAt(i j) ! pattern.charAt(j)) {match false;break;}}if (match) {return i; // 找到匹配}}// 计算下一个窗口的哈希值if (i n - m) {textHash (256 * (textHash - text.charAt(i) * h) text.charAt(i m)) % PRIME;// 处理负数哈希值if (textHash 0) {textHash PRIME;}}}return -1; // 未找到匹配}// 打印结果public static void main(String[] args) {String text ABABCABABDABACDABABCABAB;String pattern ABABCABAB;int position search(text, pattern);if (position -1) {System.out.println(未找到匹配);} else {System.out.println(模式串在位置 position 处匹配);System.out.println(text);// 打印指示匹配位置的指针for (int i 0; i position; i) {System.out.print( );}System.out.println(pattern);}}}优化使用更好的哈希函数比如使用更复杂的哈希函数来减少冲突javapublic class ImprovedRabinKarp {private final static long PRIME1 1000000007; // 第一个哈希的质数private final static long PRIME2 1000000009; // 第二个哈希的质数// 使用双哈希来减少冲突public static int search(String text, String pattern) {int m pattern.length();int n text.length();if (m n) return -1;if (m 0) return 0;// 计算哈希乘数long h1 1;long h2 1;for (int i 0; i m - 1; i) {h1 (h1 * 256) % PRIME1;h2 (h2 * 256) % PRIME2;}// 计算模式串和第一个窗口的哈希值long patternHash1 0;long patternHash2 0;long textHash1 0;long textHash2 0;for (int i 0; i m; i) {patternHash1 (256 * patternHash1 pattern.charAt(i)) % PRIME1;patternHash2 (256 * patternHash2 pattern.charAt(i)) % PRIME2;textHash1 (256 * textHash1 text.charAt(i)) % PRIME1;textHash2 (256 * textHash2 text.charAt(i)) % PRIME2;}// 滑动窗口比较哈希值for (int i 0; i n - m; i) {// 两个哈希都相等时再进行字符比较if (patternHash1 textHash1 patternHash2 textHash2) {boolean match true;for (int j 0; j m; j) {if (text.charAt(i j) ! pattern.charAt(j)) {match false;break;}}if (match) {return i; // 找到匹配}}// 计算下一个窗口的哈希值if (i n - m) {textHash1 (256 * (textHash1 - text.charAt(i) * h1) text.charAt(i m)) % PRIME1;textHash2 (256 * (textHash2 - text.charAt(i) * h2) text.charAt(i m)) % PRIME2;// 处理负数哈希值if (textHash1 0) textHash1 PRIME1;if (textHash2 0) textHash2 PRIME2;}}return -1; // 未找到匹配}}优点平均情况下时间复杂度为O(nm)接近线性时间在多模式匹配场景下效率高可以通过预处理模式串提高效率滚动哈希计算使得算法高效移动窗口实现相对简单原理容易理解缺点哈希冲突可能导致额外的字符比较最坏情况下的时间复杂度为O(n*m)哈希函数的选择对算法性能影响很大需要注意数值溢出问题对于短模式串和文本串预处理开销可能抵消算法优势应用场景1文档相似度检测和抄袭检测2网络安全中的特征码匹配3多模式字符串搜索引擎4编译器中的词法分析器扩展Rabin-Karp指纹算法Rabin-Karp算法的一个变种应用于文件相似度比较javapublic class RabinKarpFingerprint {private final static long PRIME 1000000007;private final static int WINDOW_SIZE 5; // 指纹窗口大小public static SetLong generateFingerprints(String text) {SetLong fingerprints new HashSet();int n text.length();if (n WINDOW_SIZE) {fingerprints.add(calculateHash(text, n));return fingerprints;}// 计算第一个窗口的哈希值long textHash calculateHash(text, WINDOW_SIZE);fingerprints.add(textHash);// 计算哈希乘数long h 1;for (int i 0; i WINDOW_SIZE - 1; i) {h (h * 256) % PRIME;}// 滑动窗口计算所有长度为WINDOW_SIZE的子串哈希值for (int i 0; i n - WINDOW_SIZE - 1; i) {textHash (256 * (textHash - text.charAt(i) * h) text.charAt(i WINDOW_SIZE)) % PRIME;if (textHash 0) {textHash PRIME;}fingerprints.add(textHash);}return fingerprints;}public static double calculateSimilarity(String text1, String text2) {SetLong fingerprints1 generateFingerprints(text1);SetLong fingerprints2 generateFingerprints(text2);// 计算交集大小SetLong intersection new HashSet(fingerprints1);intersection.retainAll(fingerprints2);// 计算并集大小SetLong union new HashSet(fingerprints1);union.addAll(fingerprints2);// 杰卡德相似度系数return (double) intersection.size() / union.size();}private static long calculateHash(String str, int length) {long hash 0;for (int i 0; i length; i) {hash (256 * hash str.charAt(i)) % PRIME;}return hash;}}扩展子字符串哈希一些编程竞赛里也使用Rabin-Karp思想进行高效的子字符串查询javapublic class SubstringHash {private static final long PRIME 1000000007;private static final int BASE 256;private long[] hash; // 前缀哈希值private long[] pow; // BASE的幂private String s; // 源字符串public SubstringHash(String s) {this.s s;int n s.length();hash new long[n 1];pow new long[n 1];// 预计算BASE的幂pow[0] 1;for (int i 1; i n; i) {pow[i] (pow[i - 1] * BASE) % PRIME;}// 计算所有前缀的哈希值hash[0] 0;for (int i 0; i n; i) {hash[i 1] (hash[i] * BASE s.charAt(i)) % PRIME;}}// 计算子串s[l..r]的哈希值0-indexedpublic long substringHash(int l, int r) {// 获取s[0...r]的哈希值减去s[0...l-1]的哈希值需要进行适当调整long result (hash[r 1] - (hash[l] * pow[r - l 1]) % PRIME) % PRIME;if (result 0) {result PRIME;}return result;}// 检查两个子串是否相同public boolean areSubstringsEqual(int l1, int r1, int l2, int r2) {if (r1 - l1 ! r2 - l2) {return false; // 长度不同}return substringHash(l1, r1) substringHash(l2, r2);}}相关的 LeetCode 热门题目28. 实现 strStr()187. 重复的DNA序列 - 利用Rabin-Karp滚动哈希思想解决1044. 最长重复子串 - 结合二分查找和Rabin-Karp算法1554. 只有一个不同字符的字符串Rabin-Karp算法巧妙结合哈希计算和滚动窗口技术在字符串匹配领域提供了一种高效的解决方案特别适合多模式匹配和大规模文本处理场景。Boyer-Moore算法Boyer-Moore算法是一种高效的字符串匹配算法由 Robert S. Boyer和J Strother Moore 设计于1977年。它从右向左比较字符并利用两个启发式规则坏字符规则和好后缀规则在不匹配情况下实现较大跳跃减少比较次数。Boyer-Moore算法在实际应用中大部分情况下比朴素算法和KMP算法更高效。算法步骤预处理模式串构建坏字符表和好后缀表将模式串对齐到文本串的开始位置从模式串的最右侧字符开始比较从右向左进行匹配如果发生不匹配通过以下规则计算跳转距离坏字符规则根据不匹配字符在模式串中的最右位置决定跳转距离好后缀规则根据已匹配部分在模式串中的重复情况决定跳转距离选择两个规则中的最大跳转距离移动模式串重复步骤3-5直到找到匹配或到达文本串末尾核心特性从右向左比较与大多数字符串匹配算法不同从模式串的末尾开始比较双规则跳转利用坏字符规则和好后缀规则计算跳转距离时间复杂度最坏情况O(m*n)m是模式串长度n是文本串长度平均情况接近O(n/m)空间复杂度O(km)其中k是字符集大小m是模式串长度适用范围特别适合长模式串和大字符集场景基础实现javapublic class BoyerMoore {private final int R; // 字符集大小private int[] badChar; // 坏字符表private int[] goodSuffix; // 好后缀表private int[] borderPos; // 边界位置表private String pattern; // 模式串public BoyerMoore(String pattern) {this.R 256; // ASCII字符集this.pattern pattern;int m pattern.length();// 初始化坏字符表badChar new int[R];for (int c 0; c R; c) {badChar[c] -1; // 初始化为-1}for (int j 0; j m; j) {badChar[pattern.charAt(j)] j; // 记录每个字符最右出现位置}// 初始化好后缀表和边界位置表goodSuffix new int[m];borderPos new int[m];processSuffixes();}// 预处理好后缀表private void processSuffixes() {int m pattern.length();int i m, j m 1;borderPos[i] j;// 计算边界位置while (i 0) {while (j m pattern.charAt(i - 1) ! pattern.charAt(j - 1)) {if (goodSuffix[j] 0) {goodSuffix[j] j - i;}j borderPos[j];}i--; j--;borderPos[i] j;}// 计算好后缀表j borderPos[0];for (i 0; i m; i) {if (goodSuffix[i] 0) {goodSuffix[i] j;}if (i j) {j borderPos[j];}}}// 搜索文本串中的匹配public int search(String text) {int n text.length();int m pattern.length();if (m 0) return 0;int skip;for (int i 0; i n - m; i skip) {skip 0;for (int j m - 1; j 0; j--) {if (pattern.charAt(j) ! text.charAt(i j)) {// 坏字符规则skip Math.max(1, j - badChar[text.charAt(i j)]);// 好后缀规则if (j m - 1) {skip Math.max(skip, goodSuffix[j 1]);}break;}}if (skip 0) return i; // 找到匹配}return -1; // 没有找到匹配}// 测试public static void main(String[] args) {String text HERE IS A SIMPLE EXAMPLE;String pattern EXAMPLE;BoyerMoore bm new BoyerMoore(pattern);int position bm.search(text);if (position -1) {System.out.println(未找到匹配);} else {System.out.println(模式串在位置 position 处匹配);System.out.println(text);for (int i 0; i position; i) {System.out.print( );}System.out.println(pattern);}}}优化策略简化好后缀表构建对于一些应用场景可以只使用坏字符规则简化算法实现javapublic class SimplifiedBoyerMoore {private final int R; // 字符集大小private int[] badChar; // 坏字符表private String pattern; // 模式串public SimplifiedBoyerMoore(String pattern) {this.R 256; // ASCII字符集this.pattern pattern;int m pattern.length();// 初始化坏字符表badChar new int[R];for (int c 0; c R; c) {badChar[c] -1; // 初始化为-1}for (int j 0; j m; j) {badChar[pattern.charAt(j)] j; // 记录每个字符最右出现位置}}// 搜索文本串中的匹配public int search(String text) {int n text.length();int m pattern.length();if (m 0) return 0;int skip;for (int i 0; i n - m; i skip) {skip 0;for (int j m - 1; j 0; j--) {if (pattern.charAt(j) ! text.charAt(i j)) {// 仅使用坏字符规则skip Math.max(1, j - badChar[text.charAt(i j)]);break;}}if (skip 0) return i; // 找到匹配}return -1; // 没有找到匹配}}缓存预计算结果针对需要重复搜索同一模式串的场景可以预计算并缓存结果javapublic class CachedBoyerMoore {private MapString, BoyerMoore cache new HashMap();public int search(String text, String pattern) {// 检查缓存中是否有预计算的Boyer-Moore对象BoyerMoore bm cache.get(pattern);if (bm null) {bm new BoyerMoore(pattern);cache.put(pattern, bm);}return bm.search(text);}}优点在实际应用中大部分场景比KMP和朴素算法更高效最好情况下可以跳过大量文本实现亚线性时间复杂度对于长模式串和大字符集特别有效预处理跟模式串有关与文本串长度无关缺点预处理复杂特别是好后缀表的构建需要额外空间存储坏字符表和好后缀表最坏情况下时间复杂度仍为O(m*n)对于短模式串预处理开销可能抵消算法优势好后缀规则的实现较复杂容易出错应用场景1文本编辑器的查找功能2网络安全中的特征码匹配3自然语言处理中的关键词检索4大规模文本数据处理扩展Horspool算法Horspool算法是Boyer-Moore的简化版本只使用坏字符规则但是对坏字符表进行了修改javapublic class Horspool {private final int R; // 字符集大小private int[] badChar; // 坏字符表private String pattern; // 模式串public Horspool(String pattern) {this.R 256; // ASCII字符集this.pattern pattern;int m pattern.length();// 初始化坏字符表badChar new int[R];// 所有字符默认移动模式串长度for (int c 0; c R; c) {badChar[c] m;}// 模式串中的字符除了最后一个设置为对应值for (int j 0; j m - 1; j) {badChar[pattern.charAt(j)] m - 1 - j;}}// 搜索文本串中的匹配public int search(String text) {int n text.length();int m pattern.length();if (m 0) return 0;if (m n) return -1;int i m - 1; // 从模式串最后一个字符对齐开始while (i n) {int k 0;while (k m pattern.charAt(m - 1 - k) text.charAt(i - k)) {k;}if (k m) {return i - m 1; // 找到匹配}// 使用坏字符规则移动i badChar[text.charAt(i)];}return -1; // 没有找到匹配}}扩展Sunday算法Sunday算法是另一种Boyer-Moore的变种它关注的是文本串中模式串后面的字符javapublic class Sunday {private final int R; // 字符集大小private int[] shift; // 移动表private String pattern; // 模式串public Sunday(String pattern) {this.R 256; // ASCII字符集this.pattern pattern;int m pattern.length();// 初始化移动表shift new int[R];// 所有字符默认移动模式串长度1for (int c 0; c R; c) {shift[c] m 1;}// 模式串中的字符设置为对应值for (int j 0; j m; j) {shift[pattern.charAt(j)] m - j;}}// 搜索文本串中的匹配public int search(String text) {int n text.length();int m pattern.length();if (m 0) return 0;if (m n) return -1;int i 0; // 从文本串开始位置while (i n - m) {int j 0;while (j m pattern.charAt(j) text.charAt(i j)) {j;}if (j m) {return i; // 找到匹配}// 下一个位置超出文本串长度返回-1if (i m n) {return -1;}// 使用Sunday算法的移动规则i shift[text.charAt(i m)];}return -1; // 没有找到匹配}}相关的 LeetCode 热门题目28. 实现strStr()459. 重复的子字符串686. 重复叠加字符串匹配1392. 最长快乐前缀KMP算法KMPKnuth-Morris-Pratt算法是一种高效的字符串匹配算法核心思想是利用已经部分匹配的信息避免重复比较在文本串中快速查找模式串。KMP算法特别适合处理长文本和重复性高的模式串时间复杂度是O(mn)m是模式串长度n是文本串长度。KMP算法的关键在于构建一个部分匹配表也叫失败函数或者next数组这个表记录了当匹配失败时模式串指针应该回退到的位置让算法跳过已知不可能匹配的位置提高匹配效率。算法步骤KMP算法主要分为两个阶段预处理阶段计算模式串的部分匹配表next数组构建一个数组记录每个位置的最长相等前后缀长度该数组用于在匹配失败时确定模式串指针的回退位置匹配阶段使用部分匹配表在文本串中查找模式串从左到右同时遍历文本串和模式串当字符不匹配时根据next数组回退模式串指针当模式串完全匹配时记录匹配位置并继续查找其他匹配核心特性线性时间复杂度O(mn)其中m是模式串长度n是文本串长度高效利用历史信息通过预处理避免了重复比较只需一次遍历文本串文本串指针不会回退空间复杂度O(m)仅需存储模式串的部分匹配表适用场景特别适合长文本和具有重复性的模式串基础实现暴力解法javapublic class NaiveStringMatcher {