Como encontro a sobreposição de duas strings no bash? [fechadas]


11

Eu tenho duas cordas. Para fins de exemplo, eles são definidos assim:

string1="test toast"
string2="test test"

O que eu quero é encontrar a sobreposição começando no início das strings. Com sobreposição, quero dizer a string "test t" no meu exemplo acima.

# I look for the command 
command "$string1" "$string2"
# that outputs:
"test t"

Se as strings fossem, string1="atest toast"; string2="test test"elas não teriam sobreposição desde que a verificação começa no início e o "a" no início de string1.



Esta é exatamente a razão pela qual as pessoas não devem postar; agora ele tem várias respostas em cada site que são diferentes e é tópico para os dois sites. Eu acho que vou deixar aqui de qualquer maneira #
Michael Mrozek

Respostas:


10

Você pode pensar em uma função como esta, com alguma verificação de erro para adicionar

common_prefix() {
  local n=0
  while [[ "${1:n:1}" == "${2:n:1}" ]]; do
    ((n++))
  done
  echo "${1:0:n}"
}

Acabei de notar que, quando executado com dois argumentos vazios / nulos, ele entra em um loop ∞. [[ -z "$1$2" ]] && returncorrige isso.
precisa saber é o seguinte

Este método é exponencialmente mais lento (ao invés de linearmente). À medida que a corda dobra de comprimento, o tempo aumenta em um fator de 4 (aprox.). Aqui estão algumas comparações de comprimento de cadeia / tempo com a divisão binária de Gilles : .. 64 0m0.005s vs 0m0.003s - 128 0m0.013s vs 0m0.003s - 256 0m0.041s vs 0m0.003s - 512 0m0.143s vs 0m0.005s - 1024 0m0.421s vs 0m0.009s - 2048 0m1.575s vs 0m0.012s - 4096 0m5.967s vs 0m0.022s - 8192 0m24.693s vs 0m0.049s -16384 1m34.004s vs 0m0.085s - 32768 6m34.721s vs 0m0.168s - 65536 27m34.012s vs 0m0.370s
Peter.O

2
@ Peter.O Quadraticamente, não exponencialmente.
Gilles 'SO- stop be evil'

Eu acho que o bash armazena seqüências de caracteres internamente com comprimento implícito, portanto, para obter o ncaractere th, é necessário verificar os ncaracteres para verificar se eles não são o byte zero que termina a string. Isso é consistente com o bash sendo incapaz de armazenar um byte zero em uma variável.
Peter Cordes

8

Isso pode ser feito inteiramente dentro do bash. Embora a manipulação de strings em um loop no bash seja lenta, existe um algoritmo simples que é logarítmico no número de operações do shell, portanto o bash puro é uma opção viável mesmo para strings longas.

