Có bất kỳ thuật toán tìm kiếm tốt cho một nhân vật?


23

Tôi biết một số thuật toán khớp chuỗi cơ bản như KMP hoặc Boyer-Moore, nhưng tất cả chúng đều phân tích mẫu trước khi tìm kiếm. Tuy nhiên, nếu một trong số đó có một ký tự, thì không có nhiều thứ để phân tích. Vì vậy, có thuật toán nào tốt hơn tìm kiếm ngây thơ so sánh mọi ký tự của văn bản?


13
Bạn có thể ném các hướng dẫn SIMD vào nó, nhưng bạn sẽ không nhận được gì tốt hơn O (n).
CodeInChaos

7
Đối với một tìm kiếm hoặc nhiều tìm kiếm trong cùng một chuỗi?
Christophe

KMP chắc chắn không phải là thứ mà tôi sẽ gọi là thuật toán khớp chuỗi "cơ bản" ... Tôi thậm chí không chắc nó nhanh như vậy, nhưng nó quan trọng về mặt lịch sử. Nếu bạn muốn một cái gì đó cơ bản hãy thử thuật toán Z.
Mehrdad

Giả sử có một vị trí ký tự mà thuật toán tìm kiếm không nhìn vào. Sau đó, nó sẽ không thể phân biệt giữa các chuỗi với ký tự kim ở vị trí đó và các chuỗi có một ký tự khác ở vị trí đó.
dùng253751

Câu trả lời:


29

Nó được hiểu rằng trường hợp xấu nhất là O(N), có một số tối ưu hóa vi mô rất tốt đẹp.

Phương pháp ngây thơ thực hiện so sánh ký tự và so sánh cuối văn bản cho mỗi ký tự.

Sử dụng một sentinel (tức là một bản sao của ký tự đích ở cuối văn bản) làm giảm số lượng so sánh xuống 1 trên mỗi ký tự.

Ở cấp độ twiddling có:

#define haszero(v)      ( ((v) - 0x01010101UL) & ~(v) & 0x80808080UL )
#define hasvalue(x, n)  ( haszero((x) ^ (~0UL / 255 * (n))) )

để biết nếu bất kỳ byte nào trong một từ ( x) có một giá trị cụ thể ( n).

Biểu thức con v - 0x01010101UL, ước tính thành một bit cao được đặt trong bất kỳ byte nào mỗi khi byte tương ứng vbằng 0 hoặc lớn hơn 0x80.

Biểu thức con ~v & 0x80808080ULước tính các bit cao được đặt theo byte trong đó byte của vkhông có tập bit cao (do đó byte nhỏ hơn 0x80).

Bằng cách ANDing hai biểu thức con ( haszero) này, kết quả là các bit cao được đặt trong đó các byte vbằng 0, vì các bit cao được đặt do giá trị lớn hơn 0x80trong biểu thức phụ đầu tiên bị che đi bởi lần thứ hai (27 tháng 4, 1987 bởi Alan Mycroft).

Bây giờ chúng ta có thể XOR giá trị để kiểm tra ( x) bằng một từ đã được điền với giá trị byte mà chúng ta quan tâm ( n). Bởi vì XORing một giá trị với chính nó dẫn đến một byte bằng 0 và khác không, chúng ta có thể chuyển kết quả đến haszero.

Điều này thường được sử dụng trong một strchrthực hiện điển hình .

(Stephen M Bennet đã đề xuất điều này vào ngày 13 tháng 12 năm 2009. Thông tin chi tiết khác trong Hacker Twiddling nổi tiếng ).


PS

mã này bị hỏng cho bất kỳ sự kết hợp nào 1111bên cạnh một0

Bản hack vượt qua bài kiểm tra sức mạnh vũ phu (chỉ cần kiên nhẫn):

#include <iostream>
#include <limits>

bool haszero(std::uint32_t v)
{
  return (v - std::uint32_t(0x01010101)) & ~v & std::uint32_t(0x80808080);
}

bool hasvalue(std::uint32_t x, unsigned char n)
{
  return haszero(x ^ (~std::uint32_t(0) / 255 * n));
}

bool hasvalue_slow(std::uint32_t x, unsigned char n)
{
  for (unsigned i(0); i < 32; i += 8)
    if (((x >> i) & 0xFF) == n)
      return true;

  return false;
}

