Самый быстрый способ выполнить поиск подстроки без учета регистра в C / C++?

Примечание

вопрос ниже был задан в 2008 году в кодексе с 2003 года. Как ОП обновление показывает, что весь этот пост был устаревшим алгоритмами vintage 2008 и сохраняется здесь только как историческое любопытство.


мне нужно выполнить быстрый поиск подстроки без учета регистра в C / C++. Мои требования следующие:

  • должен вести себя как strstr() (т. е. возвращать указатель на точку соответствия).
  • должны будьте нечувствительны к регистру (doh).
  • должен поддерживать текущую локаль.
  • должен быть доступен в Windows (MSVC++ 8.0) или легко переноситься в Windows (т. е. из библиотеки с открытым исходным кодом).

вот текущая реализация, которую я использую (взято из библиотеки GNU C):

/* Return the offset of one string within another.
   Copyright (C) 1994,1996,1997,1998,1999,2000 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, write to the Free
   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307 USA.  */

/*
 * My personal strstr() implementation that beats most other algorithms.
 * Until someone tells me otherwise, I assume that this is the
 * fastest implementation of strstr() in C.
 * I deliberately chose not to comment it.  You should have at least
 * as much fun trying to understand it, as I had to write it :-).
 *
 * Stephen R. van den Berg, berg@pool.informatik.rwth-aachen.de */

/*
 * Modified to use table lookup instead of tolower(), since tolower() isn't
 * worth s*** on Windows.
 *
 * -- Anders Sandvig (anders@wincue.org)
 */

#if HAVE_CONFIG_H
# include <config.h>
#endif

#include <ctype.h>
#include <string.h>

typedef unsigned chartype;

char char_table[256];

void init_stristr(void)
{
  int i;
  char string[2];

  string[1] = '';
  for (i = 0; i < 256; i++)
  {
    string[0] = i;
    _strlwr(string);
    char_table[i] = string[0];
  }
}

#define my_tolower(a) ((chartype) char_table[a])

char *
my_stristr (phaystack, pneedle)
     const char *phaystack;
     const char *pneedle;
{
  register const unsigned char *haystack, *needle;
  register chartype b, c;

  haystack = (const unsigned char *) phaystack;
  needle = (const unsigned char *) pneedle;

  b = my_tolower (*needle); 
  if (b != '')
  {
    haystack--;             /* possible ANSI violation */
    do
      {
        c = *++haystack;
        if (c == '')
          goto ret0;
      }
    while (my_tolower (c) != (int) b);

    c = my_tolower (*++needle);
    if (c == '')
        goto foundneedle;

    ++needle;
    goto jin;

    for (;;)
    {
      register chartype a;
        register const unsigned char *rhaystack, *rneedle;

        do
        {
          a = *++haystack;
          if (a == '')
              goto ret0;
          if (my_tolower (a) == (int) b)
              break;
          a = *++haystack;
          if (a == '')
              goto ret0;
        shloop:
          ;
        }
      while (my_tolower (a) != (int) b);

jin:      
      a = *++haystack;
      if (a == '')
          goto ret0;

        if (my_tolower (a) != (int) c)
          goto shloop;

        rhaystack = haystack-- + 1;
        rneedle = needle;

        a = my_tolower (*rneedle);

        if (my_tolower (*rhaystack) == (int) a)
          do
          {
              if (a == '')
                goto foundneedle;

              ++rhaystack;
          a = my_tolower (*++needle);
              if (my_tolower (*rhaystack) != (int) a)
                break;

          if (a == '')
                goto foundneedle;

          ++rhaystack;
              a = my_tolower (*++needle);
          }
          while (my_tolower (*rhaystack) == (int) a);

        needle = rneedle;       /* took the register-poor approach */

      if (a == '')
          break;
    }
  }
foundneedle:
  return (char*) haystack;
ret0:
  return 0;
}

можете ли вы сделать этот код быстрее, или вы знаете о лучшей реализации?

Примечание: я заметил, что библиотека GNU C теперь имеет новая реализация strstr(), но я не уверен, насколько легко его можно изменить, чтобы быть нечувствительным к регистру, или если он на самом деле быстрее, чем старый (в моем случае). Я также заметил, что старая реализация по-прежнему используется для широкого символьных строк, поэтому, если кто-нибудь знает, почему, пожалуйста, поделитесь.

обновление

чтобы прояснить ситуацию-на случай, если это еще не было-я не писал эту функцию, это часть библиотеки GNU C. Я только изменил его, чтобы быть нечувствительным к регистру.

