Diseño de algoritmos - Algoritmo KMP

problema de coincidencia de patrón de cadena

Supongamos que hay dos cadenas S, T, donde S es la cadena principal (cadena de texto), T es la subcadena (cadena de patrón),

Necesitamos encontrar la subcadena que coincida con T en S y, si se encuentra correctamente, devolver la posición del primer carácter de la subcadena coincidente en la cadena principal.

Solución de algoritmo violento

icono

Asumiendo S = "aabaabaaf" y T = "aabaaf", el proceso de solución de fuerza bruta se muestra en la siguiente figura:

 El proceso de emparejamiento en la figura anterior se divide en dos ciclos:

El bucle externo, es decir, el control del número de rondas coincidentes, o en otras palabras, el control de la posición inicial coincidente de la cadena S, como:

  • En la ronda 0, la cadena T se compara desde la posición de índice 0 de la cadena S
  • En la primera ronda, la cadena T se empareja desde la posición de índice 1 de la cadena S
  • ...
  • En la k-ésima ronda, la cadena T se empareja desde la posición del índice k de la cadena S

El bucle interno, es decir, el rango de longitud k ~ k + t de la cadena T y la cadena S se combinan carácter por carácter uno por uno,

  • Si se encuentra que hay una inconsistencia en los caracteres correspondientes al dígito, significa que la ronda actual de emparejamiento falla y pasa directamente a la siguiente ronda
  • Si los caracteres en todas las posiciones son iguales, significa que la coincidencia es exitosa, es decir, la misma subcadena que T se encuentra en S, y la posición inicial de la subcadena es k

Supongamos que s.longitud = n, t.longitud = m, entonces la complejidad temporal de la solución violenta es O(n * m)

Código

Código fuente del algoritmo JS

/**
 * @param {*} s 正文串
 * @param {*} t 模式串
 * @returns 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
 */
function indexOf(s, t) {
  // k指向s的起始匹配位置
  for (let k = 0; k <= s.length - t.length; k++) {
    let i = k;
    let j = 0;

    while (j < t.length && s[i] == t[j]) {
      i++;
      j++;
    }

    if (j == t.length) {
      return k;
    }
  }

  return -1;
}

const s = "aabaabaafaab";
const t = "aabaaf";
console.log(indexOf(s, t));

Código fuente del algoritmo de Java

public class Main {
  public static void main(String[] args) {
    String s = "aabaabaaf";
    String t = "aabaaf";
    System.out.println(indexOf(s, t));
  }

  /**
   * @param s 正文串
   * @param t 模式串
   * @return 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
   */
  public static int indexOf(String s, String t) {
    // k指向s的起始匹配位置
    for (int k = 0; k <= s.length() - t.length(); k++) {
      int i = k;
      int j = 0;

      while (j < t.length() && s.charAt(i) == t.charAt(j)) {
        i++;
        j++;
      }

      if (j == t.length()) {
        return k;
      }
    }

    return -1;
  }
}

Código fuente del algoritmo de Python

def indexOf(s, t):
    """
    :param s: 正文串
    :param t: 模式串
    :return: 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
    """

    # k指向s的起始匹配位置
    for k in range(len(s) - len(t) + 1):
        i = k
        j = 0

        while j < len(t) and s[i] == t[j]:
            i += 1
            j += 1

        if j == len(t):
            return k

    return -1


if __name__ == '__main__':
    s = "aabaabaaf"
    t = "aabaaf"

    print(indexOf(s, t))

Algoritmo KMP

Estrategias mejoradas para soluciones de fuerza bruta

Para el problema de coincidencia de patrones de cadenas, el algoritmo de fuerza bruta no es la solución óptima. Aunque s y t son cadenas aleatorias, estas cadenas aleatorias también tienen ciertas reglas que se pueden usar.

Por ejemplo, en el ejemplo anterior, s = "aabaabaaf", t = "aabaaf"

Después del fracaso en la ronda 0, ¿están condenadas al fracaso la primera ronda y la segunda ronda?

La siguiente figura muestra el último fallo de coincidencia en la ronda 0:

Observamos la parte que coincide con éxito, es decir, la parte "aabaa", que tiene cierta simetría,

Si abstraemos la parte detrás de "aabaa" de S y T, como se muestra en la siguiente figura, entonces:

  • La ronda 0 de coincidencia falló porque la coincidencia de la "parte abstracta" falló
  • La primera ronda, la segunda ronda de falla de coincidencia, de hecho, la parte "aabaa" de la falla de coincidencia:

 Simplifiquemos la primera ronda, la segunda ronda y la tercera ronda nuevamente, como se muestra en la siguiente figura:

