【问题标题】:Fastest way to do a case-insensitive substring search in C/C++?在 C/C++ 中进行不区分大小写的子字符串搜索的最快方法?
【发布时间】:2010-09-17 17:07:16
【问题描述】:

注意

下面的问题是在 2008 年提出的关于 2003 年的一些代码的问题。正如 OP 的更新所示,整个帖子已被 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] = '\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 库现在有 a new implementation of strstr(),但我不确定它是否可以轻松修改为不区分大小写,或者它实际上是否比旧的(在我的情况下)。我也注意到the old implementation is still used for wide character strings,所以如果有人知道为什么,请分享。

更新

只是为了说明清楚——以防它还没有——我没有编写这个函数,它是 GNU C 库的一部分。我只是将其修改为不区分大小写。

另外,感谢您提供有关 strcasestr() 的提示并查看其他来源(如 OpenBSD、FreeBSD 等)的其他实现。这似乎是要走的路。上面的代码来自 2003 年,这就是为什么我在这里发布它以希望有更好的版本可用,显然是这样。 :)

【问题讨论】:

  • 这个技巧无济于事,但您至少应该清除所有不必要的代码,例如您使用“goto jin”语句跳过的代码。
  • ...而且我对你缺乏阅读能力的印象并不深刻。正如源代码 cmets 和我下面的注释中所述,我没有编写此代码。
  • 我想看看人们是否知道更快的方法来进行不区分大小写的子字符串搜索——因为我需要它来在我的程序中提供快速搜索——结果证明,更快的 strcasestr () 实际上现在可用。
  • MSVC++ 有一个名为“StrStrI”的函数,参见msdn.microsoft.com/en-us/library/windows/desktop/…

标签: c++ c optimization string glibc


【解决方案1】:

独立于平台使用:

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 提供的内置函数来执行此操作。
  • 我知道 wcsstr ,但它只是不区分大小写,我有一个解决方案将所有转换为小写(大写),然后使用 wcsstr 。
【解决方案2】:

您发布的代码大约是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 只是一个简单的 shell 脚本:

#!/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() 不存在——或者至少我不知道它(根据 glibc CVS 历史它是在 2005 年添加的)。似乎 MSVC++ 没有提供 strcasestr(),但也许我可以从 glibc 移植它。
【解决方案3】:

您可以使用 StrStrI 函数来查找字符串中第一次出现的子字符串。比较不区分大小写。 不要忘记包含它的标题 - Shlwapi.h。 看看这个:http://msdn.microsoft.com/en-us/library/windows/desktop/bb773439(v=vs.85).aspx

【讨论】:

    【解决方案4】:

    这不会考虑语言环境,但如果你可以改变 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);
    }
    

    【讨论】:

      【解决方案5】:

      如果您可以控制针字符串始终为小写,那么您可以编写 stristr() 的修改版本来避免查找,从而加快代码速度。它不是一般的,但它可以更快 - 稍微快一点。类似的 cmets 适用于 haystack,但您更有可能从无法控制的来源读取 haystack,因为您无法确定数据是否满足要求。

      性能上的提升是否值得完全是另一个问题。对于 99% 的应用程序,答案是“不,不值得”。您的应用程序可能是少数重要的应用程序之一。更有可能不是。

      【讨论】:

        【解决方案6】:

        使用boost string 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;
        }
        

        【讨论】:

          【解决方案7】:

          如果您想减少 CPU 周期,您可以考虑这一点 - 假设我们处理的是 ASCII 而不是 Unicode。

          制作一个包含 256 个条目的静态表。表中的每个条目都是 256 位。

          要测试两个字符是否相等,请执行以下操作:

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

          要构建表,您在 table[char1] 中的任何位置都设置了位,您认为它与 char2 匹配。因此,在构建表格时,您将在第 'a' 项(和第 'A' 项)中设置索引处的位,以用于 'a' 和 'A'。

          现在进行位查找会变慢(位查找很可能是移位、掩码和相加),因此您可以使用字节表代替,因此您可以使用 8 位来表示 1 位。这将需要 32K - 所以万岁 - 你已经达到了时间/空间的权衡!我们可能想让表格更灵活,所以假设我们这样做 - 表格将改为定义同余。

          当且仅当存在将它们定义为等效的函数时,才认为两个字符是全等的。所以'A'和'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 处停止。这就是为什么 ASCII 有 500 个扩展。这并不重要,这是 2008 年,全世界现在都在使用 Unicode
          【解决方案8】:

          假设两个输入字符串都已经是小写了。

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

          【讨论】:

          • 我已经尝试了各种实现,在比较之前我会将字符串转换为小写,但是当您在长字符串中搜索短字符串时,结果会变慢。
          • 另外,如果两个字符串大小写相同,你可以使用 strstr()... ;)
          【解决方案9】:

          我建议您采用一些已经存在的常见 strcasestr 实现。例如 glib、glibc、OpenBSD、FreeBSD 等。您可以通过 google.com/codesearch 搜索更多内容。然后,您可以进行一些性能测量并比较不同的实现。

          【讨论】:

            【解决方案10】:

            你为什么使用 _strlwr(string);在 init_stristr() 中?这不是标准功能。大概是为了支持语言环境,但由于它不是标准的,我只是使用:

            char_table[i] = tolower(i);
            

            【讨论】:

            猜你喜欢
            • 2013-11-05
            • 2012-04-30
            • 2017-12-01
            • 1970-01-01
            • 1970-01-01
            • 2016-09-27
            • 2018-09-29
            • 1970-01-01
            • 2011-12-20
            相关资源
            最近更新 更多