Revisitar o algoritmo KMP de estrutura de dados e algoritmo

prefácio

O algoritmo KMP é um algoritmo de correspondência de strings, que pode encontrar a posição de ocorrência de uma string padrão em uma string principal. Em aplicações práticas, a correspondência de strings é um problema muito comum, como pesquisar palavras-chave em mecanismos de pesquisa, encontrar strings em editores de texto e assim por diante.

Os inventores do algoritmo KMP são Donald Knuth , James H. Morris e Vaughan Pratt , que publicaram um artigo "Fast Pattern Matching in Strings" em 1977, do qual Donald Knuth também é autor de "The Art of Computer Programming" .

Comparado com a complexidade de tempo do algoritmo de correspondência de força bruta O (nm) O(nm)O ( nm ) , a vantagem do algoritmo KMP é que sua complexidade de tempo éO ( n + m ) O(n+m)O ( n+m ) , onde n é o comprimento da string de texto e m é o comprimento da string padrão. Além disso, o algoritmo KMP também pode lidar com alguns casos especiais, como substrings repetidas na string padrão.

1. Princípio

1.1 Lei da Violência

Correspondência de string significa que há duas strings, ou seja, a string de texto s e a string de padrão p, calcule a posição em que p aparece pela primeira vez em s e retorne -1 se não aparecer.

O método de força bruta é o algoritmo de correspondência de string mais simples. Sua ideia é muito simples: comece a partir do primeiro caractere da string principal e compare cada caractere da string padrão por vez. Se a correspondência for bem-sucedida, continue a comparar o próximo caractere , caso contrário, refaça a correspondência a partir do próximo caractere na string principal.

Complexidade de Tempo de Força Bruta O ( m ∗ n ) O(m*n)O ( mn )

public int bruteForce(String s, String p) {
    
    
	int lenS = s.length();
	int lenP = p.length();
	if (lenS < lenP) return -1;

    for (int i = 0; i <= lenS - lenP; i++) {
    
    
        int pos = 0;
        while (pos < lenP) {
    
    
            if (s.charAt(i + pos) != p.charAt(pos)) {
    
    
                break;
            }
            pos++;
        }
        if (pos == lenP) return i;
    }
    return -1;
}

1.2 Sufixo comum mais longo

kmp_01. png

Usando o exemplo acima como referência, você descobrirá que existem muitas comparações redundantes no método de força bruta, pois uma vez que não há correspondência, a comparação será reiniciada diretamente do próximo dígito. O KMP é otimizado nessa área, não mais o próximo dígito, mas O número de saltos é determinado por uma próxima matriz.

A ideia central do algoritmo KMP é usar as informações que foram combinadas para minimizar o número de correspondências entre a string padrão e a string principal. Especificamente, o algoritmo KMP mantém uma próxima matriz, que é usada para registrar o comprimento do prefixo e sufixo comum mais longo na frente de cada caractere na string padrão. Durante o processo de correspondência, se o caractere atual não corresponder, a posição da string padrão é ajustada de acordo com o valor da próxima matriz, de modo que a string padrão se mova para a direita o máximo possível, reduzindo assim o número de correspondências .

Por que é o sufixo comum mais longo? Na verdade, pode ser bem entendido com o seguinte exemplo

kmp_02. png

Quando a correspondência falhar em j=6, você saberá que os primeiros 6 caracteres correspondem corretamente, então como obter a próxima posição de salto?

  • A olho nu podemos ver que a primeira letra é a, ou seja, o prefixo é a, então deve pular para a próxima posição de caractere a (i=2)

  • Verifica-se também que existe o mesmo que o prefixo ab, então a melhor posição deve ser a posição do próximo ab (i=4)

  • Por analogia, parece estar procurando a string mais longa com o mesmo prefixo e pulando para a posição correspondente

  • Na verdade, descobriu-se que a posição correspondente à string mais longa com o mesmo prefixo não é a solução ideal. A solução ideal está relacionada ao sufixo e há mais possibilidades após o sufixo.

  • Se houver a cadeia de caracteres mais longa t que é igual ao prefixo e não é um sufixo, então o prefixo + o último 1 caractere definitivamente não corresponderá a t + o último 1 caractere, o que é um desperdício de uma oportunidade de correspondência e é não é a melhor posição

  • Portanto, a melhor posição deve ser o prefixo comum mais longo e a parte do sufixo. Tome o exemplo acima: é a parte comum mais longa do sufixo de s[0:5] e o prefixo de p[0:5]. ab de abacab

