了解了一下KMP 算法,自己尝试实现,也比较简单,具体原理参考google baidu,不再重复,这里只作为一个学习的纪录。
use Data::Dumper;
my $from = 'abababd ababc';
my $find = 'ababc';
# 分隔字符串为array
my @from = split '', $from;
my @find = split '', $find;
print $find, "\n";
print @{ calc( \@find ) }, "\n";
print "\n";
print kmp( \@from, \@find );
############################
# 计算字符串的重复值
sub calc {
my ($array) = @_;
# 取与字串相同长度的array 并将第一个置0
my @tmp = (@$array);
$tmp[0] = 0;
# 之后的每一个字符都与开头字串比较,取@tmp 前一个值作为比较字符
# 若为0,前一字符个与开头不重复,比较字符指向开头0
# 若不为0,前一个字符与开头有重复,如果还相同,@tmp 值加1,否则为0,后继自符重新从头开始比较
for ( 1 .. $#tmp ) {
if ( $array->[$_] eq $array->[ $tmp[ $_ - 1 ] ] ) {
$tmp[$_] = $tmp[ $_ - 1 ] + 1;
}
else {
$tmp[$_] = 0;
}
}
return \@tmp;
}
sub kmp {
my ( $from, $find ) = @_;
# 初始从开头比较
my $i = 0;
my $tmp = calc($find);
# 剩余字串长度小于搜索字串,则退出返回-1
while ( ( $#{$from} - $i ) >= $#{$find} ) {
# 观察每次步进长度
print @$from[ $i .. $#{$from} ], "\n";
# j 标记搜索到的字串长度
my $j = 0;
while ( $find->[$j] eq $from->[ $i + $j ] ) {
# 搜索到的长度与字符串长度相同,说明找到,返回index位置
if ( $j == $#{$find} ) {
return $i;
}
else {
$j++;
}
}
# 有相同的字符串,根据计算的值来跳转多个字符并重新比较
# $j 为相同的个数(以1为基数),@tmp[$j-1] 为同开始字符重复的个数
if ($j) {
$i += $j - $tmp->[ $j - 1 ];
}
else {
$i++;
}
}
return -1;
}
输出:#字符串以及计算重复值结果
ababc
00120
#每次查找过程
abababd ababc
ababd ababc
abd ababc
d ababc
ababc
ababc
8
优化:
由于KMP 搜索过程中已经判断了子字符串的重复性,下一次判断过程中可以根据计算的重复值跳过部分开头字符,直接从不重复的地方开始比较。如下为改进的算法:
sub kmp {
my ( $from, $find ) = @_;
# 初始从开头比较
my $i = 0;
my $tmp = calc($find);
my $j = 0;
# 剩余字串长度小于搜索字串,则退出返回-1
while ( ( $#{$from} - $i ) >= $#{$find} ) {
# 观察每次步进长度
print "$i $j => ", @$from[ $i .. $#{$from} ], "\n";
# j 标记搜索到的字串长度
while ( $j <= $#{$find} && $find->[$j] eq $from->[ $i + $j ] ) {
$j++;
print ".\n"; # 记录由于$j 的变化自符比较的减少过程,用于debug
}
# 有相同的字符串,根据计算的值来跳转多个字符并重新比较
# $j 为相同的个数(以1为基数),@tmp[$j-1] 为同开始字符重复的个数
if ( !$j ) {
$i += 1;
}
elsif ( $j <= $#{$find} ) {
$i += $j - $tmp->[ $j - 1 ];
$j = $tmp->[ $j - 1 ];
}
else {
return $i;
}
}
return -1;
}