检测字符串或token串中公共子串的方法探究
笔者研究生在读,从事代码大数据方面的研究。这是笔者的第一篇博客,主要讲的是在研究代码克隆检测时灵光一闪想到的一种检测字符串或token串中公共子串的方法,我把它叫做“后缀矩阵”法。熟悉这一块的朋友可能知道后缀树或后缀数组算法,也是用来检测公共子串的。没错,笔者的灵感亦是来自后缀数组。下面来具体讲一下后缀数组和“后缀矩阵”法的算法流程,以及两种算法的比较。
可能读者还不太了解后缀数组算法,我这里不做详细介绍,这里只通过举例子来说明,感兴趣的可能参考其它文章。下面我们就以检测字符串中的公共子串为例来介绍。
后缀数组检测公共子串的流程图如下:
假设输入字符串为“abcdab”,那么该字符串所有的后缀就是(abcdab,bcdab,cdab,dab,ab,b),对应的后缀序号为(0,1,2,3,4,5),后缀序号即为从字符串中切分出后缀的切分点。(0,1,2,3,4,5)即为根据字符串构建的后缀数组。
接下来要为后缀数组排序,排序的规则是这样的:从后缀的第一个字符开始对位比较,如果两个后缀其中之一首先出现较小的字符(ASCII码靠前),则先出现较小字符的后缀排在另一个前面;如果字符比完了还没有出现较小的字符,则较短的排在较长的前面。例如给abcdab和bcdab排序,很明显abcdab排在前面,因为它先出现了较小的字符a。再例如给abcdab和ab排序,则ab排在前面。排完序后后缀数组变成了(4,0,5,1,2,3),该后缀数组的索引代表排名,值代表后缀序号。
然后是计算高度数组,计算高度数组的规则是:在排好序的后缀数组基础上,计算当前后缀和比它排名前一位的后缀的字符重叠长度。因为后缀序号为4的后缀拍第一位,因为其高度为0。在计算后缀序号为0的高度时,将其与后缀序号为4的后缀进行比较,也就是ab和abcdab,高度为2,后面依次类推。最后得到高度数组为(0,2,0,1,0,0)。
最后输出一个三元组的数组[(4,0,2),(0,5,0)...],比如(4,0,2)代表后缀序号为4的后缀与序号为0的后缀其重叠长度为2。
接下来是笔者想到的另一种方法——“后缀矩阵”法。
说是后缀矩阵法,其实并没有实际用到矩阵,只是借用了矩阵的形式,该方法流程如下:
还是以字符串abcdab为例。根据字符串构建的矩阵如下:
a | b | c | d | a | b | |
a | ||||||
b | ||||||
c | ||||||
d | ||||||
a | ||||||
b |
然后填充上三角矩阵,如下:
a | b | c | d | a | b | |
a | 1 | 0 | 0 | 0 | 1 | 0 |
b | 1 | 0 | 0 | 0 | 1 | |
c | 1 | 0 | 0 | 0 | ||
d | 1 | 0 | 0 | |||
a | 1 | 0 | ||||
b | 1 |
然后在在上三角矩阵中寻找对角线元素连续为1的部分,如上表我们可以很容易发现两个连续为1的部分如图:
由上图我们可以发现:对角线上的元素一定为1,这是毋庸置疑的,所以没有意义,因为我们要找的是公共子串。还有一个就是ab,因为第一排中第二个a的位置为4(以0为其实起始),第一列中a的位置为0,连续为1的长度为2,因此我们可以得到一个三元组(4,0,2),当然我们也很容易得到(5,1,1)。这样我们就得到了公共子串。
当字符串很长或者token串很长的时候,比如做大规模代码克隆检测,你会发现第二种方法构建矩阵是不现实的,因为那会占用非常大的内存。笔者这里只是为了方便读者理解,在实际用代码实现时是可以不构建矩阵的,只使用token串本身所占的内存即可,具体方法就要各位开动脑经思考啦。
下面笔者来比较一下这两种算法。
首先是后缀数组算法,如果输入的token串长度为n,那么该算法至少需要一个长度为n的后缀数组和一个长度为 n的高度数组。该算法最耗时的地方在于后缀数组的排序过程,该过程决定了后缀数组算法的时间复杂度。
然后是后缀矩阵算法,如果输入的token串长度为n,在不构建矩阵的情况下,该算法只需要保存查找到的公共子串。查找的方式就是在“上三角矩阵”中沿对角线查找连续为1的部分,所以相当于n-1次循环,每次循环的长度从1变为n-1。因为每次循环是相互独立的,因此很容易多线程实现。