кроме того, Спасибо за совет о strcasestr() и проверка других реализаций из других источников (например, OpenBSD, FreeBSD и т. д.). Кажется, это верный путь. Код выше с 2003 года, поэтому я разместил его здесь в надежде на лучшую версию, которая, по-видимому, доступна. :)

10 ответов


код, который вы опубликовали, примерно в два раза быстрее strcasestr.

$ gcc -Wall -o my_stristr my_stristr.c
steve@solaris:~/code/tmp
$ gcc -Wall -o strcasestr strcasestr.c 
steve@solaris:~/code/tmp
$ ./bench ./my_stristr > my_stristr.result ; ./bench ./strcasestr > strcasestr.result;
steve@solaris:~/code/tmp
$ cat my_stristr.result 
run 1... time = 6.32
run 2... time = 6.31
run 3... time = 6.31
run 4... time = 6.31
run 5... time = 6.32
run 6... time = 6.31
run 7... time = 6.31
run 8... time = 6.31
run 9... time = 6.31
run 10... time = 6.31
average user time over 10 runs = 6.3120
steve@solaris:~/code/tmp
$ cat strcasestr.result 
run 1... time = 3.82
run 2... time = 3.82
run 3... time = 3.82
run 4... time = 3.82
run 5... time = 3.82
run 6... time = 3.82
run 7... time = 3.82
run 8... time = 3.82
run 9... time = 3.82
run 10... time = 3.82
average user time over 10 runs = 3.8200
steve@solaris:~/code/tmp

на


вы можете использовать функцию StrStrI, которая находит первое вхождение подстроки в строке. Сравнение не учитывает регистр. Не забудьте включить его заголовок-Shlwapi.ч. Проверьте это: http://msdn.microsoft.com/en-us/library/windows/desktop/bb773439(v=vs. 85).aspx


почему вы используете _strlwr (string); в init_stristr ()? Это не стандартная функция. Предположительно, это для поддержки локали, но поскольку это не стандарт, я бы просто использовал:

char_table[i] = tolower(i);

использовать увеличить строку algo. Он доступен, кросс-платформенный и только файл заголовка (нет библиотеки для ссылки). Не говоря уже о том, что вы должны использовать boost в любом случае.

#include <boost/algorithm/string/find.hpp>

const char* istrstr( const char* haystack, const char* needle )
{
   using namespace boost;
   iterator_range<char*> result = ifind_first( haystack, needle );
   if( result ) return result.begin();

   return NULL;
}

для независимого использования платформы:

const wchar_t *szk_wcsstri(const wchar_t *s1, const wchar_t *s2)
{
    if (s1 == NULL || s2 == NULL) return NULL;
    const wchar_t *cpws1 = s1, *cpws1_, *cpws2;
    char ch1, ch2;
    bool bSame;

    while (*cpws1 != L'')
    {
        bSame = true;
        if (*cpws1 != *s2)
        {
            ch1 = towlower(*cpws1);
            ch2 = towlower(*s2);

            if (ch1 == ch2)
                bSame = true;
        }

        if (true == bSame)
        {
            cpws1_ = cpws1;
            cpws2 = s2;
            while (*cpws1_ != L'')
            {
                ch1 = towlower(*cpws1_);
                ch2 = towlower(*cpws2);

                if (ch1 != ch2)
                    break;

                cpws2++;

                if (*cpws2 == L'')
                    return cpws1_-(cpws2 - s2 - 0x01);
                cpws1_++;
            }
        }
        cpws1++;
    }
    return NULL;
}

Я бы посоветовал вам взять некоторые из общих реализаций strcasestr, которые уже существуют. Например с GLib, glibc версии, в OpenBSD, FreeBSD и т. д. Вы можете искать больше с помощью google.com/codesearch - ... Затем можно выполнить некоторые измерения производительности и сравнить различные реализации.


предполагая, что обе входные строки уже строчные.

int StringInStringFindFirst(const char* p_cText, const char* p_cSearchText)
{
    int iTextSize = strlen(p_cText);
    int iSearchTextSize = strlen(p_cSearchText);

    char* p_cFound = NULL;

    if(iTextSize >= iSearchTextSize)
    {
        int iCounter = 0;
        while((iCounter + iSearchTextSize) <= iTextSize)
        {
            if(memcmp( (p_cText + iCounter), p_cSearchText, iSearchTextSize) == 0)
                return  iCounter;
            iCounter ++;
        }
    }

    return -1;
}

вы также можете попробовать использовать маски... если, например, большинство строк, которые вы собираетесь сравнить, содержат только символы от a до z, возможно, стоит сделать что-то вроде этого.

