字符串匹配基础(中):如何实现文本编辑器中的查找功能

字符串匹配基础(中):如何实现文本编辑器中的查找功能

就是word中把一个单词统一替换成另一个,可以用BM算法

BM算法的核心思想

模式串和主串匹配过程中,因为主串中的c在模式串中是没有的,所以模式从向后滑动的时候,只要c与模式串有重合,肯定无法匹配,可以一次性把模式串往后滑动几位,把模式串移动到c的后面

BM算法原理分析

BM算法包含两部分,是坏字符规则和好后缀规则

1.坏字符规则

按照模式串的下标从大到小的顺序,倒序匹配

从模式串的末尾往前倒着匹配,当我们发现某个字符串没法匹配的时候,把这个没有匹配的字符叫做坏字符(主串中的字符)我们把坏字符c在模式串中查找,发现模式串中并没有这个字符,这时候我们可以将模式串直接往后滑动三位(因为模式串只有3位),将模式串滑动到c后面的位置,再从模式串的末尾字符开始比较

这时候坏字符是a,不能往后滑动三位了,将模式串往后滑动2位,让两个a上下对齐,然后再从模式串的末尾字符开始,重新匹配

发生不匹配的时候,把坏字符对应的模式串中的字符下标记作si,如果坏字符在模式串中存在,把这个坏字符在模式串种的下标记作xi,如果不存在,xi = -1,那么模式串往后移动的位数就是si - xi (这里的下标是字符在模式串的下标)

a b c a c a b d c

​ a b d

​ 此时a就是坏字符,d就是坏字符对应的模式串中的字符下标,d = si = 2,a = xi = 0

如果坏字符在模式串里多处出现,计算xi的时候选择最靠后的那一个

单纯使用坏字符规则还不够,还需要用到“好后缀规则”

2.好后缀规则

当模式串滑动到下面位置的时候,模式串和主串有两个字符是匹配的,倒数第三个是坏字符

a b c a c a b c b c b a c a b c

​ a b b c a b c

​ a是坏字符,b c是好后缀

我们可以利用坏字符规则来计算模式串的滑动位数,也可以使用好后缀处理规则

好后缀是把已经匹配的bc叫做好后缀,记作{u},拿bc在模式串中寻找,如果找到了另一个跟{u}相匹配的的子串{u*},将模式串滑动的那个到子串{u*}与主串中{u}对齐的位置,如果找不到另一个等于{u}的子串,就直接将模式串,滑动到主串中{u}的后面,因为之前的任何一次往后滑动,都没有匹配到主串中的{u}的情况,但是这样有可能过度滑动

所以我们不仅要看好后缀在模式串中,是否有另一个匹配的子串,我们还要考察好后缀子串,是否存在跟模式串的前缀子串匹配的

所谓某个字符串s的后缀子串,就是最后一个字符跟s对齐的子串,比如abc的后缀子串就包括c,bc。所谓前缀子串,就是起始字符跟s对齐的子串,比如abc的前缀子串有a,ab。我们从好后缀的后缀子串中,找一个最长的并且能跟模式串的前缀子串匹配的,假设是{v}

如何选择用好后缀规则还是坏字符规则?

分别计算好后缀和坏字符往后滑动的位数,然后取两个数中最大的,作为模式串往后滑动的位数

BM算法代码实现

如何查找坏字符在模式串中出现的位置?

可以将模式串中的每个字符及其下标都存到散列表中,就可以快速找到坏字符在模式串的位置下标了,关于散列表,假设字符串的字符集不是很大,每个字符长度是1字节,用大小为256的数组来记录每个字符在模式串中 出现的位置,数组下标对应字符的ASCII码值,数组中存储这个字符在模式串中出现的位置

模式串 : a b d a

​ 0 1 2 3 bc: -1 -1 …… 3 1 -1 2 …… -1

​ 0 1 …… 97 98 99 100 …… 255

ASCII码表: a 97

​ b 98

​ c 99

​ d 100 ……

b是模式串 ,m是模式串的长度,bc是散列表

private static final int SIZE = 256;//全局变量或成员变量
private void generateBC(char[] b ,int  m ,int[] bc){
	for(int i = 0 ; i < SIZE ; ++i){
		bc[i] = -1;   //初始化bc
	}
	for(int i = 0 ; i < m ; ++i){
		int ascii  = (int)b[i];//计算b[i]的ASCII值
		bc[ascii] = i ;
	}
}

先仅用坏字符原则,不考虑si-xi计算得到的移动位数可能会出现负数的情况

public int bm(char[] a , int n ,char[] b ,int m){
	int[] bc = new int [SIZE];  //记录模式串中每个字符最后出现的位置
	generateBC(b,m,bc);  //构建坏字符哈希表
	int i  =  0;  //i表示主串与模式串对齐的第一个字符
	while( i <= n - m){
		int j ;
		for (j = m - 1 ; j  >= 0 ; --j){   //模式串从后往前匹配
			if(a[i+j] != b[j]) break;   //坏字符对应模式串中的下标是j
		}
		if(j < 0){
			return i;   //匹配成功,返回主串与模式串第一个匹配的字符的位置
		}
		//这里等同于将模式串往后滑动  j - bc[(int)a[i+j]]位
		i = i + (j - bc[(int)a[i+j]]);
	}
	return -1;
}

好后缀也是模式串本身的后缀子串,所以,在模式串和主串正式匹配之前,通过预处理模式串,对应的另一个可匹配子串的位置

