使用kmp算法实现字符串的indexOf方法

535 阅读2分钟

KMP算法是一种改进的字符串匹配算法,主要就是利用已匹配的字符计算移动位数。

想要详细了解该算法,可以参考阮一峰这篇文章

一般我们写字符串匹配算法,都是逐位比较如:'abcdabgabcdabfg'、'abcdabf',匹配到'g'时,不相同了,然后就从第一字符串的第二位开始和第二字符串的第一位比较,相同就继续比较下一位,不同则第一字符串继续向后移动一位。这是比较好理解的做法,但是太慢了。

kmp算法,就是利用已匹配的字符信息计算下一次需要移动的位数。

公式:移动位数 = 已匹配的字符数 - 对应的部分匹配值

已匹配的字符:'abcdab'

部分匹配值就是已匹配的字符的前缀子串与后缀子串的最长公共元素的长度,如下:

前缀子串:'a'、'ab'、'abc'、'abcd'、'abcda'。

后缀子串:'bcdab'、'cdab'、'dab'、'ab'、'b'。

则部分匹配值为2,即'ab'。

所以移动位数为6 - 2 = 4。

不多说,直接上代码:

/**
 * @description 使用KMP算法计算移动位数
 * @param {string} pat 已匹配的字符串
 * @returns {number} 返回移动位数
 */
const getNext = (pat) => {
  const len = pat.length;

  if (len === 1) {
    return 1;
  }

  /**
   * 例如:字符串 pat = ‘abcdab’ => 前缀子串:prevs = [a, ab, abc, abcd, abcda]
   * 后缀子串:nexts = [bcdab, cdab, dab, ab, b]
   * 公共元素ab => 部分匹配值2
   * 推理可得只需要判断: prevs[0] =? nexts[4]  prevs[1] =? nexts[3] ...
   * 推理可得:pat.substr(0, i + 1) =? pat.substring(pat.length - i - 1, pat.length); 
   * i为数组索引,最大为pat.length - 2
   */
  let patLen = 0;
  for (let i = 0; i < len - 1; i++) {
    const num = i + 1;

    // 后缀子串的第一个字符必须要等于前缀子串的第一个字符即字符串的第一个字符,才有可能相等
    // 前缀子串的最后一个字符必须要等于后缀子串的最后一个字符即字符串的最后一个字符。
    if (pat[0] !== pat[len - num] || pat[len - 1] !== pat[i]) {
      continue;
    }

    // i = 0时,prev = 'a', next = 'b'
    // i = 4时, prev = 'abcda', next = 'bcdab'
    const prev = pat.substr(0, num);
    const next = pat.substring(len - num, len);

    if (prev === next && prev.length > patLen) {
      patLen = prev.length;
    }
  }

  return len - patLen;
}

/**
 * @description 使用kmp算法实现字符串的indexOf方法
 * @param {string} mainStr 主串
 * @param {string} childStr 子串
 * @returns {number} 找到了返回位置,反之则返回-1
 */
const kmpIndexOf = (mainStr, childStr) => {
  if (typeof childStr !== 'string' || typeof mainStr !== 'string') {
    throw new TypeError('arguments type error.');
  }

  if (childStr.length > mainStr.length) {
    throw new RangeError('arguments error.');
  }

  let shortIdx = 0;
  let longIdx = 0;

  while (longIdx < mainStr.length && shortIdx < childStr.length) {
    // 相同则继续比较下一位
    if (mainStr[longIdx] === childStr[shortIdx]) {
      longIdx++;
      shortIdx++;
      continue;
    }

    if (shortIdx === 0 || shortIdx === 1) {
      // 已匹配的字符数为零或一,则直接将主串匹配位置向后移一位,子串从头开始。
      longIdx++;
      shortIdx = 0;
    } else {
      /**
       * 使用kmp算法计算移动位数:移动位数 = 已匹配的字符数 - 对应的部分匹配值
       * 例如:字符串a = ‘abcdabgabcdabfg’和字符串b = ‘abcdabf’
       * 第一次的匹配字符为:‘abcdab’。
       * 前缀子串:'a'、'ab'、'abc'、'abcd'、'abcda',后缀子串:'bcdab'、'cdab'、'dab'、'ab'、'b'。
       * 部分匹配值为‘ab’,即2.
       * 则下一次匹配,a从索引为4的位置开始,b从索引为2的地方开始。
       */
      const begIdx = longIdx - shortIdx;
      const patStr = childStr.substring(0, shortIdx);
      const move = getNext(patStr);

      // 主串匹配位置向后移动move位
      longIdx = begIdx + move;

      // 子串不需要从0开始,可以理解为子串位置从0移动部分匹配值的长度
      shortIdx -= move;
    }
  }

  // 如果子串索引不等于子串长度,则代表没有找到
  if (shortIdx !== childStr.length) {
    return -1;
  }

  return longIdx - shortIdx;
}

原文链接,点这里。