Entonces, es obvio que se puede encontrar que la primera ronda y la segunda ronda están condenadas al fracaso.

Tomemos otro ejemplo:

Si la S y la T anteriores no coinciden en la ronda 0 debido a la parte abstracta, en la siguiente ronda, puede saltar directamente a la posición simétrica para comenzar a coincidir, porque la coincidencia en la posición asimétrica definitivamente fallará.

En este caso, se está saltando dos rondas de emparejamiento, es decir, ahorrando dos rondas de tiempo de emparejamiento.

Piénselo de nuevo, ¿la nueva coincidencia de la parte simétrica omitida directamente arriba realmente ahorra solo dos rondas de coincidencia?

La siguiente figura muestra que después de que falla la ronda 0 de emparejamiento, salte directamente a la parte simétrica y comience a emparejar nuevamente.

Si corresponde al proceso de solución violenta, entonces la parte donde se dibuja una X debajo es el proceso omitido

Observemos los cambios de los punteros i y j durante el salto a la parte simétrica

Se puede encontrar que la posición del puntero i en S no ha cambiado, y el puntero j apunta de nuevo a la posición central "b" de la cadena simétrica "aabaa" de T.

Entonces, ¿cuál es la complejidad temporal del algoritmo mejorado anterior?

Dado que el algoritmo anterior garantiza que el puntero i no retrocederá, la complejidad del tiempo es solo O (n).

Y este algoritmo es en realidad el algoritmo KMP.

tabla de prefijos

Ya conocemos el principio general del algoritmo KMP, el más crítico de los cuales es encontrar la parte simétrica de su subcadena en la cadena patrón T,

Entonces, ¿cómo lograr esta función a través del código?

Los tres fundadores del algoritmo KMP, K, M y P, propusieron el concepto de una tabla de prefijos.

Por ejemplo, T = "aabaaf", primero debemos encontrar todas las subcadenas de T:

  • a
  • Automóvil club británico
  • aab
  • padre
  • el padre
  • aaaaaaaaaaaaa

Luego calcule la longitud del prefijo y el sufijo idénticos más largos de estas subcadenas

Supongamos que la longitud de la cadena s es n, entonces:

  • El prefijo son todas las subcadenas cuyo índice inicial debe ser 0 y el índice final <n-1
  • El sufijo son todas las subcadenas cuyo índice final debe ser n-1 y el índice inicial debe ser >0

por lo tanto

  • el prefijo y el sufijo no pueden ser la cadena en sí
  • El prefijo y el sufijo de la cadena s pueden superponerse

Tomemos un ejemplo, como enumerar todos los prefijos y sufijos de la subcadena "aabaa" de T

longitud prefijo sufijo
1 a a
2 Automóvil club británico Automóvil club británico
3 aab balido
4 padre Papá

Entre ellos, el prefijo y sufijo más largo e idéntico es "aa".

Tenga en cuenta que para determinar si el prefijo y el sufijo son iguales, se comparan uno por uno de izquierda a derecha, por lo que en el ejemplo anterior, el prefijo "aab" con una longitud de 3 y el sufijo "baa" son diferentes.

Puede haber superposición del mismo prefijo y sufijo,

Por ejemplo, en la siguiente cadena "ababab", el prefijo y sufijo más largo con el mismo nombre es "abab"

longitud prefijo sufijo
1 a b
2 abdominales abdominales
3 aba bebé
4 padre padre
5 ababa papaya

Por lo tanto, las longitudes del prefijo y el sufijo idénticos más largos de todas las subcadenas de T = "aabaaf" son:

subcadena de T sufijo idéntico más largo La longitud del prefijo y el sufijo idénticos más largos
a ninguno 0
Automóvil club británico a 1
aab ninguno 0
padre a 1
el padre Automóvil club británico 2
aaaaaaaaaaaaa ninguno 0

En la tabla de prefijos anterior, generalmente usamos la siguiente matriz para representar

siguiente = [0, 1, 0, 1, 2, 0]

Aplicación de la tabla de prefijos

Anteriormente calculamos la siguiente matriz de la tabla de prefijos a mano

siguiente = [0, 1, 0, 1, 2, 0]

Entonces, ¿cuál es el significado de los elementos de la siguiente matriz?

