字符串匹配(KMP算法)
KMP算法,全称Knuth-Morris-Pratt算法,是一种用于字符串匹配的算法。下面是对KMP算法的详细解析:
一、KMP算法概述
- 基本概念:
- KMP算法是由Donald Knuth、Vaughan Pratt和James Morris共同发明的,因此算法名称取自这三位发明人名字的首字母组合。
- 该算法主要用于解决字符串(主串)中的模式串(子串)定位问题,如“求子串出现的起始位置”、“求子串的出现次数”等。
- 核心思想:
- KMP算法对朴素的字符串匹配算法进行了改进,它利用匹配失败时已知的部分匹配信息,保持主串的指针不回溯,通过修改模式串的指针,使模式串尽量地移动到有效的匹配位置。
- 在匹配失败时,不再按照朴素匹配算法的规则重新回溯主串指针和子串指针,而是保持主串指针不动,尽可能地移动子串指针到有效匹配位置。
- 关系推导:
- 假设主串为S,模式串为T。当主串的第i个字符与模式串的第j个字符失配后,主串的第i个字符将与模式串的第k个字符(k<j)继续比较。
- 此时,需要找到一个位置k,使得模式串的前k-1个字符与主串中失配位置之前的子串相等。这个位置k可以通过部分匹配表(Next数组)来确定。
二、Next数组概述
- 含义:
- Next数组是KMP算法的核心,它记录了模式串中每个位置之前的子串的最大相同前后缀长度。
- 在匹配过程中,当模式串的某个字符与主串的字符不匹配时,可以根据Next数组直接跳转到模式串的下一个可能匹配位置。
- 如何求Next数组:
- 初始化Next[0]为-1或0(根据具体实现而定,有的实现中Next[0]不参与匹配过程,因此可以设为-1表示无效)。
- 对于模式串的每个位置j(从1开始),如果模式串的第j+1个字符与前缀中的某个字符相等(即S[j+1] == S[Next[j]+1]),则Next[j+1] = Next[j] + 1。
- 否则,需要回溯Next数组,找到一个新的位置k(k = Next[Next[j]-1]),使得S[1...k] = S[j-k+1...j-1],并判断S[j+1]是否等于S[k+1]。如果相等,则Next[j+1] = k + 1;否则,继续回溯直到找到匹配或Next[k]为0为止。
function KMP_SEARCH(text, pattern): next = COMPUTE_NEXT(pattern) n = length(text) m = length(pattern) i = 0 // text的指针 j = 0 // pattern的指针 while i < n: if j == -1 or text[i] == pattern[j]: i += 1 j += 1 if j == m: return i - j // 匹配成功,返回匹配的起始位置 else: j = next[j-1] // 移动模式串指针 return -1 // 未找到匹配 function COMPUTE_NEXT(pattern): m = length(pattern) next = array[m] next[0] = -1 // 或0,根据具体实现而定 len = 0 i = 1 while i < m: if pattern[i] == pattern[len]: len += 1 next[i] = len i += 1 else: if len > 0: len = next[len-1] else: next[i] = 0 i += 1 return next
三、KMP算法的应用场景
KMP算法广泛应用于各种需要快速、高效字符串匹配的场景中,如:
- 字符串搜索:在大规模文本数据中快速定位特定字符串。
- 字符串编辑:处理字符串中的替换、插入和删除操作。
- 自动补全:实现搜索引擎的自动完成功能。
- 基因序列匹配:在生物信息学领域中匹配DNA或RNA序列。
- 代码编辑器:实现代码编辑器中的代码提示功能等。
题目描述:
在一个字符串中查找另一个字符串的出现位置,可以使用朴素的字符串匹配算法,但KMP(Knuth-Morris-Pratt)算法更高效。
解题思路:
- KMP算法通过预处理模式串(要查找的字符串)来构建一个部分匹配表(也叫做“最长公共前后缀数组”),该表记录了每个位置的字符串前缀的最长公共前后缀长度。
- 在匹配过程中,当发现不匹配时,可以根据部分匹配表直接跳转到下一个可能匹配的位置,而不需要像朴素算法那样每次只移动一个字符的位置。
代码示例(简化版,不包含部分匹配表的构建过程):
// 注意:这里只是展示了KMP算法的思路,实际使用时需要完整实现部分匹配表的构建
function kmpSearch(text, pattern) {
// 假设已经有了部分匹配表 next[]
let next = []; // 这里应该是通过预处理pattern得到的部分匹配表
let i = 0; // text的索引
let j = 0; // pattern的索引
while (i < text.length) {
if (text[i] === pattern[j]) {
i++;
j++;
}
if (j === pattern.length) {
// 找到匹配,返回匹配起始位置
return i - j;
} else if (i < text.length && text[i] !== pattern[j]) {
// 不匹配,根据部分匹配表移动j
if (j !== 0) {
j = next[j - 1];
} else {
i++;
}
}
}
// 未找到匹配,返回-1
return -1;
}
// 注意:实际使用时,需要补充next数组的构建逻辑
反转字符串
题目描述:
给定一个字符串,要求输出其反转后的字符串。
解题思路:
- 使用双指针方法,一个指针从字符串的开头开始,另一个指针从字符串的末尾开始。
- 交换两个指针所指向的字符,然后同时向中间移动指针,直到两个指针相遇。
- 这种方法的时间复杂度为O(n),空间复杂度为O(1)。
/**
* @param {character[]} s
* @return {void} Do not return anything, modify s in-place instead.
*/
var reverseString = function(s) {
// s.reverse(); 修改原数组
//双指针交换,一个指向字符串头一个指向字符串尾
let length=s.length;
let mid=length/2;
let right=length-1;
for(let left=0;left<mid;left++){
[s[left],s[right]]=[s[right],s[left]]
right--;
}
};
反转字符串II
/**
* @param {string} s
* @param {number} k
* @return {string}
*/
var reverseStr = function(s, k) {
//字符串=》字符数组
let sArr=s.split('');
let length=s.length;
//每计数2*k for!!!
for(let i=0;i<length;i+=2*k){
let left=i;
//right指向要反转的最后字符们不要mid!
//三元表达式,判断i+k-1右边的指针是否超过字符长度(2k与2k>=k),没超过就是交换前k
//超过标识字符小于k,就全交换剩下的字符
let right=(i+k-1)>=length?length-1:i+k-1;
while(left<right){
[sArr[left],sArr[right]]=[sArr[right],sArr[left]];
left++;
right--;
}
}
// let start=0;
// let end=k-1;
// let mid=k/2;
// let times=0;
// while(true){
// if(length<k){
// start=2*k*times;
// end=length-1;
// let mid=2*k+length/2;
// for(;start<end;start++){
// //交换
// [sArr[start],sArr[end]]=[sArr[end],sArr[start]];
// //移动指针;
// end--;
// }
// break;
// }
// if(length>=k&&length<2*k){
// start=2*k*times;
// //end,设置为2k+k-1,共n个数!
// end=2*k*times+k-1;
// for(;start<end;start++){
// //交换
// [sArr[start],sArr[end]]=[sArr[end],sArr[start]];
// //移动指针;
// end--;
// }
// break;
// }
// for(;start<end;start++){
// //交换
// [sArr[start],sArr[end]]=[sArr[end],sArr[start]];
// //移动指针;
// end--;
// }
// times++;
// length=length-2*k;
// }
//字符数组=》字符串
return sArr.join('');
};
替换数字
题目描述:
给定一个字符串和若干对替换规则,要求按照规则替换字符串中的部分子串。
解题思路:
- 可以使用正则表达式来匹配和替换字符串中的子串。
- 遍历替换规则,对每个规则构建一个正则表达式,并使用
String.prototype.replace()
方法进行替换。 - 注意,替换顺序可能会影响最终结果,因此可能需要按照特定顺序(如从长到短)来处理替换规则。
function replaceNumber(s){
let length=s.length;
let inputArr=s.split("");
let baseCode="a".charCodeAt(0);
for(let i=0;i<length;i++){
let curCode=inputArr[i].charCodeAt(0);
if(!(curCode-baseCode>=0&&curCode-baseCode<26)){
inputArr[i]="number"
}
}
return inputArr.join("");
// //2.正则表达式
// let regex=/[a-z]/g
// const replacedS=s.replace(regex,"number")
// return replacedS;
}
反转单词 (先整体后局部,先反转所有字符,进一步再反转单词)
//用已经存在的api
let wordArr=s.split(" ");
let filtered=wordArr.filter(item=>item!=="");
let length=filtered.length;
let right=length-1;
for(let left=0;left<length;left++){
if(left>=right){
break;
}
[filtered[left],filtered[right]]=[filtered[right],filtered[left]];
//这里是已经在一个循环中了,你还来一个left++,哭死@jing
right--;
}
return filtered.join(" ");
/**
* @param {string} s
* @return {string}
*/
var reverseWords = function(s) {
// //用已经存在的api
// let wordArr=s.split(" ");
// let filtered=wordArr.filter(item=>item!=="");
// let length=filtered.length;
// let right=length-1;
// for(let left=0;left<length;left++){
// if(left>=right){
// break;
// }
// [filtered[left],filtered[right]]=[filtered[right],filtered[left]];
// //这里是已经在一个循环中了,你还来一个left++,哭死@jing
// right--;
// }
// return filtered.join(" ");
//要求不用额外空间,即在原来字符串上操作!!!
let strArr=Array.from(s);//[' ','h','l'...]
// 1.移除多余空格
// 1.1正则表达式 str.replace(/\s+/g, ' ').trim();
//1.2 手动实现 ['h','e'...]
removeSpace(strArr);
//2.将整个字符串反转(eulb si)
reverseManual(strArr,0,strArr.length-1)
//3.将每个单词反转(blue is)!!!!
let start=0;
//等于str.length表示已经遍历完成
for(let i=0;i<=strArr.length;i++){
//每次碰到空格符号或到字符串最后的后一项
if(strArr[i]===" "||i===strArr.length){
reverseManual(strArr,start,i-1);
start=i+1;
}
}
return strArr.join('')
};
function removeSpace(strArr){
//双指针用于去除多余空格[前后多余,中间多个空格->一个空格]
let fast=0;
let slow=0;
for(;fast<strArr.length;fast++){
//前面或中间多个
if(strArr[fast]===" "&&(fast===0||strArr[fast-1]===" "))continue;
else{
strArr[slow++]=strArr[fast];
}
}
//移除 末尾空格,通过修改数组长度
strArr.length=strArr[slow-1]===" "?slow-1:slow;
}
function reverseManual(strArr,start,end){
let left=start;
let right=end;
while(left<right){
[strArr[left],strArr[right]]=[strArr[right],strArr[left]];
right--;
left++;
}
}
右翻转(abcdefg->fgabcde,与上类似,先整体后局部/先局部后整体)
扫描二维码关注公众号,回复:
17409488 查看本文章