int main()
{
  const std::uint64_t stop(std::numeric_limits<std::uint32_t>::max());

  for (unsigned c(0); c < 256; ++c)
  {
    std::cout << "Testing " << c << std::endl;

    for (std::uint64_t w(0); w != stop; ++w)
    {
      if (w && w % 100000000 == 0)
        std::cout << w * 100 / stop << "%\r" << std::flush;

      const bool h(hasvalue(w, c));
      const bool hs(hasvalue_slow(w, c));

      if (h != hs)
        std::cerr << "hasvalue(" << w << ',' << c << ") is " << h << '\n';
    }
  }

  return 0;
}

Rất nhiều sự ủng hộ cho một câu trả lời làm cho giả định một chararacter = một byte, ngày nay không còn là tiêu chuẩn nữa

Cảm ơn bạn đã nhận xét.

Câu trả lời có nghĩa là bất cứ điều gì ngoại trừ một bài tiểu luận về mã hóa nhiều byte / độ rộng biến đổi :-) (trong tất cả các công bằng không phải là lĩnh vực chuyên môn của tôi và tôi không chắc đó là những gì OP đang tìm kiếm).

Dù sao, đối với tôi, các ý tưởng / thủ thuật trên có thể phần nào được điều chỉnh theo MBE (đặc biệt là mã hóa tự đồng bộ hóa ):

  • như đã lưu ý trong nhận xét của Johan, hack có thể 'dễ dàng' được mở rộng để hoạt động với hai byte hoặc bất cứ thứ gì (tất nhiên bạn không thể kéo dài nó quá nhiều);
  • một chức năng điển hình định vị một ký tự trong chuỗi ký tự đa nhân:
    • chứa các cuộc gọi đến strchr/ strstr(ví dụ: GNUlib coreutils mbschr )
    • hy vọng chúng sẽ được điều chỉnh tốt.
  • kỹ thuật sentinel có thể được sử dụng với một chút tầm nhìn xa.

1
Đây là phiên bản hoạt động của SIMD dành cho người nghèo.
Ruslan

@Ruslan Tuyệt đối! Điều này thường là trường hợp cho hack hack twiddling hiệu quả.
manlio

2
Câu trả lời tốt đẹp. Từ khía cạnh dễ đọc, tôi không hiểu tại sao bạn viết 0x01010101ULtrong một dòng và ~0UL / 255tiếp theo. Nó cho ấn tượng rằng chúng phải là các giá trị khác nhau, vì nếu không, tại sao lại viết nó theo hai cách khác nhau?
hvd

3
Điều này thật tuyệt vì nó kiểm tra 4 byte cùng một lúc, nhưng nó yêu cầu nhiều hướng dẫn (8?), Vì #defines sẽ mở rộng thành ( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL ). Không phải so sánh byte đơn sẽ nhanh hơn?
Jed Schaaf

1
@DocBrown, mã có thể dễ dàng được tạo để hoạt động cho các byte kép (tức là một nửa) hoặc nibble hoặc bất cứ thứ gì. (có tính đến cảnh báo tôi đã đề cập).
Johan - phục hồi Monica

20

Bất kỳ thuật toán tìm kiếm văn bản nào tìm kiếm mọi lần xuất hiện của một ký tự trong một văn bản đã cho đều phải đọc từng ký tự của văn bản ít nhất một lần, điều đó là hiển nhiên. Và vì điều này là đủ cho tìm kiếm một lần, nên không thể có thuật toán nào tốt hơn (khi suy nghĩ theo thứ tự thời gian chạy, được gọi là "tuyến tính" hoặc O (N) cho trường hợp này, trong đó N là số ký tự để tìm kiếm thông qua).

Tuy nhiên, đối với việc triển khai thực tế, chắc chắn có rất nhiều tối ưu hóa vi mô có thể, không thay đổi thứ tự thời gian chạy một cách tổng thể, nhưng giảm thời gian chạy thực tế. Và nếu mục tiêu không phải là tìm thấy mọi sự xuất hiện của một nhân vật, mà chỉ là lần đầu tiên, bạn có thể dừng lại ở lần xuất hiện đầu tiên, tất nhiên. Tuy nhiên, ngay cả trong trường hợp đó, trường hợp xấu nhất vẫn là nhân vật bạn đang tìm kiếm là nhân vật cuối cùng trong văn bản, vì vậy thứ tự thời gian chạy trường hợp xấu nhất cho mục tiêu này vẫn là O (N).