El elemento next[j] es en realidad la longitud más larga del mismo prefijo y sufijo de la subcadena 0~j, por ejemplo:

  • next[0] es la longitud más larga del mismo prefijo y sufijo de la subcadena 0~0 "a" de T
  • next[1] es la longitud más larga del mismo prefijo y sufijo de la subcadena 0~1 "aa" de T
  • next[2] es la longitud más larga del mismo prefijo y sufijo de la subcadena 0~2 "aab" de T
  • next[3] es la longitud más larga del mismo prefijo y sufijo de la subcadena 0~3 "aaba" de T
  • next[4] es la longitud más larga del mismo prefijo y sufijo de la subcadena 0~4 "aabaa" de T
  • next[5] es la longitud más larga del mismo prefijo y sufijo de la subcadena 0~5 "aabaaf" de T

Entonces, ¿cómo aplicar junto al algoritmo KMP?

Por ejemplo, en la siguiente figura, cuando s[i] != t[j], como analizamos anteriormente, necesitamos realizar las siguientes acciones:

  • i puntero permanece apuntando a
  • El puntero j retrocede al centro de la parte simétrica.

La ventaja de este ejercicio es que

  • Evitando el retroceso del puntero i (aumentando las rondas de comparación redundantes)
  • Evite la coincidencia redundante de la parte anterior al centro de la parte simétrica (porque debe ser igual, es una coincidencia redundante)

Sin embargo, la expresión de la posición central de la parte simétrica aquí en realidad está muy poco estudiada. Una expresión más rigurosa: debería ser "la posición después de la posición final del prefijo" en el prefijo y el sufijo idénticos más largos.

Y la última posición después de la posición final del prefijo con el sufijo idéntico más largo es en realidad la longitud del sufijo idéntico más largo .

Por lo tanto, cuando s[i] != t[j], debemos hacer j = next[ j - 1 ]

Además, si j = 0, no se puede hacer coincidir, y next[j-1] tendrá una excepción fuera de los límites en este momento, por lo que para esta situación i, debemos tratarla de manera especial, como se muestra en la figura a continuación, es un j = 0 que no se puede igualar Condición:

En este punto, debemos mantener i++, j sin cambios y continuar emparejando

En realidad, esto no entra en conflicto con la condición de que el puntero i no retrocede según lo estipulado en el algoritmo KMP anterior. Porque el puntero i en el proceso anterior no retrocede.

Implementación del algoritmo KMP (excluyendo la implementación de generación de tablas de prefijos)

Aquí, la lógica de generación de la tabla de prefijos no se implementa primero, y la lógica del algoritmo KMP simplemente se implementa

Código fuente del algoritmo JS

/**
 * @param {*} s 正文串
 * @param {*} t 模式串
 * @returns 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
 */
function indexOf(s, t) {
  // 手算的T串"aabaaf"对应的前缀表
  let next = [0, 1, 0, 1, 2, 0];
  // 手算的T串"cabaa"对应的前缀表
  // next = [0, 0, 0, 0, 0];

  let i = 0; // 扫描S串的指针
  let j = 0; // 扫描T串的指针

  // 如果 i 指针扫描到S串结束位置,或者 j 指针扫描到T串的结束位置,都应该结束查找
  while (i < s.length && j < t.length) {
    if (s[i] == t[j]) {
      // 如果 s[i] == t[j],则当前位置匹配成功,继续匹配下一个位置
      i++;
      j++;
    } else {
      // 如果 s[i] != t[j],则说明当前位置匹配失败,
      // 根据KMP算法,我们只需要回退T串的 j 指针到 next[j-1]位置,即最长相同前缀的结束位置后面一个位置,而S串的 i 指针保持不动
      if (j > 0) {
        j = next[j - 1];
      } else {
        // 如果 j = 0,则说明S子串subS和T在第一个字符上就匹配不上, 此时T不匹配字符T[j]前面已经没有前后缀了,因此只能匹配下一个S子串
        i++;
      }
    }
  }

  // 如果最终可以在S串中找到匹配T的子串,则T串的所有字符都应该被j扫描过,即最终 j = t.length
  if (j >= t.length) {
    // 则S串中匹配T的子串的首字符位置应该在 i - t.length位置,因为 i 指针最终会扫描到S串中匹配T的子串的结束位置的后一个位置
    return i - j;
  } else {
    // 否则就是没有在S中找到匹配T的子串
    return -1;
  }
}

