Bagaimana cara menemukan tumpang tindih dari dua string di bash? [Tutup]

11

Saya punya dua string. Demi contoh mereka ditetapkan seperti ini:

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

Yang saya inginkan adalah menemukan tumpang tindih mulai dari awal string. Dengan tumpang tindih maksud saya string "test t" dalam contoh saya di atas.

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

Jika string adalah string1="atest toast"; string2="test test"mereka tidak akan tumpang tindih sejak cek dimulai dari awal dan "a" di awal string1.

membingungkan
sumber
Inilah tepatnya alasan orang tidak seharusnya melakukan posting silang; sekarang memiliki beberapa jawaban di setiap situs yang berbeda, dan itu pada topik untuk kedua situs. Saya pikir saya hanya akan meninggalkannya di sini
Michael Mrozek

Jawaban:

10

Anda dapat memikirkan fungsi seperti ini, dengan beberapa pemeriksaan kesalahan untuk ditambahkan

common_prefix() {
  local n=0
  while [[ "${1:n:1}" == "${2:n:1}" ]]; do
    ((n++))
  done
  echo "${1:0:n}"
}
enzotib
sumber
Saya hanya memperhatikan bahwa ketika dijalankan dengan dua args kosong / nol ia memasuki ∞ loop. [[ -z "$1$2" ]] && returnmemperbaikinya.
Peter.O
Metode ini lebih lambat secara eksponensial (bukan linear). Karena string berlipat ganda panjangnya, waktu meningkat dengan faktor 4 (kira-kira). Berikut ini adalah perbandingan string-panjang / waktu dengan binary-split 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 Secara kuadratik, tidak eksponensial.
Gilles 'SANGAT berhenti menjadi jahat'
Saya kira bash menyimpan string secara internal dengan panjang implisit, jadi mendapatkan nkarakter th memerlukan pemindaian nkarakter untuk memeriksa bahwa mereka bukan nol-byte penghentian string. Ini konsisten dengan bash karena tidak dapat menyimpan nol-byte dalam suatu variabel.
Peter Cordes
8

Ini bisa dilakukan sepenuhnya di dalam bash. Meskipun melakukan manipulasi string dalam satu loop dalam bash lambat, ada algoritma sederhana yang logaritmik dalam jumlah operasi shell, jadi bash murni adalah pilihan yang layak bahkan untuk string panjang.

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

Kotak alat standar termasuk cmpuntuk membandingkan file biner. Secara default, ini menunjukkan offset byte dari byte pertama yang berbeda. Ada kasus khusus ketika satu string adalah awalan dari yang lain: cmpmenghasilkan pesan berbeda pada STDERR; cara mudah untuk mengatasinya adalah dengan mengambil string mana yang paling pendek.

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

Catatan yang cmpberoperasi pada byte, tetapi manipulasi string bash beroperasi pada karakter. Ini membuat perbedaan dalam lokal multibyte, untuk contoh lokal menggunakan set karakter UTF-8. Fungsi di atas mencetak awalan terpanjang dari string byte. Untuk menangani string karakter dengan metode ini, pertama-tama kita dapat mengkonversi string ke encoding dengan lebar tetap. Dengan asumsi set karakter lokal adalah bagian dari Unicode, UTF-32 sesuai dengan tagihan.

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 'SANGAT berhenti menjadi jahat'
sumber
Meninjau kembali pertanyaan ini (1 tahun ke depan), saya telah menilai kembali jawaban terbaik . Ini semua cukup sederhana: gunting patah batu, gunting memotong kertas, kertas pembungkus batu. dan binary makan berurutan! .. bahkan untuk string yang cukup pendek .. dan untuk string char 10.000 moderat sedang diproses secara berurutan melalui while char-by-char, saya masih menunggu saat saya menulis ini .. waktu berlalu .. masih menunggu (mungkin ada sesuatu salah dengan sistem saya) .. waktu berlalu .. pasti ada sesuatu yang salah; hanya 10.000 iterasi! Ah! kesabaran adalah suatu kebajikan (mungkin kutukan dalam kasus ini) .. 13m53.755s .. vs, 0m0.322s
Peter.O
3 metode yang diberikan di sini adalah yang tercepat tercepat dari semua jawaban yang disajikan .. Pada dasarnya, cmpadalah yang tercepat (tetapi tidak berbasis char). Selanjutnya adalah iconvdan kemudian sangat respectibly cepat binary-splitjawaban. Terima kasih Gilles. Butuh waktu setahun bagi saya untuk sampai ke titik ini, tetapi lebih baik terlambat daripada tidak sama sekali. (PS. 2 kesalahan ketik dalam iconvkode: $masuk =$LC_CTYPE}dan \ masuk UTF-32) \ ) ... PPS. sebenarnya string yang saya sebutkan di atas lebih dari 10.000 karakter. Itu adalah hasil dari {1..10000} yaitu, 48.894, tetapi itu tidak 'mengubah diferensial
Peter.O
6

Selain itu, dengan asumsi string tidak mengandung karakter baris baru:

string1="test toast"
string2="test test"
printf "%s\n" "$string1" "$string2" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/'
jfg956
sumber
Tapi duplikat dengan ini .
jfg956
Cemerlang! langsung menuju ke perpustakaan tips & trik saya :-)
hmontoliu
Atau, untuk string bash , yang tidak dapat berisi \0. Menggunakan trdan \0, metode ini dapat menangani baris baru dalam 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
Saya baru saja menguji sedmetode ini sedikit lebih jauh, dan tampaknya menggunakan kembali-referensi dengan cara ini (dalam pola pencarian) sangat mahal. Itu masih mengungguli perulangan byte-by-byte berurutan (oleh kira-kira faktor 3), tetapi di sini adalah sebuah contoh: untuk dua string 32kb (dengan byte terakhir yang berbeda), dibutuhkan 2m4.880s, dibandingkan dengan binary-split Gilles metode0m0.168s
Peter.O
2

Ini kelihatannya kasar bagi saya, tetapi Anda bisa melakukannya melalui kekerasan:

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

Saya ingin beberapa algoritma pintar ada, tetapi saya tidak dapat menemukannya dengan pencarian singkat.

Bruce Ediger
sumber
2
bandingkan setengah dan ulangi adalah n * log (n) daripada n ^ 2.
Gilles 'SANGAT berhenti menjadi jahat'
2
Untuk referensi umum, ini agak lambat. Dua string karakter 32.768 (karakter terakhir yang berbeda) mengambil 6m27.689s.
Peter.O