KMP算法
target(查找子串) source(待匹配子串)
KMP算法是在原有的暴力逐个比较算法上进行的改进,即从target的第一个字符与source的第一个字符进行比较,若两个字符相等则逐个向后比较,当出现不相等字符时,target回到第一个字符,source向后移动一个字符重复之前的步骤。KMP算法主要思想是当出现比较不相等的情况时,对target的回溯位置(新的进行比较的字符)进行优化,以减少不必要的比较。
例如:
source = abcabcabe
target = abcabe
前五个字符经过比较相等后到第六个字符比较时发现不相等,在暴力逐个比较的算法下target会回到第一个字符"a"与source的第二个字符"b"进行比较,但这时发现target中的"bc"在之前的比较已经知道与source中对应位置相等,而"bc"已知与target的第一个"a"不相等,则没有必要再用"a"与source中第二和第三的位置的b和c进行比较。而target后面第四个和第五个位置上的a和b与第一和第二个位置上的a和b相同,在之前的比较已经知道第四第五个位置与source对应位置相同,则新的比较中不需要再用target第一和第二个位置的a和b与source中的第四和第五个位置的a和b进行比较。由此target的回溯位置即为第三个字符c,与source中的第六个字符(最初比较时发现不相同的字符)进行比较。
在具体的实现中用一个next数组去存储target中每个元素的回溯位置,这里引入一个概念最长相等前后缀,用公式很难解释清楚,这里用例子来说明。比如:abcab 最长相等前后缀为ab abcabe 这里则不存在最长相等前后缀 因为前缀是从字符串的第一个字符开始 后缀是以字符串的最后一个字符结束 ababa 这里的最长相等前后缀为aba 前缀和后缀可以出现交叉,而next数组即target每个元素的回溯位置为当前元素前面最长相等前后缀中前缀的下一个元素,因为后缀已经与source中对应位置比较过并发现相等,则不需要对前缀再次进行重复比较。
next数组在前面说过的基础上可以进一步优化。举个例子:source = abacabaefg target = abacabac 按照之前所说,当target中的最后一个c与source中的e比较发现不相等时,应该回溯到第四个位置的c,而已经知道第四个位置的c与最后一个位置的c相等,则相当于已经知道回溯的位置进行的新的比较是不相等的,则没有必要进行这次比较,可以回溯到第四个位置的c应该回溯的位置。总结来说就是当出现回溯位置元素与当前元素相等,回溯位置应为第一次回溯位置元素再往前回溯的位置。
例题LeetCode 28.实现 strStr()
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1:
输入: haystack = “hello”, needle = “ll”
输出: 2
示例 2:
输入: haystack = “aaaaa”, needle = “bba”
输出: -1
下面是java实现的代码
class Solution {
public int strStr(String haystack, String needle) {
if(haystack == null || needle == null)
return -1;
int m = haystack.length();
int n = needle.length();
if(n == 0)
return 0;
int[] nextval = new int[n];
nextval[0] = -1;
int k = -1, i = 0;
//初始化nextval数组(改进后的next数组)
while(i < n - 1){
//k == -1这个条件起到了回溯作用 与后面的k = nextval[k]对应
//例如:abc 0位置元素与1位置元素不相等 此时k = 0, i = 1
//进入k = nextval[k]语句后 k = -1 再次循环回到if语句后
//k++, i++ 即可以向后移动一个元素与第一个元素进行比较
if(k == -1 || needle.charAt(k) == needle.charAt(i)){
i++;
k++;
if(needle.charAt(k) != needle.charAt(i))
nextval[i] = k; //出现连续判断相等时 k相当于记录了前面判断相等的次数 当出现不比较不相等时 用k确定回溯位置
//未出现相等时(当前元素前无最长相等前后缀) k为0 即回溯到第一个元素
else nextval[i] = nextval[k]; //若回溯的元素与当前元素相等 则再进行回溯
//即回溯位置为回溯元素回溯的位置
}
else k = nextval[k];
}
int j = 0, l = 0;
//主循环
//j遍历待查找字符串 l遍历搜索字符串
while(j < m && l < n){
//l == -1时 说明搜索字符串的第一个元素与待查找字符串的某一个元素不相等 需要将待查找字符串向后移动一个元素
//若比较相等 则不断向后比较直到l遍历了所有元素
if(l == -1 || haystack.charAt(j) == needle.charAt(l)){
j++;
l++;
}
//比较不相等 搜索字符串的元素向前回溯
else l = nextval[l];
}
//l == n说明有一个连续字符串与搜索字符串相同 起始位置为j - n
if(l == n)
return j - n;
else return -1;
}
}
Boyer-Moore算法
target(查找子串) source(待匹配子串)
Boyer-Moore算法与KMP算法的思想有一些相似指出。KMP是通过当出现比较不相同的情况时修改回溯的位置来减少比较次数,而Boyer-Moore是从右向左比较target中的元素与对应位置是否匹配,当不匹配时正常情况下只向后移动一个元素重新比较,但在这里会根据不同情况修改移动位置来减少比较次数。有些不同的是KMP修改的是target中的指针,而Boyer-Moore修改的是source中的指针。
例如:source = f i n d i n a h a y s t a c k n e e d l e
target = n e e d l e
第一次进行比较的是target中的第六个元素"e"和source中的第六个元素"n",此时若不匹配会有三种情况。
1.source中的字符没有出现在target中,也就是说此时无论这个字符对应target中的哪个位置,都不可能匹配,也就更不会出现整体上的字符串与target匹配,此时将target整体跳过这个字符。
2.source中的字符出现在target中,也就是说此时这个字符与target中的相同字符处于对应位置,可能会出现整体匹配情况。这里用一个right数组去记录target中元素在target中最右面出现的位置。例如right[e(这里实际应该是字符对应的ANSII码)] = 5,right[n] = 0。假设i是在source中移动的指针,i指向的元素对应target中的第一个元素,假设j是target的长度减1,则i+j是每次比较开始的位置(当比较相同时,j–,不断向前比较),j-right则是i需要向后移动的距离,这里也包含了情况1,因为若target中不存在source中的不匹配元素,默认right值为-1。这样保证了source中的不匹配元素在新一轮的比较中会与target对应位置元素相同(这里可能会出现j-right小于0的情况 也就是情况3)。
3.当j<right时,也就是source中的不匹配元素在target当前比较位置的右面出现,此时i向左移动没有意义,因为在前面两种情况下前面已经不存在出现整体匹配的可能性。所以此时将i向后移动一个位置,因为在target对应位置的左面可能有与source中不匹配元素相同的元素,无法排除整体匹配的可能性。
例题LeetCode 28.实现 strStr()
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1:
输入: haystack = “hello”, needle = “ll”
输出: 2
示例 2:
输入: haystack = “aaaaa”, needle = “bba”
输出: -1
下面是java实现的代码
class Solution {
public int strStr(String haystack, String needle) {
if(haystack == null || needle == null)
return -1;
int m = haystack.length();
int n = needle.length();
if(n == 0)
return 0;
int[] right = new int[256];
//初始化right数组 -1代表在needle中不存在
for(int i = 0; i < 256; i++)
right[i] = -1;
//记录needle中元素在needle中最右面出现的位置 这个元素在right数组中的下标就是它的ANSII码
for(int i = 0; i < n; i++)
right[needle.charAt(i)] = i;
//skip为i向后移动的距离
int skip = 0;
for(int i = 0; i <= m - n; i += skip){
skip = 0;
for(int j = n - 1; j >= 0; j--){
if(haystack.charAt(i+j) != needle.charAt(j)){
skip = j - right[haystack.charAt(i+j)];
if(skip < 0)
skip = 1;
break;
}
}
if(skip == 0)
return i;
}
return -1;
}
}
Rabin-Karp算法
target(查找子串) source(待匹配子串)
Rabin-Karp算法较KMP算法更加容易理解,其主要思想是通过构造一个hash函数去计算target的hash值去与source中长度相等的子串的hash值进行比较。由于通过hash值的计算而不需要去用target中的每一个字符一个一个与source中的字符进行比较,时间复杂度从O(m*n)到O(m+n)。需要注意的是由于两个不同的字符串hash值的计算存在小概率的可能会相等,在hash值相等的情况下需要对两段子串进行equals比较。
例如:
source = abcdefg
target = cde
首先计算target的hash值,然后对source进行遍历,先求出target长度(“abc”)的hash值,进行比较发现不相等后将d加入hash值的计算并从hash值减去子串第一个子串保证与target长度相等。直到向后循环到"def"发现hash值相等,进而对两段子串进行equals的比较(在循环中不相同的子串可能出现hash值计算相等的情况)。
例题LeetCode 28.实现 strStr()
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1:
输入: haystack = “hello”, needle = “ll”
输出: 2
示例 2:
输入: haystack = “aaaaa”, needle = “bba”
输出: -1
下面是java实现的代码
class Solution {
public int strStr(String haystack, String needle) {
int BASE = 100000; //防止后面hash的计算超过int范围 用hash值对BASE进行"%"运算
if(haystack == null || needle == null)
return -1;
int m = haystack.length();
int n = needle.length();
if(n == 0)
return 0;
int targetcode = 0;
//计算target的hash值 第0位乘31的n-1次幂 一直到第n-1位乘31的0次幂
for(int i = 0; i < n; i++){
targetcode = (targetcode * 31 + needle.charAt(i)) % BASE;
}
int power = 1;
//计算一个值 用来后面从hash值中删除最前面的那个字符维护长度与target相等
for(int i = 0; i < n; i++)
power = (power * 31) % BASE;
int hashcode = 0;
//主循环 当hash值计算的子串长度未达到target的长度时continue
// 当hash值计算的子串加入了新的子串长度大于target 删除第一个字符
// 当hash值相等时 用equals比较两个字符串具体字符是否相同
for(int i = 0; i < m; i++){
hashcode = (hashcode * 31 + haystack.charAt(i)) % BASE;
if(i < n - 1)
continue;
if(i >= n){
hashcode -= (haystack.charAt(i - n) * power) % BASE;
if(hashcode < 0)
hashcode += BASE;
}
if(hashcode == targetcode){
if(haystack.substring(i - n + 1, i + 1).equals(needle))
return i - n + 1;
}
}
return -1;
}
}