Come trovo la sovrapposizione di due stringhe in bash? [chiuso]


11

Ho due stringhe. Per il bene dell'esempio sono impostati in questo modo:

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

Quello che voglio è trovare la sovrapposizione a partire dall'inizio delle stringhe. Con sovrapposizione intendo la stringa "test t" nel mio esempio precedente.

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

Se le stringhe fossero string1="atest toast"; string2="test test", non si sovrapporrebbero poiché il controllo inizia dall'inizio e la "a" all'inizio di string1.



Questo è esattamente il motivo per cui le persone non dovrebbero cross-post; ora ha più risposte su ogni sito che sono diverse ed è in argomento per entrambi i siti. Penso che lo lascerò comunque qui
Michael Mrozek

Risposte:


10

Puoi pensare a una funzione come questa, con qualche controllo degli errori da aggiungere

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

Ho appena notato che quando viene eseguito con due argomenti vuoto / null entra in un ciclo ∞. [[ -z "$1$2" ]] && returnlo risolve.
Peter

Questo metodo è esponenzialmente più lento (piuttosto che lineare). Man mano che la corda si raddoppia in lunghezza, il tempo aumenta di un fattore 4 (circa). Ecco alcuni confronti lunghezza-stringa / tempo con la divisione binaria di 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, non esponenzialmente.
Gilles 'SO- smetti di essere malvagio' il

Immagino che bash memorizzi le stringhe internamente con lunghezza implicita, quindi ottenere il ncarattere th richiede la scansione dei ncaratteri per verificare che non siano il byte zero che termina la stringa. Ciò è coerente con l'incapacità di bash di memorizzare un byte zero in una variabile.
Peter Cordes,

8

Questo può essere fatto interamente all'interno di bash. Sebbene la manipolazione delle stringhe in un ciclo in bash sia lenta, esiste un semplice algoritmo logaritmico nel numero di operazioni della shell, quindi bash puro è un'opzione praticabile anche per stringhe lunghe.

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 cassetta degli attrezzi standard include cmpper confrontare i file binari. Per impostazione predefinita, indica l'offset di byte dei primi byte diversi. C'è un caso speciale quando una stringa è un prefisso dell'altra: cmpproduce un messaggio diverso su STDERR; un modo semplice per affrontarlo è quello di prendere qualunque stringa sia la più 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"
}

Si noti che cmpopera su byte, ma la manipolazione di stringhe di bash opera su caratteri. Ciò fa la differenza nelle impostazioni locali multibyte, ad esempio impostazioni locali che utilizzano il set di caratteri UTF-8. La funzione sopra stampa il prefisso più lungo di una stringa di byte. Per gestire le stringhe di caratteri con questo metodo, possiamo prima convertire le stringhe in una codifica a larghezza fissa. Supponendo che il set di caratteri della locale sia un sottoinsieme di Unicode, UTF-32 si adatta al conto.

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

Rivisitando questa domanda (1 anno dopo), ho rivalutato la risposta migliore . È tutto abbastanza semplice: le forbici si rompono, le forbici tagliano la carta, la carta avvolge la roccia. e il binario mangia sequenziale! .. anche per stringhe piuttosto brevi .. e per quanto riguarda una stringa moderata da 10000 caratteri che viene elaborata sequenzialmente tramite while char-by-char, sto ancora aspettando mentre scrivo .. il tempo passa .. ancora aspettando (forse c'è qualcosa sbagliato nel mio sistema) .. il tempo passa .. ci deve essere qualcosa di sbagliato; sono solo 10.000 iterazioni! Ah! la pazienza è una virtù (forse una maledizione in questo caso) .. 13m53.755s .. vs, 0m0.322s
Peter.O

I 3 metodi indicati qui sono il più veloce tra tutte le risposte presentate. Fondamentalmente, cmpè il più veloce (ma non è basato sul carattere). Il prossimo è iconve quindi la risposta molto rispettabilmente veloce binary-split. Grazie Gilles. Mi ci è voluto un anno per arrivare a questo punto, ma meglio tardi che mai. (PS. 2 mod di errore di battitura nel iconvcodice: $in =$LC_CTYPE}e \ in UTF-32) \ ) ... PPS. in realtà la stringa che ho menzionato sopra era più lunga di 10.000 caratteri. È stato il risultato di {1..10000} che è, 48.894, ma questo non cambia il differenziale
Peter.O

6

In sed, supponendo che le stringhe non contengano caratteri di nuova riga:

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

Ma duplica con questo .
jfg956,

Brillante! va direttamente alla mia libreria di suggerimenti e trucchi :-)
hmontoliu

Oppure, per una stringa bash , che non può contenere \0. Usando tre \0, il metodo può gestire i newline nella stringa, ....{ 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

Ho appena testato questo sedmetodo un po 'di più e sembra che l'uso di riferimenti indietro in questo modo (nel modello di ricerca) sia estremamente costoso. Supera ancora il ciclo sequenziale byte per byte (di circa un fattore 3), ma ecco un esempio: per due stringhe da 32kb (con l'ultimo byte diverso), ci vuole 2m4.880s, rispetto alla divisione binaria di Gilles metodo0m0.168s
Peter.O

2

Mi sembra rozzo, ma puoi farlo con la forza 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))}

Voglio che esista un algoritmo intelligente, ma non riesco a trovarne nessuno con una breve ricerca.



2
Per riferimento generale, è un po 'lento. Due stringhe di 32768 caratteri (l'ultimo carattere diverso) hanno richiesto 6m27.689s.
Peter
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.