Nota: O algoritmo KMP pode exercer habilidades poderosas apenas quando a string padrão tiver caracteres altamente repetitivos. Se forem caracteres completamente diferentes, ele degenerará em um algoritmo violento

2. Implementação do código

2.1 próxima matriz

O cálculo da próxima matriz é a chave para o algoritmo KMP, que é definido da seguinte forma:

next[i] indica o comprimento do prefixo e sufixo idênticos mais longos antes de cada posição na string padrão, ou seja, na substring p[0:i-1] terminando com o i - 1º caractere, o último que é tanto um prefixo e um sufixo O comprimento da string longa.

Especificamente, podemos calcular o próximo array recursivamente, e podemos seguir os passos abaixo:

  1. próximo[0]=-1
  2. Defina i para representar a posição calculada atual e j para representar o comprimento do sufixo de prefixo idêntico mais longo antes da posição atual
  3. O objetivo subsequente é encontrar p[0 : j-1] == p[ij : i-1] Se tal j for encontrado, então o valor de next[i] é j. Se nenhum j for encontrado, o valor de next[i] é 0
  4. Se p[i] == p[j], basta definir next[i+1] = next[i] + 1, e j é o comprimento do prefixo e sufixo mais longo anterior, que é next[i], ou seja próximo[++i] = ++j
  5. Se p[i] != p[j], j precisa ser rastreado de volta para a posição anterior next[j]
public static int[] getNext(String pattern) {
    
    
    int[] next = new int[pattern.length()];
    next[0] = -1;
    int i = 0, j = -1; // j为当前已经匹配的前缀的最长公共前后缀的长度
    while (i < pattern.length() - 1) {
    
    
        if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
    
    
            next[++i] = ++j; // 长度加1,并且将指针移向下一位
        } else {
    
    
            j = next[j]; // 回溯
        }
    }
    return next;
}

2.2 Visualização a seguir

A geração do próximo array é a coisa mais importante para entender o KMP. Visualizar como gerá-lo é muito útil para entender o KMP.

import tkinter as tk
import time

def changeColor(canvas, rects, color):
    for rect in rects:
        canvas.itemconfig(rect, fill=color)
def visualize_next(pattern):
    next = [-1] * len(pattern)
    root = tk.Tk()
    root.title("KMP Next Visualization")
    canvas_width = 800
    canvas_height = 600
    canvas = tk.Canvas(root, width=canvas_width, height=canvas_height)
    canvas.pack()
    block_width = 50
    block_height = 50
    x_margin = 50
    y_margin = 50
    nextbox = []
    pbox = []


    x = x_margin
    y = y_margin
    canvas.create_text(x-block_width/2, y+block_height/2, text="索引", font=("Arial", 16))
    for i in range(len(pattern)):
        canvas.create_rectangle(x, y, x+block_width, y+block_height, outline="black")
        canvas.create_text(x+block_width/2, y+block_height/2, text=str(i), font=("Arial", 16))
        x += block_width

    x = x_margin
    y += block_height
    canvas.create_text(x-block_width/2, y+block_height/2, text="p", font=("Arial", 16))
    for i in range(len(pattern)):
        pbox.append(canvas.create_rectangle(x, y, x+block_width, y+block_height, outline="black"))
        canvas.create_text(x+block_width/2, y+block_height/2, text=str(pattern[i]), font=("Arial", 16))
        x += block_width

    x = x_margin
    y += block_height
    canvas.create_text(x-block_width/2, y+block_height/2, text="Next", font=("Arial", 16))

    for i in range(len(pattern)):
        canvas.create_rectangle(x, y, x+block_width, y+block_height, outline="black")
        nextbox.append(canvas.create_text(x+block_width/2, y+block_height/2, text="", font=("Arial", 16)))
        x += block_width
    
    i = 0
    j = -1
    x = x_margin
    y += block_height
    i_rect = canvas.create_rectangle(x, y, x+block_width, y+block_height, fill="red")
    i_text = canvas.create_text(x+block_width/2, y+block_height/2, text=str("i"), font=("Arial", 16))
    y += block_height
    j_rect = canvas.create_rectangle(x - block_width, y, x, y+block_height, fill="blue")
    j_text = canvas.create_text(x- block_width/2, y+block_height/2, text=str("j"), font=("Arial", 16))

    canvas.itemconfig(nextbox[0], text=str("-1"))
    time.sleep(1)
    while i < len(pattern) - 1:
        changeColor(canvas, pbox, '')
        if j == -1 or pattern[i] == pattern[j]:
            i += 1
            j += 1
            canvas.move(i_rect, block_width, 0)
            canvas.move(i_text, block_width, 0)
            canvas.move(j_rect, block_width, 0)
            canvas.move(j_text, block_width, 0)
            canvas.itemconfig(nextbox[i], text=str(j))
            changeColor(canvas, pbox[0:j], 'blue')
            changeColor(canvas, pbox[i-j:i], 'red')
            canvas.update()
            time.sleep(1)
        else:
            tmp = j
            j = next[j]
            canvas.move(j_rect, (j - tmp)*block_width, 0)
            canvas.move(j_text, (j - tmp)*block_width, 0)

            canvas.update()
            time.sleep(1)
    root.mainloop()