如何表示模式串中不同的后缀子串呢?因为后缀子串的最后一个字符的位置是固定的,下标为m-1,只需要记录长度即可,通过长度,我们可以确定一个唯一的后缀子串,比如

模式串: c a b c a b

后缀子串 长度

​ b 1

​ a b 2

​ c a b 3

b c a b 4

a b c a b 5

引入最关键的变量suffix数组,suffix数组的下标k,表示后缀子串的长度,下标对应的数组值存储的是在模式串中跟好后缀{u}相匹配的子串{u*}的起始下标值

模式串: c a b c a b

​ 0 1 2 3 4 5

后缀子串 长度 suffix

​ b 1 suffix[1] =2

​ a b 2 suffix[2] = 1

​ c a b 3 suffix[3] = 0

b c a b 4 suffix[4] = -1

a b c a b 5 suffix[5] = -1

如果模式串中有多个字符跟后缀子串{u}相匹配,那么suffix数组中该存储哪一个子串的起始位置呢?存储模式串中最靠后的那个字符的起始位置,也就是下标最大的那个子串的起始位置

不仅在模式串中,查找跟好后缀相匹配的另一个子串,还要在好后缀的后缀子串中查找最长的能跟模式串前缀子串相匹配的后缀子串,需要一个boolean类型的prefix数组来记录模式串的后缀子串是否能匹配模式串的前缀子串

模式串: c a b c a b

​ 0 1 2 3 4 5

​ 后缀子串 长度 suffix prefix

​ b 1 suffix = 2 prefix = false

​ a b 2 suffix = 1 prefix = false

​ c a b 3 suffix = 0 prefix = true

​ b c a b 4 suffix = -1 prefix = false

​ a b c a b 5 suffix = -1 prefix = false

将下标从0到i 的子串(i是0到m-1)与整个模式串,求公共后缀子串,如果公共后缀子串的长度是k,那么suffix[k] = j(j 就是公共后缀子串的起始下标)如果j = 0,就是说公共后缀子串也是模式串的前缀子串,把prefix[k] = true

//b表示模式串,m表示长度,suffix,prefix数组事先申请好了
private void generateGS(char[] b ,int m,int[] suffix ,boolean[] prefix){
	for(int i = 0 ; i < m ; ++i){   //初始化
		suffix[i] = -1;
		prefix[i] = false;
	}
	for(int i = 0 ; i < m -1 ;++i){   //b[0,i]
		int j = i ; 
		int k = 0;//公共后缀子串长度
		while(j >= 0 && b [j] == b[m-1-k]){   //与b[ o ,m-1]求公共后缀子串
			--j;
			++k;
			suffix[k] = j+1;   //j+1表示公共后缀子串在b[0,i]中的起始下标
		}
		if( j == -1) prefix[k] = true ; //如果公共后缀子串也是模式串的前缀子串
	}
}

在模式串跟主串匹配的过程中,遇到不能匹配的字符时,如何根据好后缀规则,计算模式串往后滑动的位数?

假设好后缀的长度是k,先拿好后缀在suffix数组中查找其匹配的子串,如果suffix[k] != -1(-1表示不存在匹配的子串),那我们将模式串往后移动 j-suffix[k]+1位(j表示坏字符对应的模式串中的字符下标),如果suffix[k]=-1,表示模式串中不存在另一个跟好后缀匹配的子串片段。用下面这条规则处理:

好后缀的后缀子串b[r,m-1](其中,r取值从j+2到m-1)的长度k = m -r,如果prefix[k] = true,表示长度为k的后缀子串,有可匹配的前缀子串,这样可以把模式串后移r位

如果两条规则都没有找到可以匹配好后缀及其后缀子串的子串,将整个模式串后移m位

//a,b表示主串和模式串;n,m表示主串和模式串的长度
public int bm(char[] a ,int n , char[] b ,int m ){
	int[] bc = new int[SIZE] ; //记录模式串中每个字符最后出现的位置
	generateBC(b,m,bc);     //构建坏字符哈希表
	int[] suffix = new int [m];
	boolean[] prefix = new boolean[m];
	int i = 0;  // j 表示主串与模式串匹配的第一个字符
	while( i <= n - m){
		int j;
		for(j = m - 1;j >= 0 ; --j){   //模式串从后往前匹配
			if(a[i+j] != b[j])  break;   // 坏字符对应模式串中的下标是j
		}
		if(j < 0){
			return i ;   // 匹配成功,返回主串与模式串第一个匹配的字符的位置
		}
		int x = j - bc[(int) a[i+j]];
		int y = 0;
		if(j < m-1){      //如果有好后缀的话
			y = moveByGS(j ,m,suffix,prefix);
		}
		i = i +Math.max(x,y);
	}
	return -1;
}

//j表示坏字符对应的模式串中的字符下标;m表示模式串长度
private int moveByGS(int j ,int m ,int[] suffix,boolean[] prefix){
	int k = m - 1 - j;//好后缀长度
	if(suffix[k] != -1) return j - suffix[k] +1;
	for(int r = j +2 ; r <= m -1;++r){
		if(prefix[m-r] == true){
			return r;
		}
	}
	return m ;
}

BM算法的思想是利用模式串本身的特点,在模式串中某个字符与主串不能匹配的时候,将模式串往后多滑动几位,以此来减少不必要的比较

发布了75 篇原创文章 · 获赞 9 · 访问量 9187

猜你喜欢

转载自blog.csdn.net/ywangjiyl/article/details/104480982