8

Nếu "haystack" của bạn được tìm kiếm nhiều lần, một cách tiếp cận dựa trên biểu đồ sẽ cực kỳ nhanh chóng. Sau khi biểu đồ được xây dựng, bạn chỉ cần tra cứu con trỏ để tìm câu trả lời.

Nếu bạn chỉ cần biết liệu mẫu tìm kiếm có mặt hay không, một bộ đếm đơn giản có thể giúp ích. Nó có thể được mở rộng để bao gồm (các) vị trí mà tại đó mỗi ký tự được tìm thấy trong đống cỏ khô, hoặc vị trí của lần xuất hiện đầu tiên.

string haystack = "agtuhvrth";
array<int, 256> histogram{0};
for(character: haystack)
     ++histogram[character];

if(histogram['a'])
    // a belongs to haystack

1

Nếu bạn cần tìm kiếm các ký tự trong cùng một chuỗi nhiều lần, thì cách tiếp cận có thể là chia chuỗi thành các phần nhỏ hơn, có thể theo cách đệ quy và sử dụng các bộ lọc nở cho mỗi phần này.

Vì bộ lọc nở có thể cho bạn biết chắc chắn nếu một ký tự không nằm trong phần của chuỗi được "đại diện" bởi bộ lọc, bạn có thể bỏ qua một số phần trong khi tìm kiếm ký tự.

Ví dụ: Đối với chuỗi sau đây, người ta có thể chia nó thành 4 phần (mỗi phần dài 11 ký tự) và điền vào mỗi phần một bộ lọc nở (có thể lớn 4 byte) bằng các ký tự của phần đó:

The quick brown fox jumps over the lazy dog 
          |          |          |          |

Bạn có thể tăng tốc tìm kiếm của mình, ví dụ như cho nhân vật a: Sử dụng các hàm băm tốt cho các bộ lọc nở, họ sẽ nói với bạn rằng - với xác suất cao - bạn không phải tìm kiếm ở cả phần thứ nhất, thứ hai hay thứ ba. Do đó, bạn tự cứu mình khỏi việc kiểm tra 33 ký tự và thay vào đó chỉ phải kiểm tra 16 byte (đối với 4 bộ lọc nở). Đây vẫn O(n)là một yếu tố (phân số) không đổi (và để điều này có hiệu quả, bạn sẽ cần chọn các phần lớn hơn, để giảm thiểu chi phí tính toán hàm băm cho ký tự tìm kiếm).

Sử dụng một cách tiếp cận đệ quy, giống như cây sẽ đưa bạn đến gần O(log n):

The quick brown fox jumps over the lazy dog 
   |   |   |   |   |   |   |   |---|-X-|   |  (1 Byte)
       |       |       |       |---X---|----  (2 Byte)
               |               |-----X------  (3 Byte)
-------------------------------|-----X------  (4 Byte)
---------------------X---------------------|  (5 Byte)

Trong cấu hình này, một nhu cầu (một lần nữa, giả sử chúng tôi đã gặp may và không nhận được kết quả dương tính giả từ một trong các bộ lọc) để kiểm tra

5 + 2*4 + 3 + 2*2 + 2*1 bytes

để đến phần cuối cùng (trong đó người ta cần kiểm tra 3 ký tự cho đến khi tìm thấy a).

Sử dụng sơ đồ phân chia tốt (tốt hơn như trên), bạn sẽ nhận được kết quả khá tốt với điều đó. (Lưu ý: Bộ lọc Bloom ở gốc cây phải lớn hơn gần với lá, như trong ví dụ, để có xác suất dương tính giả thấp)


Kính gửi downvoter, vui lòng giải thích lý do tại sao bạn nghĩ rằng câu trả lời của tôi không hữu ích.
Daniel Jour

1

Nếu chuỗi sẽ được tìm kiếm nhiều lần (vấn đề "tìm kiếm" điển hình), giải pháp có thể là O (1). Giải pháp là xây dựng một chỉ số.

Ví dụ :

Bản đồ, trong đó Key là Ký tự và Giá trị là danh sách các chỉ mục cho ký tự đó trong chuỗi.

Với điều này, một tra cứu bản đồ duy nhất có thể cung cấp câu trả lời.

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.