Làm thế nào để tôi tìm thấy sự chồng chéo của hai chuỗi trong bash? [đóng cửa]


11

Tôi có hai chuỗi. Vì lợi ích của ví dụ, chúng được đặt như thế này:

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

Điều tôi muốn là tìm sự trùng lặp bắt đầu từ đầu chuỗi. Với sự chồng chéo tôi có nghĩa là chuỗi "test t" trong ví dụ trên của tôi.

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

Nếu các chuỗi là string1="atest toast"; string2="test test"chúng sẽ không có sự trùng lặp vì kiểm tra bắt đầu hình thành từ đầu và "a" khi bắt đầu string1.



Đây chính xác là lý do mọi người không nên đăng bài chéo; bây giờ nó có nhiều câu trả lời trên mỗi trang web khác nhau và đó là chủ đề cho cả hai trang web. Tôi nghĩ rằng dù sao tôi cũng sẽ để nó ở đây
Michael Mrozek

Câu trả lời:


10

Bạn có thể nghĩ về một chức năng như thế này, với một số kiểm tra lỗi để thêm

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

Tôi chỉ nhận thấy rằng khi chạy với hai đối số rỗng / null, nó đi vào một vòng ∞. [[ -z "$1$2" ]] && returnsửa nó
Peter.O

Phương pháp này chậm hơn theo cấp số nhân (thay vì tuyến tính). Khi chuỗi tăng gấp đôi chiều dài, thời gian tăng theo hệ số 4 (xấp xỉ). Dưới đây là một số so sánh chuỗi dài / giờ để Gilles' nhị phân-chia : .. 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 so với 0m0.009s - 2048 0m1.575s so với 0m0.012s - 4096 0m5.967s so với 0m0.022s - 8192 0m24.693s so với 0m0.049s -16384 1m34.004s so với 0m0.085s - 32768 6m34.721s so với 0m0.168s - 65536 27m34.012s so với 0m0.370s
Peter.O

2
@ Peter.O bậc hai, không theo cấp số nhân.
Gilles 'SO- ngừng trở nên xấu xa'

Tôi đoán bash lưu trữ các chuỗi bên trong với độ dài ngầm định, do đó, việc nhận được nký tự thứ yêu cầu quét các nký tự để kiểm tra xem chúng có phải là byte không kết thúc chuỗi không. Điều này phù hợp với bash không thể lưu trữ byte 0 trong một biến.
Peter Cordes

8

Điều này có thể được thực hiện hoàn toàn bên trong bash. Mặc dù thực hiện thao tác chuỗi trong một vòng lặp trong bash rất chậm, nhưng có một thuật toán đơn giản là logarit trong số lượng các thao tác shell, vì vậy bash thuần là một lựa chọn khả thi ngay cả đối với các chuỗi dài.

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

Hộp công cụ tiêu chuẩn bao gồm cmpđể so sánh các tệp nhị phân. Theo mặc định, nó chỉ ra độ lệch byte của các byte khác nhau đầu tiên. Có một trường hợp đặc biệt khi một chuỗi là tiền tố của chuỗi khác: cmptạo ra một thông báo khác trên STDERR; một cách dễ dàng để giải quyết vấn đề này là lấy chuỗi nào ngắn nhất.

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

Lưu ý rằng cmphoạt động trên byte, nhưng thao tác chuỗi của bash hoạt động trên các ký tự. Điều này tạo ra sự khác biệt về các địa điểm đa bào, ví dụ như các địa điểm sử dụng bộ ký tự UTF-8. Hàm trên in tiền tố dài nhất của chuỗi byte. Để xử lý các chuỗi ký tự bằng phương thức này, trước tiên chúng ta có thể chuyển đổi các chuỗi thành mã hóa có chiều rộng cố định. Giả sử bộ ký tự của miền địa phương là một tập hợp con của Unicode, UTF-32 phù hợp với hóa đơn.

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

Xem lại câu hỏi này (1 năm trở đi), tôi đã đánh giá lại câu trả lời hay nhất . Tất cả đều khá đơn giản: đá phá vỡ kéo, kéo cắt giấy, giấy bọc đá. và nhị phân ăn tuần tự! .. ngay cả đối với các chuỗi khá ngắn .. và đối với chuỗi 10000 char vừa phải được xử lý tuần tự while char-by-char, tôi vẫn chờ nó khi tôi viết điều này .. thời gian trôi qua .. vẫn đang chờ đợi (có thể có gì đó sai với hệ thống của tôi) .. thời gian trôi qua .. phải có điều gì đó sai; nó chỉ có 10.000 lần lặp! Ah! kiên nhẫn là một đức tính (có lẽ là một lời nguyền trong trường hợp này) .. 13m53.755s .. vs, 0m0.322s
Peter.O

3 phương pháp được đưa ra ở đây là nhanh nhất trong tất cả các câu trả lời được trình bày .. Về cơ bản, cmplà nhanh nhất (nhưng không dựa trên char). Tiếp theo là iconvvà sau đó là câu trả lời rất nhanh đáng kính trọng binary-split. Cảm ơn Gilles. Tôi mất một năm để đi đến điểm này, nhưng muộn còn hơn không. (PS. 2 mod đánh máy trong iconvmã: $trong =$LC_CTYPE}\ trong UTF-32) \ ) ... PPS. Trên thực tế, chuỗi tôi đã đề cập ở trên dài hơn 10.000 ký tự. Đó là kết quả của {1..10000}, là 48.894, nhưng điều đó không 'thay đổi sự khác biệt
Peter.O

6

Trong sed, giả sử các chuỗi không chứa bất kỳ ký tự dòng mới nào:

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

Nhưng trùng lặp với điều này .
jfg956

Xuất sắc! truy cập trực tiếp vào thư viện mẹo & thủ thuật của tôi :-)
hmontoliu

Hoặc, đối với chuỗi bash , không thể chứa \0. Sử dụng tr\0, phương thức có thể xử lý các dòng mới trong chuỗi, ....{ 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

Tôi vừa mới thử nghiệm sedphương pháp này một chút nữa và dường như việc sử dụng các tham chiếu ngược theo cách này (trong mẫu tìm kiếm) cực kỳ tốn kém. Nó vẫn vượt trội so với vòng lặp byte theo tuần tự (theo hệ số xấp xỉ 3), nhưng đây là một ví dụ: đối với hai chuỗi 32kb (với byte cuối cùng là khác nhau), 2m4.880sso với phân chia nhị phân của Gilles phương pháp0m0.168s
Peter.O

2

Điều này có vẻ thô thiển với tôi, nhưng bạn có thể làm điều đó thông qua sức mạnh vũ phu:

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

Tôi muốn một số thuật toán thông minh tồn tại, nhưng tôi không thể tìm thấy bất kỳ với một tìm kiếm ngắn.



2
Để tham khảo chung, nó là một chút về phía chậm. Hai chuỗi ký tự 32768 (char cuối cùng khác nhau) mất 6m27.689.
Peter.O
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.