const s = "aabaabaafaab";
let t = "aabaaf";
// t = "cabaa"; // 该T串用于测试第一个字符就不匹配的情况
console.log(indexOf(s, t));

Código fuente del algoritmo de Java

public class Main {
  public static void main(String[] args) {
    String s = "aabaabaaf";
    String t = "aabaaf";
    //    t = "cabaa"; // 该T串用于测试第一个字符就不匹配的情况

    System.out.println(indexOf(s, t));
  }

  /**
   * @param s 正文串
   * @param t 模式串
   * @return 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
   */
  public static int indexOf(String s, String t) {
    // 手算的T串"aabaaf"对应的前缀表
    int[] next = {0, 1, 0, 1, 2, 0};
    // 手算的T串"cabaa"对应的前缀表
    //    next = new int[] {0, 0, 0, 0, 0};

    int i = 0; // 扫描S串的指针
    int j = 0; // 扫描T串的指针

    // 如果 i 指针扫描到S串结束位置,或者 j 指针扫描到T串的结束位置,都应该结束查找
    while (i < s.length() && j < t.length()) {
      // 如果 s[i] == t[j],则当前位置匹配成功,继续匹配下一个位置
      if (s.charAt(i) == t.charAt(j)) {
        i++;
        j++;
      } else {
        // 如果 s[i] != t[j],则说明当前位置匹配失败,
        // 根据KMP算法,我们只需要回退T串的 j 指针到 next[j-1]位置,即最长相同前缀的结束位置后面一个位置,而S串的 i 指针保持不动
        if (j > 0) {
          j = next[j - 1];
        } else {
          // 如果 j = 0,则说明S子串subS和T在第一个字符上就匹配不上, 此时T不匹配字符T[j]前面已经没有前后缀了,因此只能匹配下一个S子串
          i++;
        }
      }
    }

    // 如果最终可以在S串中找到匹配T的子串,则T串的所有字符都应该被j扫描过,即最终 j = t.length
    if (j == t.length()) {
      // 则S串中匹配T的子串的首字符位置应该在 i - t.length位置,因为 i 指针最终会扫描到S串中匹配T的子串的结束位置的后一个位置
      return i - j;
    } else {
      // 否则就是没有在S中找到匹配T的子串
      return -1;
    }
  }
}

Código fuente del algoritmo de Python

def indexOf(s, t):
    """
    :param s: 正文串
    :param t: 模式串
    :return: 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
    """

    # 手算的T串"aabaaf"对应的前缀表
    next = [0, 1, 0, 1, 2, 0]

    # 手算的T串"cabaa"对应的前缀表
    # next = [0, 0, 0, 0, 0]

    i = 0  # 扫描S串的指针
    j = 0  # 扫描T串的指针

    # 如果 i 指针扫描到S串结束位置,或者 j 指针扫描到T串的结束位置,都应该结束查找
    while i < len(s) and j < len(t):
        # 如果 s[i] == t[j],则当前位置匹配成功,继续匹配下一个位置
        if s[i] == t[j]:
            i += 1
            j += 1
        else:
            # 如果 s[i] != t[j],则说明当前位置匹配失败
            # 根据KMP算法,我们只需要回退T串的 j 指针到 next[j-1]位置,即最长相同前缀的结束位置后面一个位置,而S串的 i 指针保持不动
            if j > 0:
                j = next[j - 1]
            else:
                # 如果 j = 0,则说明S子串subS和T在第一个字符上就匹配不上, 此时T不匹配字符T[j]前面已经没有前后缀了,因此只能匹配下一个S子串
                i += 1

    # 如果最终可以在S串中找到匹配T的子串,则T串的所有字符都应该被j扫描过,即最终 j = t.length
    if j >= len(t):
        # 则S串中匹配T的子串的首字符位置应该在 i - t.length位置,因为 i 指针最终会扫描到S串中匹配T的子串的结束位置的后一个位置
        return i - j
    else:
        # 否则就是没有在S中找到匹配T的子串
        return -1


if __name__ == '__main__':
    s = "aabaabaaf"
    t = "aabaaf"
    # t = "cabaa"  # 该T串用于测试第一个字符就不匹配的情况

    print(indexOf(s, t))

Generación de tabla de prefijos

Ya hemos calculado la tabla de prefijos a mano, pero el proceso de cálculo manual es un proceso de enumeración violento, es decir, enumerar todos los prefijos y sufijos, y luego comparar los prefijos y sufijos de la misma longitud para ver si el contenido correspondiente es lo mismo.

