단일 문자에 적합한 검색 알고리즘이 있습니까?


23

KMP 또는 Boyer-Moore와 같은 몇 가지 기본 문자열 일치 알고리즘을 알고 있지만 모두 검색하기 전에 패턴을 분석하지만 하나의 문자가 있으면 분석 할 것이 많지 않습니다. 텍스트의 모든 문자를 비교하는 순진한 검색보다 더 나은 알고리즘이 있습니까?


13
SIMD 명령을 던질 수는 있지만 O (n)보다 나은 것은 없습니다.
코드 InChaos

7
동일한 문자열에서 단일 검색 또는 여러 검색?
Christophe

KMP는 필자가 "기본"문자열 일치 알고리즘이라고 부르는 것이 아닙니다. 그렇게 빠르지도 확실하지 않지만 역사적으로 중요합니다. 기본적인 것을 원한다면 Z 알고리즘을 사용해보십시오.
Mehrdad

검색 알고리즘이 보지 않은 문자 위치가 있다고 가정하십시오. 그런 다음 해당 위치에 바늘 문자가있는 문자열과 해당 위치에 다른 문자가있는 문자열을 구분할 수 없습니다.
user253751

답변:


29

최악의 경우는 O(N)매우 미세한 미세 최적화 가 있음을 이해합니다 .

순진한 방법은 각 문자에 대한 문자 비교 및 ​​텍스트 끝 비교를 수행합니다.

사용 감시 (텍스트의 끝에서 대상 문자의 예를 복사하는) 문자 당 1 비교의 수를 줄일 수 있습니다.

비트 트위들 링 레벨에는 다음이 있습니다.

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

단어 ( x)의 바이트 가 특정 값 ( n)을 갖는지 알기 위해 .

하위 표현식 v - 0x01010101UL은 해당 바이트 in v이 0 이상일 때마다 모든 바이트에서 높은 비트 세트로 평가됩니다 0x80.

하위 표현식 ~v & 0x80808080UL은 바이트 수가 높은 비트 세트를 v갖지 않는 바이트 단위로 높은 비트 세트로 평가됩니다 (따라서 바이트는보다 작음 0x80).

이 두 하위 표현식 ( haszero) 을 AND 처리 하면 첫 번째 하위 표현식 v보다 큰 값으로 설정된 상위 비트 0x80가 두 번째 하위 마스크에 의해 가려 지기 때문에 결과는 바이트 가 0 인 상위 비트 세트입니다 (4 월 27 일, 1987 년 Alan Mycroft).

이제 x원하는 바이트 값으로 채워진 단어 로 테스트 할 값 ( ) 을 XOR 할 수 있습니다 ( n). 값 자체를 XOR하면 0 바이트가되고 그렇지 않으면 0이 아니므로 결과를로 전달할 수 있습니다 haszero.

이것은 종종 전형적인 strchr구현 에서 사용됩니다 .

(Stephen M Bennet은 2009 년 12 월 13 일에이를 제안했습니다. Bit Twiddling Hacks ).


추신

이 코드의 조합 깨진 1111A와 다음의 '0

해킹은 무차별 대입 테스트를 통과합니다 (인내심).

#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;
}

하나의 chararacter = 1 바이트로 가정하는 대답에 대한 많은 공감대가 이제는 표준이 아닙니다.

발언 감사합니다.

대답은 멀티 바이트 / 가변 폭 인코딩에 대한 에세이 이외의 것입니다 :-) (모든 공정성에서 내 전문 분야가 아니며 OP가 찾고있는 것이 확실하지 않습니다).

어쨌든 위의 아이디어 / 틱은 MBE (특히 자체 동기화 인코딩 )에 다소 적용될 수있는 것 같습니다 .

  • 요한의 의견 에서 언급했듯이 에서 알 수 해킹은 더블 바이트 또는 기타 작업을 위해 '쉽게'확장 될 수 있습니다 (물론 너무 많이 늘릴 수는 없습니다).
  • 멀티 바이트 문자열에서 문자를 찾는 일반적인 함수 :
    • strchr/에 대한 호출을 포함합니다 strstr(예 : GNUlib coreutils mbschr )
    • 잘 조정될 것으로 기대합니다.
  • 센티넬 기술은 약간의 예측으로 사용될 수 있습니다.

1
이것은 가난한 사람의 SIMD 작업 버전입니다.
Ruslan

@Ruslan 절대적으로! 효과적인 비트 트위들 링 해킹의 경우가 종종 있습니다.
manlio

