Як знайти перекриття двох рядків у баші? [зачинено]


11

У мене дві струни. Для прикладу вони встановлюються так:

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

Те, що я хочу, - це знайти перекриття, починаючи з початку рядків. Під перекриттям я маю на увазі рядок "test t" у своєму вище прикладі.

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

Якби рядки були, string1="atest toast"; string2="test test"вони не мали би перекриватись, оскільки перевірка починається з початку, а "a" на початку string1.



Це саме та причина, чому люди не повинні переписувати публікацію; тепер на кожному сайті є кілька відповідей, які відрізняються, і це тематично для обох сайтів. Я думаю, що я все-таки залишу його тут все одно
Майкл Мрозек

Відповіді:


10

Ви можете придумати таку функцію, як слід додати деяку перевірку помилок

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

Я щойно помітив, що при запуску з двома порожніми / нульовими аргументами він входить у ∞ цикл. [[ -z "$1$2" ]] && returnвиправляє це.
Пітер.O

Цей метод експоненціально повільніше (а не лінійно). Оскільки струна подвоюється в довжину, час збільшується в 4 рази. Ось декілька порівнянь довжини / часу за бінарним розколом : .. 64 0m0.005s проти 0m0.003s - 128 0m0.013s проти 0m0.003s - 256 0m0.041s проти 0m0.003s - 512 0m0.143s проти 0m0.005s - 1024 0m0.421s проти 0m0.009s - 2048 0m1.575s проти 0m0.012s - 4096 0m5.967s проти 0m0.022s - 8192 0m24.693s проти 0m0.049s -16384 1m34.004s vs 0m0.085s - 32768 6m34.721s vs 0m0.168s - 65536 27m34.012s проти 0m0.370s
Peter.O

2
@ Peter.O Квадратично, а не експоненціально.
Жил "ТАК - перестань бути злим"

Я думаю, що bash зберігає рядки внутрішньо з неявною довжиною, тому для отримання nсимволу th потрібно сканувати nсимволи, щоб перевірити, чи вони не є нульовим байтом, що закінчується рядком. Це відповідає тому, що bash не може зберігати нульовий байт у змінній.
Пітер Кордес

8

Це можна зробити повністю всередині bash. Хоча робити обробку рядків в циклі в bash повільно, існує простий алгоритм, який є логарифмічним за кількістю операцій з оболонкою, тому чистий bash є життєздатним варіантом навіть для довгих рядків.

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

Стандартна панель інструментів включає cmpпорівняння бінарних файлів. За замовчуванням він вказує зміщення байтів перших різних байтів. Існує особливий випадок, коли одна рядок є префіксом іншої: cmpстворює інше повідомлення на STDERR; найпростіший спосіб впоратися з цим - взяти ту, яка струна є найкоротшою.

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

Зверніть увагу, що cmpпрацює на байтах, але маніпуляція з рядком Баша працює на символах. Це робить різницю у багатобайтових локалях, наприклад, у локалях, що використовують набір символів UTF-8. Функція вище друкує найдовший префікс байтового рядка. Для обробки символьних рядків цим методом ми можемо спочатку перетворити рядки в кодування фіксованої ширини. Якщо припустити, що набір символів локалі є підмножиною Unicode, UTF-32 підходить до рахунку.

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

Переглядаючи це питання (1 рік тому), я переоцінив найкращу відповідь. Все досить просто: скеля ламає ножиці, ножицями вирізає папір, папір обмотує скелею. і бінарне їсть послідовно! .. навіть для досить коротких рядків .. а що стосується помірних рядків 10000 символів, які обробляються послідовно через while char-by-char, я все ще чекаю цього, коли я це пишу .. проходить час .. все ще чекаю (можливо, щось є неправильно з моєю системою) .. час проходить .. повинно бути щось не так; це лише 10 000 повторень! Ах! терпіння - це чеснота (можливо, прокляття в цьому випадку) .. 13m53.755s .. vs, 0m0.322s
Peter.O

Три наведені тут методи - це найшвидший швидкий з усіх представлених відповідей. В основному, cmpце найшвидший (але не заснований на char). Наступна - iconvі тоді дуже поважно швидка binary-splitвідповідь. Дякую Жиллю. Мені знадобився рік, щоб дійти до цього моменту, але краще пізно, ніж ніколи. (PS. 2 друкарські режими в iconvкоді: $в =$LC_CTYPE}і \ в UTF-32) \ ) ... PPS. насправді рядок, про який я згадував вище, перевищував 10 000 символів. Це було результатом {1..10000}, що становить 48,894, але це не змінить диференціалу
Peter.O

6

У sed, припускаючи, що рядки не містять символів нового рядка:

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

Але дублювати з цим .
jfg956

Блискуче! переходить безпосередньо до моєї бібліотеки порад та рекомендацій :-)
hmontoliu

Або для рядка bash , який не може містити \0. Використовуючи trі \0, метод може обробляти нові рядки в рядку, ....{ 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

Я щойно перевірив цей sedметод трохи далі, і здається, що використання зворотних посилань таким чином (у схемі пошуку) є надзвичайно дорогим. Він все ще перевершує послідовне циклічне байт-байт (приблизно на коефіцієнт 3), але ось приклад: для двох рядків 32 кбіт (при цьому останній байт відрізняється), він займає 2m4.880sпорівняно з двійковим розщепленням Гілла метод0m0.168s
Пітер.О

2

Це здається мені грубим, але ви можете це зробити через грубу силу:

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

Я хочу, щоб існував якийсь розумний алгоритм, але я не можу знайти жодного при короткому пошуку.



2
Для загального погляду, це трохи на повільній стороні. Дві символьні струни 32768 (остання чарка відрізнялася) зайняли 6м27.689с.
Пітер.O
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.