En cuanto a la generación de la tabla de prefijos, podemos usar programación dinámica para resolver.

Ahora requerimos NEXT[J], asumiendo que se conoce NEXT[J-1] = K, como en la siguiente figura

Si T[J] == T[K], entonces 

 

Entonces SIGUIENTE[J] = K + 1

(PD: si no lo entiende, puede reemplazar lo anterior con "d", y luego calcular NEXT[J] a mano)

 

Si T[J] ! = T[K]

 

Entonces, ¿cómo resolver NEXT[J]?

De hecho, para cambiar el pensamiento, podemos aplicar la idea anterior del algoritmo KMP, como se muestra en la figura a continuación, podemos imaginar la cadena T como dos cadenas avatar, las cadenas SS y TT que se muestran en la figura a continuación,

La cadena SS es la parte del rango del sufijo de la cadena T original, y la cadena TT es la parte del rango del prefijo de la cadena T original.

 

 

 

Ahora se ha determinado que SS[J] ! = TT[K] , por lo que debemos hacer retroceder el puntero K de la cadena TT, es decir, retroceder a la posición NEXT[K-1]

 

Luego proceda a comparar T[J] y T[K]:

  • Si T[J] == T[K], entonces SIGUIENTE[J] = K + 1

¿Por qué podemos pensar directamente que la parte 0~K-1 debe ser la misma que la parte JK~J-1?

De hecho, si la parte anterior 0~K-1 y la parte JK~J-1 regresan a la cadena T, como se muestra en la figura a continuación

 Si va un paso más allá, como se muestra en la siguiente figura

 

  • Si T[J] ! = T[K], entonces nuevamente K = NEXT[K-1]

Por lo tanto, la lógica de generación de la tabla de prefijos aquí en realidad aplica el algoritmo KMP, pero la tabla de prefijos aquí tiene solo una cadena T, y necesitamos abstraerla en dos cadenas virtuales SS (cadena principal virtual) y TT (cadena de modo virtual) .

Para la implementación del código de la tabla de prefijos, consulte el método getNext en la implementación del código en la siguiente sección. Puede comparar la lógica del algoritmo KMP para ver las similitudes entre los dos.

Implementación del algoritmo KMP (incluida la implementación de generación de tablas de prefijos)

Código fuente del algoritmo de Java

public class Main {
  public static void main(String[] args) {
    String s = "xyz";
    String t = "z";

    System.out.println(indexOf(s, t));
  }

  /**
   * @param s 正文串
   * @param t 模式串
   * @return 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
   */
  public static int indexOf(String s, String t) {
    int[] next = getNext(t);

    int i = 0; // 扫描S串的指针
    int j = 0; // 扫描T串的指针

    // 如果 i 指针扫描到S串结束位置,或者 j 指针扫描到T串的结束位置,都应该结束查找
    while (i < s.length() && j < t.length()) {
      // 如果 s[i] == t[j],则当前位置匹配成功,继续匹配下一个位置
      if (s.charAt(i) == t.charAt(j)) {
        i++;
        j++;
      } else {
        // 如果 s[i] != t[j],则说明当前位置匹配失败,
        // 根据KMP算法,我们只需要回退T串的 j 指针到 next[j-1]位置,即最长相同前缀的结束位置后面一个位置,而S串的 i 指针保持不动
        if (j > 0) {
          j = next[j - 1];
        } else {
          // 如果 j = 0,则说明S子串subS和T在第一个字符上就匹配不上, 此时T不匹配字符T[j]前面已经没有前后缀了,因此只能匹配下一个S子串
          i++;
        }
      }
    }

    // 如果最终可以在S串中找到匹配T的子串,则T串的所有字符都应该被j扫描过,即最终 j = t.length
    if (j == t.length()) {
      // 则S串中匹配T的子串的首字符位置应该在 i - t.length位置,因为 i 指针最终会扫描到S串中匹配T的子串的结束位置的后一个位置
      return i - j;
    } else {
      // 否则就是没有在S中找到匹配T的子串
      return -1;
    }
  }

  public static int[] getNext(String t) {
    int[] next = new int[t.length()];

    int j = 1;
    int k = 0;

    while (j < t.length()) {
      if (t.charAt(j) == t.charAt(k)) {
        next[j] = k + 1;
        j++;
        k++;
      } else {
        if (k > 0) {
          k = next[k - 1];
        } else {
          j++;
        }
      }
    }

    return next;
  }
}

