先来规定一些符号,S表示一个字符串,|S|表示字符串的长度,没有特殊说明n表示匹配串的长度,m表示模式串的长度。prefix(i)表示S的后缀S[i...n]。
zbox算法只是对一个字符串S求出一个数组Zbox[]。Zbox[i]表示的是prefix[i]和字符串S的最长公共前缀的长度。即zbox算法的目的是初始化所有字符串后缀对与原串的最长公共前缀长度。
先来看看通过Zbox[]数组如何进行精确匹配。匹配串记为S,模式串记为P。要用一个数组把S和P关联起来,一个常用的方法是把S和P连接。这里把P放在前面得到新串T = P+S。现在对新串进行zbox的过程求出Zbox[]。根据定义可知S串如果从i处匹配上了P串,则Zbox[i + m] = m。于是可以遍历T串的[m, n + m - 1],寻找匹配结果。
zbox的快速求Zbox[]的方法是在暴力的基础上对于可以确认的以前匹配过的字符直接跳过。算法的关键在于在比较中不断更新区间[l,r](zBox[l] = r - l + 1,这是通过计算prefix(l)更新的),使以后要计算的zBox[i]尽可能在[l, r]内。
现在考虑怎么通过[l, r]降低复杂度:
- T[l, r] = T[0, r - l + 1];
- i在[l, r]间;
- 由1,2:T[i, r] = t[i - l. r - l + 1];
- 由3:
- zBox[i] = zBox[i - l], 当i+zBox[i - l] < r, 因为i+zBox[i - l] + 1不会再匹配了
- zBox[i] = r - l + 1 + 暴力向后比较得到的值, 当i+zBox[i - l] >= r, 因为只能保证T[i, r] = t[i - l. r - l + 1],后面只能暴力计算不能根据prefix(i - l)(都不相等了)
上代码~
1 unsigned int * zbox(char * str) 2 { 3 int idx = 0; 4 int len = strlen(str); 5 unsigned int *zBox = (unsigned int *)malloc(len * sizeof(unsigned int)); 6 int l = 0, r = 0; 7 8 if(zBox == NULL) return NULL; 9 10 zBox[0] = len; 11 for(idx = 1; idx < len; ++idx) 12 { //这里的l必然小于idx,因为l总是等于之前的idx 13 if(idx > r)//现在正在计算的后缀不在已匹配字符段内,必须从头开始比较 14 { 15 l = r = idx; 16 while(r < len && str[r - l] == str[r]) ++r; 17 zBox[idx] = r - l, --r; 18 } 19 else //现在正在计算的后缀在已匹配字符段内,可以跳过某些字符 20 { 21 //这里还分两种情况,超界和未超界 22 int k = idx - l; 23 if(zBox[k] < r - idx + 1) zBox[idx] = zBox[k]; //未超界,当前最大ZBOX值求出,直接赋值 24 else 25 { 26 //超界,需要跳过一些字符继续比较。 27 l = idx; 28 while(r < len && str[r - l] == str[r]) ++r; 29 zBox[idx] = r - l, --r; 30 } 31 } 32 } 33 return zBox; 34 }
从这个代码可以看出r是递增不减的且r最大值是n - 1。所以均摊到外层每个循环里,while循环平均进行了1次,因此zbox复杂度为O(n)。