Jak znaleźć nakładanie się dwóch łańcuchów w bash? [Zamknięte]

11

Mam dwa sznurki. Dla przykładu są one ustawione następująco:

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

Chcę znaleźć zakładkę zaczynającą się na początku ciągów. Przez nakładanie mam na myśli ciąg „test t” w moim powyższym przykładzie.

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

Gdyby łańcuchy były string1="atest toast"; string2="test test", nie zachodziłyby one na siebie, ponieważ kontrola zaczyna się od początku, a „a” na początku string1.

dezorientować
źródło
To jest dokładnie powód, dla którego ludzie nie powinni przesyłać pocztą; teraz ma wiele odpowiedzi na każdej stronie, które są różne, i jest temat na obie strony. Myślę, że i tak zostawię to tutaj
Michael Mrozek

Odpowiedzi:

10

Możesz pomyśleć o takiej funkcji z dodaniem kontroli błędów

common_prefix() {
  local n=0
  while [[ "${1:n:1}" == "${2:n:1}" ]]; do
    ((n++))
  done
  echo "${1:0:n}"
}
enzotib
źródło
Właśnie zauważyłem, że po uruchomieniu z dwoma pustymi / zerowymi argumentami wchodzi w pętlę ∞. [[ -z "$1$2" ]] && returnnaprawia to.
Peter.O
Ta metoda jest wykładniczo wolniejsza (a nie liniowa). Gdy struna podwaja się, czas rośnie czterokrotnie (w przybliżeniu). Oto kilka porównań długości łańcucha / czasu do podziału binarnego Gillesa : .. 64 0m0,005s vs 0m0,003s - 128 0m0,013s vs 0m0,003s - 256 0m0,041s vs 0m0,003s - 512 0m0,133 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 Kwadratowo, nie wykładniczo.
Gilles 'SO - przestań być zły'
Wydaje mi się, że bash przechowuje ciągi wewnętrznie o niejawnej długości, więc uzyskanie nznaku th wymaga skanowania nznaków w celu sprawdzenia, czy nie są one kończącym ciąg zerowy bajtem. Jest to spójne z tym, że bash nie może zapisać zera w bajcie.
Peter Cordes,
8

Można to zrobić całkowicie w bashu. Chociaż manipulowanie łańcuchem w pętli w bash jest powolne, istnieje prosty algorytm logarytmiczny w liczbie operacji powłoki, więc czyste bash jest realną opcją nawet dla długich łańcuchów.

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

Standardowy zestaw narzędzi obejmuje cmpporównywanie plików binarnych. Domyślnie wskazuje przesunięcie bajtów pierwszych różnych bajtów. Istnieje szczególny przypadek, gdy jeden ciąg znaków jest przedrostkiem drugiego: cmpgeneruje inny komunikat na STDERR; łatwym sposobem na poradzenie sobie z tym jest wybranie dowolnego krótszego ciągu.

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

Zauważ, że cmpdziała na bajtach, ale manipulacja ciągami bash działa na znakach. To robi różnicę w ustawieniach wielobajtowych, na przykład ustawień regionalnych wykorzystujących zestaw znaków UTF-8. Funkcja powyżej wyświetla najdłuższy prefiks ciągu bajtów. Aby obsłużyć ciągi znaków za pomocą tej metody, możemy najpierw przekonwertować ciągi znaków na kodowanie o stałej szerokości. Zakładając, że zestaw znaków lokalizacji jest podzbiorem Unicode, UTF-32 pasuje do rachunku.

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- przestań być zły”
źródło
Powracając do tego pytania (rok później), ponownie oceniłem najlepszą odpowiedź. To wszystko jest bardzo proste: kamień łamie nożyce, nożyce tną papier, papier owija kamień. a binarny zjada sekwencyjnie! .. nawet dla dość krótkich łańcuchów .. i jeśli chodzi o przetwarzanie sekwencyjnego umiarkowanego ciągu 10000 znaków while char-by-char, wciąż czekam na to, kiedy to piszę ... czas mija .. wciąż czekam (może coś jest źle z moim systemem) .. czas płynie .. musi być coś nie tak; to tylko 10 000 iteracji! Ach! cierpliwość jest cnotą (być może przekleństwem w tym przypadku) .. 13m53.755s .. vs, 0m0.322s
Peter.O
3 podane tutaj metody są najszybsze ze wszystkich przedstawionych odpowiedzi. Zasadniczo cmpjest najszybszy (ale nie jest oparty na znakach). Kolejnym jest iconvi to bardzo respectibly szybka binary-splitodpowiedź. Dzięki Gilles. Dotarcie do tego punktu zajęło mi rok, ale lepiej późno niż wcale. (PS. 2 modyfikacje literówek w iconvkodzie: $in =$LC_CTYPE}i \ in UTF-32) \ ) ... PPS. tak naprawdę ciąg, o którym wspomniałem powyżej, był dłuższy niż 10 000 znaków. Był to wynik {1..10000}, który wynosi 48 894, ale to nie zmienia różnicy
Peter.O
6

W sed, zakładając, że ciągi nie zawierają żadnych znaków nowej linii:

string1="test toast"
string2="test test"
printf "%s\n" "$string1" "$string2" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/'
jfg956
źródło
Ale powielasz to .
jfg956,
Znakomity! idzie bezpośrednio do mojej biblioteki porad i wskazówek :-)
hmontoliu
Lub, dla ciągu bash , który nie może zawierać \0. Za pomocą tri \0, metoda może obsłużyć { printf "%s" "$string1" |tr \\n \\0; echo; printf "%s" "$string2" |tr \\n \\0; echo; } | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/' |tr \\0 \\n
znaki
Właśnie przetestowałem tę sedmetodę nieco dalej i wydaje się, że użycie referencji w ten sposób (we wzorcu wyszukiwania) jest niezwykle drogie. Nadal przewyższa sekwencyjne zapętlenie bajt po bajcie (o współczynnik około 3), ale oto przykład: dla dwóch ciągów 32kb (z ostatnim bajtem innym), zajmuje to 2m4.880s, w porównaniu do podziału binarnego Gillesa metoda0m0.168s
Peter.O,
2

Wydaje mi się to prymitywne, ale możesz to zrobić brutalną siłą:

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

Chcę, aby istniał jakiś sprytny algorytm, ale nie mogę go znaleźć za pomocą krótkiego wyszukiwania.

Bruce Ediger
źródło
2
porównaj połowę i powtórz to n * log (n) zamiast n ^ 2.
Gilles 'SO - przestań być zły'
2
Dla ogólnego odniesienia jest to trochę powolna strona. Dwa ciągi znaków 32768 (ostatni znak jest inny) zajęły 6m27.689s.
Peter.O,