2
좋은 대답입니다. 가독성 측면 0x01010101UL에서 한 줄과 ~0UL / 255다음 줄에 글을 쓰는 이유를 이해하지 못합니다 . 서로 다른 가치를 가져야한다는 인상을줍니다. 그렇지 않으면 왜 두 가지 다른 방식으로 씁니까?
hvd

3
한 번에 4 바이트를 확인하기 때문에 멋지지만 #defines로 확장 되므로 여러 (8?) 명령이 필요 합니다 ( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL ). 싱글 바이트 비교가 더 빠르지 않습니까?
Jed Schaaf

1
@DocBrown, 코드는 더블 바이트 (즉, 하프 워드) 나 니블 (nibble) 등에서 작동하도록 쉽게 만들 수 있습니다. (내가 언급 한 경고를 고려하여).
Johan-Monica Monica

20

주어진 텍스트에서 단일 문자의 모든 발생을 검색하는 모든 텍스트 검색 알고리즘은 텍스트의 각 문자를 적어도 한 번 읽어야합니다. 이 방법은 일회성 검색에 충분하므로 더 나은 알고리즘을 사용할 수 없습니다 (이 경우에는 "선형"또는 O (N)이라고하는 런타임 순서 측면에서 생각할 때 N은 문자 수임) 검색).

그러나 실제 구현의 경우 실행 시간 순서를 전체적으로 변경하지 않고 실제 실행 시간을 줄이는 미세 최적화가 많이 가능합니다. 그리고 목표가 한 인물의 모든 사건을 찾는 것이 아니라 첫 번째 사건만을 찾는 것이라면 물론 첫 사건에서 멈출 수 있습니다. 그럼에도 불구하고, 그 경우에도 최악의 경우는 여전히 찾고있는 문자가 텍스트의 마지막 문자 이므로이 목표의 최악의 런타임 순서는 여전히 O (N)입니다.


8

"haystack"을 두 번 이상 검색하면 히스토그램 기반 접근 방식이 매우 빠릅니다. 히스토그램이 작성된 후에는 답을 찾기 위해 포인터 검색 만 필요합니다.

검색된 패턴이 있는지 여부 만 알면 간단한 카운터가 도움이 될 수 있습니다. 각 문자가 건초 더미에서 발견되는 위치 또는 첫 번째 발생 위치를 포함하도록 확장 될 수 있습니다.

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

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

1

이 매우 동일한 문자열에서 문자를 두 번 이상 검색해야하는 경우 문자열을 더 작은 부분으로, 재귀 적으로 나누고, 각 부분에 대해 블룸 필터를 사용하는 것이 가능합니다.

블룸 필터는 캐릭터가 아닌지 확실히 알려줄 수 있기 때문에 필터를 "표현"는 무엇 문자열의 일부에 문자를 검색하는 동안, 당신은 몇 가지 부분을 건너 뛸 수 있습니다.

예를 들어 : 다음 문자열의 경우 4 개의 부분 (각 11 자 길이)으로 분할하고 각 부분에 해당 부분의 문자로 블룸 필터 (아마도 4 바이트)를 채울 수 있습니다.

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

예를 들어 캐릭터에 대한 검색 속도를 높일 수 있습니다 a. 블룸 필터에 좋은 해시 함수를 사용하면 높은 확률로 첫 번째, 두 번째 또는 세 번째 부분을 모두 검색 할 필요가 없습니다. 따라서 33 문자를 확인하지 않고 16 바이트 만 검사하면됩니다 (4 개의 블룸 필터). 이것은 여전히O(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)

이 구성에서 (다시, 우리가 운이 좋으며 필터 중 하나에서 오 탐지를 얻지 않았다고 가정) 다시 확인해야합니다.

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

마지막 부분에 도착하기 위해 (찾을 때까지 3 문자를 확인해야합니다. a ).

좋은 (상기보다 나은) 세분화 체계를 사용하면 꽤 좋은 결과를 얻을 수 있습니다. (참고 : 예에 표시된 것처럼 오 탐지 확률이 낮도록 나무의 루트에있는 블룸 필터가 나뭇잎에 근접한 것보다 커야합니다.)


다운 보더 여러분, 왜 저의 대답이 도움이되지 않는다고 생각하는지 설명해주십시오.
Daniel Jour

1

문자열을 여러 번 검색 할 경우 (일반적인 "검색"문제) 해결책은 O (1) 일 수 있습니다. 해결책은 색인을 작성하는 것입니다.

예 :

Map. 여기서 Key는 문자이고 Value는 문자열에서 해당 문자의 색인 목록입니다.

이를 통해 단일 맵 조회가 답을 제공 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.