Código fuente del algoritmo JS

/**
 * @param {*} s 正文串
 * @param {*} t 模式串
 * @returns 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
 */
function indexOf(s, t) {
  let next = getNext(t);

  let i = 0; // 扫描S串的指针
  let j = 0; // 扫描T串的指针

  // 如果 i 指针扫描到S串结束位置,或者 j 指针扫描到T串的结束位置,都应该结束查找
  while (i < s.length && j < t.length) {
    if (s[i] == t[j]) {
      // 如果 s[i] == t[j],则当前位置匹配成功,继续匹配下一个位置
      i++;
      j++;
    } else {
      // 如果 s[i] != t[j],则说明当前位置匹配失败,
      // 根据KMP算法,我们只需要回退T串的 j 指针到 next[j-1]位置,即最长相同前缀的结束位置后面一个位置,而S串的 i 指针保持不动
      if (j > 0) {
        j = next[j - 1];
      } else {
        // 如果 j = 0,则说明S子串subS和T在第一个字符上就匹配不上, 此时T不匹配字符T[j]前面已经没有前后缀了,因此只能匹配下一个S子串
        i++;
      }
    }
  }

  // 如果最终可以在S串中找到匹配T的子串,则T串的所有字符都应该被j扫描过,即最终 j = t.length
  if (j >= t.length) {
    // 则S串中匹配T的子串的首字符位置应该在 i - t.length位置,因为 i 指针最终会扫描到S串中匹配T的子串的结束位置的后一个位置
    return i - j;
  } else {
    // 否则就是没有在S中找到匹配T的子串
    return -1;
  }
}

function getNext(t) {
  const next = new Array(t.length).fill(0);

  let j = 1;
  let k = 0;

  while (j < t.length) {
    if (t[j] == t[k]) {
      next[j] = k + 1;
      j++;
      k++;
    } else {
      if (k > 0) {
        k = next[k - 1];
      } else {
        j++;
      }
    }
  }

  return next;
}

const s = "aabaabaafaab";
let t = "aabaaf";
console.log(indexOf(s, t));

Código fuente del algoritmo de Python

def getNext(t):
    next = [0] * len(t)

    j = 1
    k = 0

    while j < len(t):
        if t[j] == t[k]:
            next[j] = k + 1
            j += 1
            k += 1
        else:
            if k > 0:
                k = next[k - 1]
            else:
                j += 1

    return next


def indexOf(s, t):
    """
    :param s: 正文串
    :param t: 模式串
    :return: 在s中查找与t相匹配的子串,如果成功找到,则返回匹配的子串第一个字符在主串中的位置
    """

    next = getNext(t)

    # 手算的T串"cabaa"对应的前缀表
    # next = [0, 0, 0, 0, 0]

    i = 0  # 扫描S串的指针
    j = 0  # 扫描T串的指针

    # 如果 i 指针扫描到S串结束位置,或者 j 指针扫描到T串的结束位置,都应该结束查找
    while i < len(s) and j < len(t):
        # 如果 s[i] == t[j],则当前位置匹配成功,继续匹配下一个位置
        if s[i] == t[j]:
            i += 1
            j += 1
        else:
            # 如果 s[i] != t[j],则说明当前位置匹配失败
            # 根据KMP算法,我们只需要回退T串的 j 指针到 next[j-1]位置,即最长相同前缀的结束位置后面一个位置,而S串的 i 指针保持不动
            if j > 0:
                j = next[j - 1]
            else:
                # 如果 j = 0,则说明S子串subS和T在第一个字符上就匹配不上, 此时T不匹配字符T[j]前面已经没有前后缀了,因此只能匹配下一个S子串
                i += 1

    # 如果最终可以在S串中找到匹配T的子串,则T串的所有字符都应该被j扫描过,即最终 j = t.length
    if j >= len(t):
        # 则S串中匹配T的子串的首字符位置应该在 i - t.length位置,因为 i 指针最终会扫描到S串中匹配T的子串的结束位置的后一个位置
        return i - j
    else:
        # 否则就是没有在S中找到匹配T的子串
        return -1


if __name__ == '__main__':
    s = "aabaabaaf"
    t = "aabaaf"
    # t = "cabaa"  # 该T串用于测试第一个字符就不匹配的情况

    print(indexOf(s, t))

Supongo que te gusta

Origin blog.csdn.net/qfc_128220/article/details/131311563
Recomendado
Clasificación