if __name__ == "__main__":
    pattern = "abacabb"
    visualize_next(pattern)

kmp_next.gif

2,3 KMP

Transforme o algoritmo de força bruta:

public static int kmp(String s, String p) {
    
    
    int[] next = getNext(p);

    int lenS = s.length();
    int lenP = p.length();
    if (lenS < lenP) return -1;
    int i = 0;

    while (i <= lenS - lenP) {
    
    
        int pos = 0;
        while (pos < lenP) {
    
    
            if (s.charAt(i + pos) != p.charAt(pos)) {
    
    
                break;
            }
            pos++;
        }
        if (pos == lenP) return i;
        i += pos - next[pos];
    }
    return -1;
}

3. Resumo

3.1 Vantagens

  • A complexidade de tempo do algoritmo KMP é O(n+m), onde n é o comprimento da string principal e m é o comprimento da string padrão. Comparado com o O(n*m) do método violento, o algoritmo KMP é mais eficiente.

  • O módulo re em Python é implementado em linguagem C, e a camada inferior usa o algoritmo KMP para corresponder às expressões regulares. As expressões regulares geralmente são usadas para processar uma grande quantidade de dados de texto, portanto, os requisitos de desempenho para correspondência de expressão regular são relativamente altos. Usar o algoritmo KMP pode melhorar a eficiência da correspondência de expressão regular, portanto, usar o algoritmo KMP no módulo re em Python para obter correspondência de expressão regular pode melhorar o desempenho do programa.

  • O algoritmo KMP pode lidar com a situação em que há substrings repetidas na string padrão, enquanto outros algoritmos de correspondência de string não podem lidar com isso.

3.2 Desvantagens

  • Não há muitas aplicações no caso de strings pequenas.O método String.indexOf no JDK usa um algoritmo baseado em correspondência de força bruta em vez de KMP. Isso ocorre porque, em aplicações práticas, o comprimento da string geralmente não é muito longo e o algoritmo KMP precisa de espaço adicional para armazenar a tabela de prefixos, o que aumentará o uso de memória. Portanto, para strings mais curtas, um melhor desempenho pode ser obtido usando um algoritmo de correspondência de força bruta. Além disso, o método String.indexOf no JDK também usa algumas técnicas de otimização, como ignorar um determinado comprimento de caracteres quando a correspondência falha para reduzir o número de comparações. Essas técnicas de otimização podem melhorar a eficiência do algoritmo na maioria dos casos, de modo a atender às necessidades de aplicações práticas.
  • A limitação do algoritmo KMP é que ele precisa pré-processar a sequência padrão, e a complexidade de tempo desse pré-processamento é O (m) O(m)O ( m ) , portanto, a eficiência do algoritmo KMP pode ser afetada quando a sequência de padrão for muito longa.
  • O algoritmo KMP só pode ser usado para corresponder a uma única sequência de padrão e não pode lidar com a correspondência de várias sequências de padrão.

referência

  1. (Original) Explicação detalhada do algoritmo KMP
  2. Algoritmo KMP para pesquisa de padrões

Acho que você gosta

Origin blog.csdn.net/qq_23091073/article/details/131423272
Recomendado
Clasificación