// JS中字符串内不可单独修改
const readline = require('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
const inputs = []; // 存储输入
rl.on('line', function(data) {
inputs.push(data);
}).on('close', function() {
const res = deal(inputs);
// 打印结果
console.log(res);
})
// 对传入的数据进行处理
function deal(inputs) {
let [k, s] = inputs;
const len = s.length - 1;
k = parseInt(k);
str = s.split('');
str = reverseStr(str, 0, len - k)
str = reverseStr(str, len - k + 1, len)
str = reverseStr(str, 0, len)
return str.join('');
}
// 根据提供的范围进行翻转
function reverseStr(s, start, end) {
while (start < end) {
[s[start], s[end]] = [s[end], s[start]]
start++;
end--;
}
return s;
}
回文字符串判断
题目描述:
判断一个字符串是否是回文串(即正读和反读都相同的字符串)。
解题思路:
- 可以使用双指针方法,一个从字符串开头开始,一个从字符串末尾开始,比较两个指针所指向的字符是否相同。
- 也可以先将字符串反转,然后比较反转后的字符串和原字符串是否相同。
代码示例(双指针方法):
function isPalindrome(s) {
let left = 0;
let right = s.length - 1;
while (left < right) {
if (s[left] !== s[right]) {
return false;
}
left++;
right--;
}
return true;
}
console.log(isPalindrome("racecar")); // 输出 true