关于java中字符串匹配的思考

最近自己一直在看字符串匹配的算法,结合自己擅长的java语言,聊聊java中字符串匹配的一些感想。

一、char类型的大小

字符串匹配,其实无非就是对文本和模式中逐个字符进行比较。在java中,char类型的变量是占两个字节的。也就是说,字符串匹配时是每次两个字节去比较。而在C语言中,char类型是占一个字节的,所以匹配时只能一个字节一个字节地去匹配。所以对于同样的文本和模式,java中匹配的次数会比c语言少一倍。

这么分析貌似觉得java的字符串匹配会比较快,其实不然。

现在流行的字符串匹配算法,除了蛮力法外,其它算法都需要额外的存储空间(典型的空间换时间)。而额外的空间大小,往往又是跟字符集相关的。例如sunday算法(其它算法也类似),对模式进行预处理时,把模式中所有字符的位置记录下来,具体实现一般是采用哈希方式,即把模式串中每个char字符映射到一个数组中,对应数组元素的值就是该字符在模式串的下标。哈希数组的大小跟char类型大小相关。如果char是一个字节,数组大小为256;如果char是两个字节,数组大小为65536!65536个元素的int数组在java中占用256kb内存!

虽然可以通过设计哈希函数,使用小一点的数组来减小空间。但是这样必然会有冲突,怎么解决冲突也是一个难题。况且有了冲突之后,算法的性能还能不能达到预期效果也是一个问题。估计美国人研究这些算法时都是只用ASCII,最多也就是256个字符,额外空间还不算大,但是想要把算法扩展到unicode就成了一个挑战(除非不在乎额外空间)。

二、神奇的String.indexOf(String)

使用java的人估计都会用过这个方法。不知道有没有人会去认真查看这个方法的源码。其实源码也很简单,是使用蛮力法进行匹配的。

    /**
     * Code shared by String and StringBuffer to do searches. The
     * source is the character array being searched, and the target
     * is the string being searched for.
     *
     * @param   source       the characters being searched.
     * @param   sourceOffset offset of the source string.
     * @param   sourceCount  count of the source string.
     * @param   target       the characters being searched for.
     * @param   targetOffset offset of the target string.
     * @param   targetCount  count of the target string.
     * @param   fromIndex    the index to begin searching from.
     */
    static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) {
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (targetCount == 0) {
            return fromIndex;
        }

        char first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);

        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            /* Look for first character. */
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }

            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);

                if (j == end) {
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }

这个算法的时间复杂度很明显是O(m*n),n和m分别为文本和模式的长度。但是它实际运行的效率可是比其它复杂度为O(n)的算法还要高。

随机生成长度100000的文本,模式为文本后缀,测试20000次
算法时间(ms)
Rabin-Karp1921
KMP6822
sunday1103
horspool252
String.indexOf(String)504

其实认真观察其代码,会发现它在一个大循环里面,是有两个小循环。

第一个循环是检测相同的首字符:

            /* Look for first character. */
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }

第二个循环再做余下字符的比较:

            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);

                if (j == end) {
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }

但是一般写字符串匹配蛮力法时,都是把这两个循环合并成一个的,如下所示

    public static int forceSearch(String text, String pattern) {
        int patternLength = pattern.length();
        int textLength = text.length();

        for (int i = 0, n = textLength - patternLength; i <= n; i++) {
            int j = 0;
            for (; j < patternLength && text.charAt(i + j) == pattern.charAt(j); j++) {
                ;
            }
            if (j == patternLength) {
                return i;
            }
        }
        return -1;
    }

这个forceSearch算法进行和上表一样的测试时,花费时间为3090ms(时间为String.indexOf的6倍)。但如果在循环中加上这段代码,会发现效率和String.indexOf(String)没有什么区别。

            if (text.charAt(i) != first) {//char first = pattern.charAt(0);
                while (++i <= n && text.charAt(i) != first)
                    ;
            }

也就是说,上面这个检查首字符的循环非常地块。

为什么会这样?我猜测原因有两个:

  1. 这个循环充分地利用了cpu缓存。
  2. java的JIT编译器检测到这个循环是热点代码,在运行时把它直接编译成机器指令。

对于第1个理由,我用分别用C语言编写带首字符检查循环的代码和不带首字符循环的代码,却发现两者运行时间差不多。

所以不是因为缓存的原因,而是JIT???我不知道自己在C上做的测试是否标准。我把这个问题放在StackOverflow上,有人建议我查看汇编来分析,但已我现在的基础,还看不懂汇编。

总之不管怎么样,java中String.indexOf方法性能很高,仅比horspool慢一点,但是horspool额外使用了一个256大小的整形数组。这也可以理解为什么java官方使用蛮力法。

三、工具和书籍

其实现在已经有很多性能很快的文本查找工具:GNU GREPACKSilver Search……如果有时间,个人打算参照这些软件的算法,自己实现一个java版本的

至于书籍,感觉字符串匹配的算法书比较少,而且都是比较老的:《柔性字符串匹配》、《Algorithms on Strings, Trees and Sequences

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值