long GetStringMask(const char* p_cText)
{
    long lMask=0;

    while(*p_cText != '')
    {       
        if (*p_cText>='a' && *p_cText<='z')
            lMask = lMask | (1 << (*p_cText - 'a') );
        else if(*p_cText != ' ')
        {
            lMask = 0;
            break;      
        }

        p_cText ++;
    }
    return lMask;
}

затем...

int main(int argc, char* argv[])
{

    char* p_cText = "this is a test";   
    char* p_cSearchText = "test";

    long lTextMask = GetStringMask(p_cText);
    long lSearchMask = GetStringMask(p_cSearchText);

    int iFoundAt = -1;
    // If Both masks are Valid
    if(lTextMask != 0 && lSearchMask != 0)
    {
        if((lTextMask & lSearchMask) == lSearchMask)
        {       
             iFoundAt = StringInStringFindFirst(p_cText, p_cSearchText);
        }
    }
    else
    {
        iFoundAt = StringInStringFindFirst(p_cText, p_cSearchText);
    }


    return 0;
}

это не будет учитывать языковой стандарт, но если вы можете изменить IS_ALPHA и TO_UPPER, вы можете заставить его рассмотреть его.

#define IS_ALPHA(c) (((c) >= 'A' && (c) <= 'Z') || ((c) >= 'a' && (c) <= 'z'))
#define TO_UPPER(c) ((c) & 0xDF)

char * __cdecl strstri (const char * str1, const char * str2){
        char *cp = (char *) str1;
        char *s1, *s2;

        if ( !*str2 )
            return((char *)str1);

        while (*cp){
                s1 = cp;
                s2 = (char *) str2;

                while ( *s1 && *s2 && (IS_ALPHA(*s1) && IS_ALPHA(*s2))?!(TO_UPPER(*s1) - TO_UPPER(*s2)):!(*s1-*s2))
                        ++s1, ++s2;

                if (!*s2)
                        return(cp);

                ++cp;
        }
        return(NULL);
}

Если вы хотите пролить циклы процессора, вы можете рассмотреть это - предположим, что мы имеем дело с ASCII, а не с Unicode.

создайте статическую таблицу с 256 записями. Каждая запись в таблице-256 бит.

чтобы проверить, равны ли два символа, вы делаете что-то вроде этого:

if (BitLookup(table[char1], char2)) { /* match */ }

чтобы построить таблицу, вы устанавливаете бит везде в таблице[char1], где вы считаете, что он соответствует char2. Таким образом, при построении таблицы вы бы установили биты на индекс для " А " и " А " В " а "- й записи (и " А " - й записи).

Теперь это будет медленным, чтобы сделать поиск битов (бит lookup будет сдвиг, Маска и добавить, скорее всего), так что вы можете использовать вместо таблицы байтов, так что вы используете 8 бит для представления 1 бит. Это займет 32K-так ура-вы попали время / пространство компромисс! Возможно, мы захотим сделать таблицу более гибкой, поэтому предположим, что вместо этого мы сделаем это - таблица определит конгруэнции.

два символы считаются конгруэнтными тогда и только тогда, когда есть функция, которая определяет их как эквивалентные. Так что 'A' и 'A' равны для регистра. 'А', 'À', 'Á" и "Â' равны чувствительноти диакритические.

таким образом, вы определяете битовые поля, соответствующие вашим конгруэнциям

#define kCongruentCase (1 << 0)
#define kCongruentDiacritical (1 << 1)
#define kCongruentVowel (1 << 2)
#define kCongruentConsonant (1 << 3)

тогда ваш тест выглядит примерно так:

inline bool CharsAreCongruent(char c1, char c2, unsigned char congruency)
{
    return (_congruencyTable[c1][c2] & congruency) != 0;
}

#define CaseInsensitiveCharEqual(c1, c2) CharsAreCongruent(c1, c2, kCongruentCase)

этот вид бит возиться с гигантскими таблицами является сердцем ctype, кстати.


Если вы можете управлять строкой иглы, чтобы она всегда была в нижнем регистре, то вы можете написать измененную версию stristr (), чтобы избежать поиска для этого, и, таким образом, ускорить код. Он не такой общий, но может быть быстрее - немного быстрее. Аналогичные комментарии относятся к стогу сена, но вы, скорее всего, будете читать стог сена из источников вне вашего контроля, поскольку вы не можете быть уверены, что данные соответствуют требованию.

ли увеличение в представлении стоит это совсем другого вопроса. Для 99% приложений, ответ "Нет, не стоит". Ваше приложение может быть одним из крошечного меньшинства, где это имеет значение. Скорее всего, нет.