Comment trouver le chevauchement de deux chaînes dans bash? [fermé]


11

J'ai deux cordes. Pour les besoins de l'exemple, ils sont définis comme suit:

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

Ce que je veux, c'est trouver le chevauchement à partir du début des chaînes. Avec chevauchement, je veux dire la chaîne "test t" dans mon exemple ci-dessus.

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

Si les chaînes étaient, string1="atest toast"; string2="test test"elles n'auraient aucun chevauchement puisque le contrôle commence au début et le "a" au début de string1.



C'est exactement la raison pour laquelle les gens ne sont pas censés effectuer un cross-post; maintenant, il a plusieurs réponses sur chaque site qui sont différentes, et c'est sur le sujet pour les deux sites. Je pense que je vais juste le laisser ici de toute façon
Michael Mrozek

Réponses:


10

Vous pouvez penser à une fonction comme celle-ci, avec une vérification des erreurs à ajouter

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

Je viens de remarquer que lorsqu'il est exécuté avec deux arguments vides / nuls, il entre dans une boucle ∞. [[ -z "$1$2" ]] && returnle corrige.
Peter.O

Cette méthode est exponentiellement plus lente (plutôt que linéaire). Lorsque la chaîne double de longueur, le temps augmente d'un facteur 4 (environ). Voici quelques comparaisons de longueur de chaîne / temps avec la séparation binaire 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 Quadratique, pas exponentiellement.
Gilles 'SO- arrête d'être méchant'

Je suppose que bash stocke les chaînes en interne avec une longueur implicite, donc obtenir le ne caractère nécessite de scanner les ncaractères pour vérifier qu'ils ne sont pas le zéro octet de fin de chaîne. Ceci est cohérent avec bash étant incapable de stocker un octet zéro dans une variable.
Peter Cordes

8

Cela peut être fait entièrement à l'intérieur de bash. Bien que la manipulation de chaînes dans une boucle en bash soit lente, il existe un algorithme simple qui est logarithmique dans le nombre d'opérations du shell, donc bash pur est une option viable même pour les chaînes longues.

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 boîte cmpà outils standard comprend pour comparer les fichiers binaires. Par défaut, il indique le décalage en octets des premiers octets différents. Il existe un cas particulier lorsqu'une chaîne est un préfixe de l'autre: cmpproduit un message différent sur STDERR; un moyen simple de résoudre ce problème consiste à prendre la chaîne la plus courte.

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

Notez que cela cmpfonctionne sur les octets, mais la manipulation des chaînes de bash fonctionne sur les caractères. Cela fait une différence dans les paramètres régionaux multioctets, pour des exemples de paramètres régionaux utilisant le jeu de caractères UTF-8. La fonction ci-dessus affiche le préfixe le plus long d'une chaîne d'octets. Pour gérer les chaînes de caractères avec cette méthode, nous pouvons d'abord convertir les chaînes en un codage à largeur fixe. En supposant que le jeu de caractères des paramètres régionaux est un sous-ensemble d'Unicode, UTF-32 convient parfaitement.

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

En revisitant cette question (1 an après), j'ai réévalué la meilleure réponse. C'est très simple: le rock casse les ciseaux, les ciseaux coupent le papier, le papier enveloppe la roche. et binaire mange séquentiel! .. même pour des chaînes assez courtes .. et comme pour une chaîne modérée de 10000 caractères traitée séquentiellement via while char-by-char, je l'attends toujours pendant que j'écris ceci ... le temps passe ... toujours en attente (peut-être qu'il y a quelque chose mal avec mon système) .. le temps passe .. il doit y avoir quelque chose de mal; ce ne sont que 10 000 itérations! Ah! la patience est une vertu (peut-être une malédiction dans ce cas) .. 13m53.755s .. vs, 0m0.322s
Peter.O

Les 3 méthodes données ici sont les plus rapides de toutes les réponses présentées. Fondamentalement, cmpest la plus rapide (mais n'est pas basée sur les caractères). Le suivant est iconvet puis la réponse très respectable rapide binary-split. Merci Gilles. Il m'a fallu un an pour en arriver là, mais mieux vaut tard que jamais. (PS. 2 mods de typo dans le iconvcode: $in =$LC_CTYPE}et \ in UTF-32) \ ) ... PPS. en fait, la chaîne que j'ai mentionnée ci-dessus dépassait 10 000 caractères. C'était le résultat de {1..10000} qui est de 48 894, mais cela ne change pas le différentiel
Peter.O

6

Dans sed, en supposant que les chaînes ne contiennent aucun caractère de nouvelle ligne:

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

Mais dupliquez avec cela .
jfg956

Brillant! va directement à ma bibliothèque de trucs et astuces :-)
hmontoliu

Ou, pour une chaîne bash , qui ne peut pas contenir \0. En utilisant tret \0, la méthode peut gérer les sauts de ligne dans la chaîne, ....{ 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

Je viens de tester cette sedméthode un peu plus loin, et il semble que l'utilisation de références arrières de cette façon (dans le modèle de recherche) soit extrêmement coûteuse. Il surpasse toujours le bouclage octet par octet séquentiel (par un facteur d'environ 3), mais voici un exemple: pour deux chaînes de 32 Ko (avec le dernier octet différent), il faut 2m4.880s, par rapport au split binaire de Gilles méthode0m0.168s
Peter.O

2

Cela me semble grossier, mais vous pouvez le faire via la force brute:

#!/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))}

Je veux qu'un algorithme intelligent existe, mais je n'en trouve pas avec une courte recherche.



2
Pour référence générale, c'est un peu lent. Deux chaînes de 32768 caractères (le dernier caractère étant différent) ont pris 6m27.689s.
Peter.O
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.