java实现字符串匹配之Rabin-Karp算法

前言

字符串匹配就是求一个子串在给定字符串的起始位置。我们先用暴力解法实现,然后在此基础上优化成Rabin-Karp算法。

暴力解法

public interface StringMatcher {

  int indexOf(String source, String target);
}

/**
 * 暴力解法
 */
public class BruteForceStringMatcher implements StringMatcher {


  @Override
  public int indexOf(String source, String target) {
    if (target.length() == 0) {
      return 0;
    }
    if (source.length() < target.length()) {
      return -1;
    }
    char[] sourceArr = source.toCharArray();
    char[] targetArr = target.toCharArray();
    for (int i = 0; i < source.length(); i++) {
      if (equals(sourceArr, i, i + targetArr.length - 1, targetArr)) {
        return i;
      }
    }
    return -1;
  }

  private boolean equals(char[] source, int start, int end, char[] target) {
    if (end >= source.length) {
      return false;
    }
    for (int i = 0; i <= end - start; i++) {
      if (source[i + start] != target[i]) {
        return false;
      }
    }
    return true;
  }

}

在主串中,检查起始位置分别是 0、1、2…n-m 且长度为 m 的 n-m+1 个子串,看有没有跟模式串匹配的。

Rabin-Karp算法

/**
 * Rabin-Karp算法,将子串的比较转换成子串哈希值的比较
 */
public class RabinKarpStringMatcher implements StringMatcher {

  /**
   * 表示进制,参考go语言中Rabin-Karp算法实现中的值
   */
  private static final int R = 16777619;
  /**
   * 哈希值可能太大,取模,随机值BigInteger.probablePrime(31, new Random())
   */
  private static final long Q = 1538824213;

  @Override
  public int indexOf(String source, String target) {
    int targetLen = target.length();
    int sourceLen = source.length();

    if (targetLen == 0) {
      return 0;
    }
    if (sourceLen < targetLen) {
      return -1;
    }
    long RM = initRM(target);
    long targetHash = hash(target, 0, targetLen - 1);
    int index = 0;
    long sourceHash = 0;
    while (index <= sourceLen - targetLen) {
      // 开始比较
      sourceHash = nextHash(source, target, index, sourceHash, RM);
      if (sourceHash == targetHash) {
        if (equals(source, index, index + targetLen - 1, target)) {
          return index;
        }
      }
      index++;
    }
    return -1;
  }

  private long nextHash(String source, String target, int index, long preHash, long RM) {
    int targetLen = target.length();
    if (index == 0) {
      return hash(source, 0, targetLen - 1);
    }
    long hash = preHash;
    // 去掉第一个字符的hash值
    hash = mod(hash - mod(RM * source.charAt(index - 1)));
    // 加上下一个字符的hash值
    hash = mod(mod(hash * R) + source.charAt(index + targetLen - 1));
    return hash;
  }

  private long hash(String str, int start, int end) {
    long hash = 0;
    for (int i = start; i <= end; i++) {
      hash = mod(hash * R + str.charAt(i));
    }
    return hash;
  }

  private long mod(long hash) {
    if (hash < 0) {
      return hash + Q;
    }
    return hash % Q;
  }

  private long initRM(String target) {
    long RM = 1;
    for (int i = 1; i < target.length(); i++) {
      RM = (R * RM) % Q;
    }
    return RM;
  }

  private boolean equals(String source, int start, int end, String target) {
    if (end >= source.length()) {
      return false;
    }
    for (int i = 0; i <= end - start; i++) {
      if (source.charAt(i + start) != target.charAt(i)) {
        return false;
      }
    }
    return true;
  }

}

暴力解法的瓶颈在于子串的比较,需要每个字符依次比较,Rabin-Karp算法将其转换成哈希值的比较,当然哈希值可能有冲突,在哈希值相等的情况下,还需要再进行一次字符串的朴素比较。关于Rabin-Karp算法的详细介绍,可以看 基础知识 - Rabin-Karp 算法 这篇博客,【go源码分析】strings.go 里的那些骚操作 这篇文章解释了为什么go语言没有显式的对hash值取模。

参考

基础知识 - Rabin-Karp 算法
子字符串查找----Rabin-Karp算法(基于散列)
Rabin-Karp算法github开源实现
Rabin-Karp在go语言中的实现
【go源码分析】strings.go 里的那些骚操作

posted @ 2021-05-06 21:59  strongmore  阅读(563)  评论(0编辑  收藏  举报