longest_common_prefix () {
  local prefix= n
  ## Truncate the two strings to the minimum of their lengths
  if [[ ${#1} -gt ${#2} ]]; then
    set -- "${1:0:${#2}}" "$2"
  else
    set -- "$1" "${2:0:${#1}}"
  fi
  ## Binary search for the first differing character, accumulating the common prefix
  while [[ ${#1} -gt 1 ]]; do
    n=$(((${#1}+1)/2))
    if [[ ${1:0:$n} == ${2:0:$n} ]]; then
      prefix=$prefix${1:0:$n}
      set -- "${1:$n}" "${2:$n}"
    else
      set -- "${1:0:$n}" "${2:0:$n}"
    fi
  done
  ## Add the one remaining character, if common
  if [[ $1 = $2 ]]; then prefix=$prefix$1; fi
  printf %s "$prefix"
}

A caixa de ferramentas padrão inclui cmppara comparar arquivos binários. Por padrão, indica o deslocamento de bytes dos primeiros bytes diferentes. Há um caso especial quando uma string é um prefixo da outra: cmpproduz uma mensagem diferente em STDERR; uma maneira fácil de lidar com isso é usar a string mais curta.

longest_common_prefix () {
  local LC_ALL=C offset prefix
  offset=$(export LC_ALL; cmp <(printf %s "$1") <(printf %s "$2") 2>/dev/null)
  if [[ -n $offset ]]; then
    offset=${offset%,*}; offset=${offset##* }
    prefix=${1:0:$((offset-1))}
  else
    if [[ ${#1} -lt ${#2} ]]; then
      prefix=$1
    else
      prefix=$2
    fi
  fi
  printf %s "$prefix"
}

Observe que cmpopera em bytes, mas a manipulação de string do bash opera em caracteres. Isso faz a diferença nos códigos de idioma multibyte, por exemplo, códigos de idioma usando o conjunto de caracteres UTF-8. A função acima imprime o prefixo mais longo de uma sequência de bytes. Para lidar com cadeias de caracteres com esse método, primeiro podemos converter as cadeias de caracteres em uma codificação de largura fixa. Supondo que o conjunto de caracteres do código de idioma seja um subconjunto de Unicode, o UTF-32 se ajusta à conta.

longest_common_prefix () {
  local offset prefix LC_CTYPE="${LC_ALL:=$LC_CTYPE}"
  offset=$(unset LC_ALL; LC_MESSAGES=C cmp <(printf %s "$1" | iconv -t UTF-32) \
                                           <(printf %s "$2" | iconv -t UTF-32) 2>/dev/null)
  if [[ -n $offset ]]; then
    offset=${offset%,*}; offset=${offset##* }
    prefix=${1:0:$((offset/4-1))}
  else
    if [[ ${#1} -lt ${#2} ]]; then
      prefix=$1
    else
      prefix=$2
    fi
  fi
  printf %s "$prefix"
}

Revisitando esta questão (1 ano depois), reavaliei a melhor resposta. É tudo muito simples: pedra quebra tesoura, tesoura corta papel, papel envolve pedra. e binário come sequencialmente! .. mesmo para seqüências de caracteres bastante curtas .. e quanto a uma seqüência moderada de 10000 caracteres sendo processada sequencialmente via while char-by-char, ainda estou esperando por isso enquanto escrevo isso .. o tempo passa .. ainda esperando (talvez haja algo errado com o meu sistema) .. o tempo passa .. deve haver algo errado; são apenas 10.000 iterações! Ah! paciência é uma virtude (talvez uma maldição, neste caso) .. 13m53.755s .. vs, 0m0.322s
Peter.O

Os 3 métodos dados aqui são os mais rápidos de todas as respostas apresentadas. Basicamente, cmpé o mais rápido (mas não é baseado em caracteres). O próximo é iconve, em seguida, a resposta muito respeitosamente rápida binary-split. Obrigado Gilles. Levei um ano para chegar a esse ponto, mas antes tarde do que nunca. (PS. 2 iconverros de digitação no código: $in =$LC_CTYPE}e \ in UTF-32) \ ) ... PPS. na verdade, a string que mencionei acima tinha mais de 10.000 caracteres. Foi o resultado de {1..10000}, ou seja, 48.894, mas isso não altera o diferencial
Peter.O

6

No sed, supondo que as strings não contenham caracteres de nova linha:

string1="test toast"
string2="test test"
printf "%s\n" "$string1" "$string2" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/'

Mas duplique com isso .
Jfg956

Brilhante! vai diretamente ao meu dicas e truques biblioteca :-)
hmontoliu

Ou, para uma sequência do bash , que não pode conter \0. Usando tre \0, o método pode lidar com novas linhas na string, ....{ printf "%s" "$string1" |tr \\n \\0; echo; printf "%s" "$string2" |tr \\n \\0; echo; } | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/' |tr \\0 \\n
Peter.O

Acabei de testar esse sedmétodo um pouco mais e parece que o uso de referências posteriores dessa maneira (no padrão de pesquisa) é extremamente caro. Ele ainda supera o loop seqüencial de byte a byte (em aproximadamente um fator de 3), mas aqui está um exemplo: para duas seqüências de 32kb (com o último byte sendo diferente), é necessário 2m4.880s, em comparação com a divisão binária de Gilles Método0m0.168s
Peter.O

2

Isso me parece grosseiro, mas você pode fazê-lo através da força bruta:

#!/bin/bash

string1="test toast"
string2="test test"

L=1  # Prefix length

while [[ ${string1:0:$L} == ${string2:0:$L} ]]
do
    ((L = L + 1))
done

echo Overlap: ${string1:0:$((L - 1))}

Quero que exista um algoritmo inteligente, mas não consigo encontrá-lo com uma breve pesquisa.


2
compare metade e repita é n * log (n) em vez de n ^ 2.
Gilles 'SO- stop be evil'

2
Para referência geral, é um pouco lento. Duas seqüências de caracteres 32768 (o último caractere sendo diferente) levaram 6m27.689s.
precisa saber é o seguinte
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.