¿Cómo encuentro la superposición de dos cadenas en bash? [cerrado]

11

Tengo dos cuerdas Por el bien del ejemplo, se establecen así:

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

Lo que quiero es encontrar la superposición que comienza en el comienzo de las cadenas. Con superposición me refiero a la cadena "prueba t" en mi ejemplo anterior.

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

Si las cadenas estuvieran, string1="atest toast"; string2="test test"no tendrían superposición ya que el cheque comienza desde el principio y la "a" al comienzo de string1.

confundir
fuente
Esta es exactamente la razón por la cual se supone que las personas no deben publicar mensajes cruzados; ahora tiene múltiples respuestas en cada sitio que son diferentes, y es sobre el tema para ambos sitios. Creo que lo dejaré aquí de todos modos
Michael Mrozek

Respuestas:

10

Puede pensar en una función como esta, con alguna verificación de error para agregar

common_prefix() {
  local n=0
  while [[ "${1:n:1}" == "${2:n:1}" ]]; do
    ((n++))
  done
  echo "${1:0:n}"
}
enzotib
fuente
Acabo de notar que cuando se ejecuta con dos args vacíos / nulos, entra en un bucle ∞. [[ -z "$1$2" ]] && returnlo arregla
Peter
Este método es exponencialmente más lento (en lugar de lineal). A medida que la cadena se dobla en longitud, el tiempo aumenta en un factor de 4 (aprox.). Aquí están algunas comparaciones cadena de longitud / hora para Gilles binaria-split : .. 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 Cuadráticamente, no exponencialmente.
Gilles 'SO- deja de ser malvado'
Supongo que bash almacena cadenas internamente con longitud implícita, por lo que obtener el ncarácter th requiere escanear ncaracteres para verificar que no sean el byte cero que termina la cadena. Esto es consistente con que bash no puede almacenar un byte cero en una variable.
Peter Cordes
8

Esto se puede hacer completamente dentro de bash. Si bien la manipulación de cadenas en un bucle en bash es lenta, existe un algoritmo simple que es logarítmico en la cantidad de operaciones de shell, por lo que bash puro es una opción viable incluso para cadenas largas.

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"
}

La caja de herramientas estándar incluye cmppara comparar archivos binarios. Por defecto, indica el desplazamiento de bytes de los primeros bytes diferentes. Hay un caso especial cuando una cadena es un prefijo de la otra: cmpproduce un mensaje diferente en STDERR; Una manera fácil de lidiar con esto es tomar la cadena que sea más corta.

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"
}

Tenga en cuenta que cmpopera en bytes, pero la manipulación de cadenas de bash funciona en caracteres. Esto marca la diferencia en configuraciones regionales de varios bytes, por ejemplo, configuraciones regionales que utilizan el juego de caracteres UTF-8. La función anterior imprime el prefijo más largo de una cadena de bytes. Para manejar cadenas de caracteres con este método, primero podemos convertir las cadenas a una codificación de ancho fijo. Suponiendo que el conjunto de caracteres de la configuración regional es un subconjunto de Unicode, UTF-32 se ajusta a la factura.

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"
}
Gilles 'SO- deja de ser malvado'
fuente
Revisando esta pregunta (1 año después), he reevaluado la mejor respuesta. Todo es bastante simple: la piedra rompe las tijeras, las tijeras cortan el papel, el papel envuelve la piedra. y binary come secuencial! ... incluso para cadenas bastante cortas ... y en cuanto a una secuencia moderada de 10000 char procesada secuencialmente while char-by-char, todavía estoy esperando mientras escribo esto ... el tiempo pasa ... todavía esperando (tal vez hay algo mal con mi sistema) ... el tiempo pasa ... debe haber algo mal; ¡son solo 10,000 iteraciones! Ah! la paciencia es una virtud (quizás una maldición en este caso) .. 13m53.755s .. vs, 0m0.322s
Peter.O
Los 3 métodos dados aquí son la respuesta más rápida de todas las respuestas presentadas. Básicamente, cmpes la más rápida (pero no está basada en caracteres). El siguiente es iconvy luego el muy respectibly rápida binary-splitrespuesta. Gracias Gilles Me llevó un año llegar a este punto, pero más vale tarde que nunca. (Modificaciones de error tipográfico PS 2 en iconvcódigo: $adentro =$LC_CTYPE}y \ adentro UTF-32) \ ) ... PPS. en realidad la cadena que mencioné anteriormente tenía más de 10,000 caracteres. Fue el resultado de {1..10000} que es 48,894, pero eso no 'cambia el diferencial
Peter.O
6

En sed, suponiendo que las cadenas no contienen ningún carácter de nueva línea:

string1="test toast"
string2="test test"
printf "%s\n" "$string1" "$string2" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/'
jfg956
fuente
Pero duplica con esto .
jfg956
¡Brillante! va directamente a mi biblioteca de consejos y trucos :-)
hmontoliu
O, para una cadena de bash , que no puede contener \0. Usando try \0, el método puede manejar nuevas líneas en la cadena, ...{ 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
Acabo de probar este sedmétodo un poco más, y parece que usar referencias posteriores de esta manera (en el patrón de búsqueda) es muy costoso. Todavía supera el bucle secuencial byte por byte (en un factor aproximado de 3), pero aquí hay un ejemplo: para dos cadenas de 32 kb (con el último byte diferente), toma 2m4.880s, en comparación con la división binaria de Gilles método0m0.168s
Peter.O
2

Esto me parece crudo, pero puedes hacerlo a través de la fuerza 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))}

Quiero que exista algún algoritmo inteligente, pero no puedo encontrar ninguno con una búsqueda corta.

Bruce Ediger
fuente
2
compare la mitad y repita es n * log (n) en lugar de n ^ 2.
Gilles 'SO- deja de ser malvado'
2
Como referencia general, es un poco lento. Dos cadenas de caracteres 32768 (el último carácter diferente) tomaron 6m27.689s.
Peter.O