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

Примечание

Приведенный ниже вопрос был задан в 2008 году о некотором коде из 2003 года. Как показывает ОП Обновить, весь этот пост был устаревшим алгоритмами 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, [email protected] */

/*
 * Modified to use table lookup instead of tolower(), since tolower() isn't
 * worth s*** on Windows.
 *
 * -- Anders Sandvig ([email protected])
 */

#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] = '\0';
  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 != '\0')
  {
    haystack--;             /* possible ANSI violation */
    do
      {
        c = *++haystack;
        if (c == '\0')
          goto ret0;
      }
    while (my_tolower (c) != (int) b);

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

    ++needle;
    goto jin;

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

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

jin:      
      a = *++haystack;
      if (a == '\0')
          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 == '\0')
                goto foundneedle;

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

          if (a == '\0')
                goto foundneedle;

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

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

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

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

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

Обновлять

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

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

Этот совет не поможет, но вы должны по крайней мере очистить весь ненужный код, например, код, который вы пропускаете с помощью оператора goto jin.

Lasse V. Karlsen 17.10.2008 14:16

... и меня не очень впечатляет ваше отсутствие навыков чтения. Я не писал этот код, как указано как в комментариях к исходному коду, так и в моем примечании ниже.

Anders Sandvig 17.10.2008 16:12

Я хотел узнать, знают ли люди о более быстрых способах поиска подстроки без учета регистра - потому что мне это нужно для обеспечения быстрого поиска в моей программе - и, как оказалось, теперь доступен более быстрый strcasestr ().

Anders Sandvig 17.10.2008 16:14

В MSVC++ есть функция под названием 'StrStrI', см. msdn.microsoft.com/en-us/library/windows/desktop/…

Omtara 23.06.2014 17:33
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
9
4
35 753
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

char_table[i] = tolower(i);

Это специальная функция для правильной обработки настроек локали. Он специфичен для Windows, как и приложение, в котором он используется, поэтому в то время проблем с переносимостью не было (см. msdn.microsoft.com/en-us/library/hkxwh33z(VS.71).aspx).

Anders Sandvig 17.10.2008 13:51

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

Ответ принят как подходящий

Опубликованный вами код примерно вдвое медленнее, чем 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

Функция main была:

int main(void)
{
        char * needle = "hello";
        char haystack[1024];
        int i;

        for(i=0;i<sizeof(haystack)-strlen(needle)-1;++i)
        {
                haystack[i]='A'+i%57;
        }
        memcpy(haystack+i,needle, strlen(needle)+1);
        /*printf("%s\n%d\n", haystack, haystack[strlen(haystack)]);*/
        init_stristr();

        for (i=0;i<1000000;++i)
        {
                /*my_stristr(haystack, needle);*/
                strcasestr(haystack,needle);
        }


        return 0;
}

Он был соответствующим образом модифицирован для тестирования обеих реализаций. Я замечаю, что, набирая это, я оставил вызов init_stristr, но это не должно сильно изменить ситуацию. bench - это простой сценарий оболочки:

#!/bin/bash
function bc_calc()
{
        echo $(echo "scale=4;$1" | bc)
}
time = "/usr/bin/time -p"
prog = "$1"
accum=0
runs=10
for a in $(jot $runs 1 $runs)
do
        echo -n "run $a... "
        t=$($time $prog 2>&1| grep user | awk '{print $2}')
        echo "time = $t"
        accum=$(bc_calc "$accum+$t")
done

echo -n "average user time over $runs runs = "
echo $(bc_calc "$accum/$runs")

Спасибо за сравнение. Это очень интересно. Мой код написан в 2003 году, когда strcasstr ​​() не существовало - или, по крайней мере, я не знал об этом (он был добавлен в 2005 году в соответствии с историей CVS glibc). Кажется, что strcasestr () не поддерживается MSVC++, но, возможно, я смогу перенести его из glibc.

Anders Sandvig 17.10.2008 16:07

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

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 != '\0')
    {       
        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;
}

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

Anders Sandvig 17.10.2008 16:17

Кроме того, если обе строки имеют одинаковый регистр, вы можете просто использовать strstr () ...;)

Anders Sandvig 17.10.2008 16:18

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

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

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

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

Чтобы построить таблицу, вы устанавливаете бит везде в таблице [char1], где считаете, что это соответствует char2. Таким образом, при построении таблицы вы должны установить биты по индексу для «a» и «A» в «a» -й записи (и «A» -й записи).

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

Два символа считаются совпадающими тогда и только тогда, когда существует функция, определяющая их как эквивалентные. Таким образом, «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.

Если вы имеете дело с ASCII, вам нужно всего 128 записей. ASCII останавливается на 127, в отличие от байтов. Вот почему существует 500 расширений ASCII. Не то чтобы это действительно важно, это 2008 год, и сейчас мир использует Unicode.

MSalters 17.10.2008 17:59

используйте алгоритм усиления строки. Он доступен, кроссплатформенный, и только файл заголовка (без библиотеки для ссылки). Не говоря уже о том, что вы все равно должны использовать 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;
}

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

Стоит ли прибавка в производительности - это вообще вопрос. На 99% приложений ответ «Нет, не стоит». Ваше приложение может быть одним из крошечного меньшинства, когда оно имеет значение. Скорее всего, это не так.

Это не будет учитывать языковой стандарт, но если вы можете изменить 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);
}

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

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

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'\0')
    {
        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'\0')
            {
                ch1 = towlower(*cpws1_);
                ch2 = towlower(*cpws2);

                if (ch1 != ch2)
                    break;

                cpws2++;

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

это работает для меня, спасибо. Просто интересно, есть ли встроенная функция, предоставляемая C++ или c для этого.

J.Doe 27.03.2019 19:20

Я знаю wcsstr, но он просто нечувствителен к регистру, у меня есть решение преобразовать все в нижний регистр (верхний регистр), а затем использовать wcsstr.

J.Doe 27.03.2019 19:22

Другие вопросы по теме