【问题标题】:What is the fastest way to decide if a digit appears in a string?确定数字是否出现在字符串中的最快方法是什么?
【发布时间】:2025-12-12 17:00:02
【问题描述】:

这个简单的解决方案很快就出现在我的脑海中。

#include <ctype.h>

int digit_exists_in
(
    const char *s
)
{
    while (*s)
    {
        if (isdigit(*s))
        {
            return 1;
        }
        else
        {
            s++;
        }
    }

    return 0;
}

int main(void)
{
    int foundDigit = digit_exists_in("abcdefg9ijklmn");

    return 0;
}

还可以应用哪些其他技术来提高速度?

实际搜索的字符串是可变长度的,字符本身是 ASCII,而不是完整的字符集。字符串以 NUL 结尾。

【问题讨论】:

    标签: c performance string


    【解决方案1】:

    任何算法都将是 O(N)。

    我认为 isdigit 已经相当高效了。

    【讨论】:

    • 至少 O(N) :) 很容易实现悲观化和“智能”算法:)
    • 好点,您可以使用一些效率较低的算法,但它们可能更难实现。
    • 为了利用短路,你不能写"!(char '9')"
    • isdigit() 通常是表格查找,即比两次比较快。并且区域设置安全启动。 ;-)
    • isdigit 通常是位掩码操作 - 比两部分测试更快。
    【解决方案2】:

    从概念上讲,没有更快的方法。这是假设您有一个字符串,其中数字的位置似乎是随机的。这迫使您在字符串中的每个项目中搜索一个数字,因此从前到后与任何其他搜索机制一样可能首先找到该数字。

    【讨论】:

    • 是的,数字的位置没有任何模式可以用来进行有根据的或基于缓存的猜测。
    【解决方案3】:

    如果您真的想减少开销时间并且不介意将其专门用于 char,那么您可以检查 0 到 9 之间的 ascii 值(包括 0 到 9)。

    48 到 57 十进制

    这会删除堆栈调用。

    我也应该说查找表...

    【讨论】:

    • 这正是 isdigit 所做的,您可以制作自己的内联 isdigit 实现。但是是的。
    • 从头开始,邦迪的回答反驳了我。
    【解决方案4】:

    正如其他人所说,你不能低于 O(N)。

    我可以想到一个具有对数速度的人为场景...假设您正在编写一个文本编辑器,它需要“此文件是否包含任何数字”功能。您可以保留文件中存在的所有唯一字符的排序数组,在​​每次击键时更新它,并使用二进制搜索查询它。不过,这可能超出了您的问题范围(并且可以通过几种更好的方式完成)。

    【讨论】:

      【解决方案5】:

      您可以使用多线程,尽管对于已经非常快的算法来说,这可能会增加太多的复杂性。

      【讨论】:

      • 好点,但是对于除了最长的字符串之外的所有字符串,由于线程的开销,这将是一种悲观。
      • 由于可怕的缓存效果,您还可能使用线程伤害自己。对于像这样的东西,人们希望真正的限制因素是内存延迟/带宽,而使用多核可能会使情况变得更糟。
      • 如果字符串足够大,听起来像是赢家。
      【解决方案6】:

      没有更快的算法,但您可以查看每条指令的完整寄存器的字节数,或使用 SIMD 操作来加快速度。您可以使用掩码和零测试来查看是否有可能在一个范围内有任何数字,或者如果您的 SIMD 操作在足够大的向量上足够快,您可以迭代测试中的特定数值字节向量比字符比较快。

      因此,例如,您可以执行以下操作:

      byte buffer[8] = { 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 };
      uint64 *mask = (uint64 *) buffer; //this is just for clarity
      
      if (*((uint64 *) s) & *mask) == 0)
          //You now don't need to do the < '0' test for the next 8 bytes in s
      

      一些优化器可能足够聪明,只需从上面的代码示例中就可以为您执行此操作。

      不过,您最好比较 TON 字节以考虑在此级别进行优化。

      【讨论】:

      • 这会告诉你是否没有字符 0..., P..., p... 那你怎么办?
      • 它肯定会告诉您当前正在查看的 8 个数字中没有数字,因此您跳到接下来的 8 个数字。
      • 哦,好的。零表示该集合中没有数字(并且没有字母> ='P'或'p'),但非零并不意味着有数字。因此,如果字母 >= 'P' 或 'p' 很少使用,这可以节省大量时间。
      • 所有数字的左半字节都有 30。如果你和他们和掩码比较,他们相等,那么它是一个数字。
      • @Evil:明白,但这不是这段代码的作用。它只告诉你字符串是否像“AbCdEfGh”,即只有字母和只有在 P 或 p 之前的字母。还是我错过了什么?
      【解决方案7】:

      真正的问题是,优化此功能有多重要?我说留下简单的解决方案并对其进行分析。只有当它导致您的代码出现瓶颈时,您才应该对其进行优化。

      【讨论】:

        【解决方案8】:

        只是为了好玩,也许是这样的:

         // accumulator
        unsigned char thereIsADigit = 0;
        // lookup table
        unsigned char IsDigit[256] = {0,0,0 ..., 1,1,1,1,1,1,1,1,1,0,0,0 ...};
        
        // an unrolled loop, something like:
        thereIsADigit |= IsDigit[s[0]];
        thereIsADigit |= IsDigit[s[1]];
        thereIsADigit |= IsDigit[s[2]];
        thereIsADigit |= IsDigit[s[3]];
        thereIsADigit |= IsDigit[s[4]];
        thereIsADigit |= IsDigit[s[5]];
        thereIsADigit |= IsDigit[s[6]];
        thereIsADigit |= IsDigit[s[7]];
        if (thereIsADigit) break;
        s += 8;
        

        在 IBM 360 上,有一个 "translate" instruction 可以一步完成。

        好的,好的, Christopher Smith 的回答让我开始思考。假设您只使用 7 位 ASCII。这是一种使用宽整数算法执行 SIMD 的方法。

        假设 C 是一个包含 4 个字符的 32 位字。

         // compare by subtracting in 8-bit 2s complement arithmetic
        ( (C + ((0x3a3a3a3a ^ 0x7f7f7f7f) + 0x01010101)) // set high bit for any char <= '9'
          &
          (0x2f2f2f2f + ((C ^ 0x7f7f7f7f) + 0x01010101)) // set high bit for any char >= '0'
        ) // high bit is set for any char <= '9' and >= '0'
        & 0x80808080 // look only at the high-order bits
        // if any of these 4 bits is 1, there is a digit in C
        // if the result is zero, there are no digits in C
        

        这取决于每个字符的高位初始为零,因此进入该位的进位不会传播。 (我相信这可以简化。)

        【讨论】:

        • 是的,输入集是 7 位 ascii。
        【解决方案9】:

        我将首先使用适当的库函数strcspn,假设该库已在极端偏见的情况下进行了优化:

        #include <string.h>
        #include <stdio.h>
        
        int digit_exists_in(const char *s)
        {
            return s[strcspn(s, "0123456789")] != '\0';
        }
        
        int main(void)
        {
            printf("%d\n", digit_exists_in("foobar"));
            printf("%d\n", digit_exists_in("foobar1"));
            return 0;
        }
        

        如果库没有得到充分优化,最好 将优化放入库中,以便每个人都可以受益。 (你有来源, 对吧?)

        【讨论】:

        • 有趣。我拒绝了 strspn,因为文档似乎暗示如果第一个字符不在搜索集中,则该函数将返回 0。
        • strspn 返回一个整数值,指定字符串中完全由 strCharSet 中的字符组成的子字符串的长度。如果字符串以 strCharSet 中没有的字符开头,则函数返回 0。没有保留返回值来指示错误。
        • 我将重温 strspn 和 strcspn。 +1
        【解决方案10】:

        让人类看看它。人类可以在 O(1) 时间内完成此操作,我们的字数甚至比现代处理器大得多。

        也就是说,使用您的方法,实际时间仍然会更好......现代核心和人脑之间的循环时间差异如何。

        【讨论】:

        • 更不用说热量负荷(twinkies/invocation :)
        【解决方案11】:

        我可能错了,但可能有更快的方法。

        快速排序字符串。

        串行搜索的最佳时间为 O(1),平均值为 O(1/2n),最差情况为 O(n)。

        快速排序最好的 O(log n),平均 O(nlog n),最坏的 O(n^2)。

        问题是,你一看到数字就可以退出快速排序。如果快速排序确实完成,数字将在排序字符串的开头,所以你会在 O(1) 中找到它。

        这里的成就是改变了最好的、平均的和最坏的情况行为。快速排序的最坏情况行为会更差,但平均行为会更好。

        【讨论】:

        • 好主意。我更喜欢归并排序,它总是 NlogN。当然,这只是比 N 慢 logN 倍 :)
        • 使用 XML 会更快吗? ;-)
        • 如果你要去 BS,那就好好做吧。任何人都可以看到 1/2 n 比 n log n 好。
        • @Matthew - O(nlog n) 是 completed 快速排序的平均值。但是,在这种方法中,快速排序通常不需要完成。
        【解决方案12】:

        当然,你可以牺牲准确性来换取速度:

        int digit_exists_in(const char* s)
        {
            return 0;
        }
        

        该算法的复杂度为 O(1),近似度为 O((246/256)^N)。

        【讨论】:

        • 同意盾牌!通过将“return 0”替换为“return isdigit(*s)”
        • 谢谢,这样我就无法从一个笑话中获得任何声誉。 (虽然现在我认为它是双向的。)
        【解决方案13】:
        顺便说一句,

        liw.fi 是对的。我对此有点惊讶,因为 strcspn 必须解决比 isdigit() 方法更普遍的问题,但似乎是这样:

        #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
        #include <ctype.h>
        #include <assert.h>
        
        #define NTESTS 10000
        #define TESTSIZE 10000
        
        char stest1[TESTSIZE];
        char stest2[TESTSIZE];
        
        int test_isdigit(char *s) {
            while (*s) {
                if (isdigit(*s)) return 1;
                s++;
            }
            return 0;
        }
        
        int test_range(char *s) {
            while (*s) {
                if ((*s >= '0') && (*s <= '9')) return 1;
                s++;
            }
            return 0;
        }
        
        int test_strcspn(char *s) {
            return s[strcspn(s, "0123456789")] != '\0';
        }
        
        int main(int argc, char **argv) {
            long int i;
            for (i=0; i<TESTSIZE; i++) {
                stest1[i] = stest2[i] = 'A' + i % 26;
            }
            stest2[TESTSIZE-1] = '5';
        
            int alg = atoi(argv[1]);
        
            switch (alg) {
                case 0:        
                    printf("Testing strcspn\n");
                    for (i=0; i<NTESTS; i++) {
                        assert(test_strcspn(stest1) == 0);
                        assert(test_strcspn(stest2) != 0);
                    }
                    break;
                case 1:
                    printf("Testing isdigit() loop\n");
                    for (i=0; i<NTESTS; i++) {
                        assert(test_isdigit(stest1) == 0);
                        assert(test_isdigit(stest2) != 0);
                    }
                    break;
                case 2:
                    printf("Testing <= => loop\n");
                    for (i=0; i<NTESTS; i++) {
                        assert(test_range(stest1) == 0);
                        assert(test_range(stest2) != 0);
                    }
                    break;
                default:
                    printf("eh?\n");
                    exit(1);
            }    
        
            return 0;
        }
        

        在他们自己的游戏中击败标准库非常困难......(通常的警告适用 - YMMV)

        $ gcc -O6 -Wall -o strcspn strcspn.c 
        
        $ time ./strcspn 0
        Testing strcspn
        
        real    0m0.085s
        user    0m0.090s
        sys 0m0.000s
        
        $ time ./strcspn 1
        Testing isdigit() loop
        
        real    0m0.753s
        user    0m0.750s
        sys 0m0.000s
        
        $ time ./strcspn 2
        Testing <= => loop
        
        real    0m0.247s
        user    0m0.250s
        sys 0m0.000s
        

        更新:只是为了好玩,我根据 Mike Dunlavey 的回答添加了位图查找版本:

        char bitmap[256] = {
                /* 0x00 */ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                /* 0x10 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                /* 0x20 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                /* 0x30 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        };
        
        int test_bitmap(char *s) {
            while (!bitmap[*(unsigned char *)s]) s++;
            return (*s);
        }
        

        略优于其他(~.170s)但仍然无法触及 strcspn!

        【讨论】:

        • 我得到了类似的结果,FWIW。 test_range() 和 test_digit() 执行几乎相同,strcspn 是明显的“赢家”。
        • 在 OS X (Leopard) 上,test_digit() 表现非常糟糕(实时 2.281 秒),而 test_range() 表现还不错(0.686 秒),而 test_strcspn() 仍然是明显的赢家(0.275秒)。所有这些的系统时间都是 0.003 到 0.006,但我注意到 test_isdigit() 的滞后时间。
        • 是的。使用数字和事实来支持你的论点是作弊 +1
        • 做得很好,实际的基准测试总是有用的。我怀疑编译器/架构之间存在巨大差异,但以防万一:任何想要使用此代码的人都应该自己运行基准测试。
        • reddit.com/r/simd/comments/myqv32/… 有一个 SIMD strcspn,它适用于 large 输入。 (对于小的输入可能更糟。)还有在Remove all unwanted characters from a string buffer, in-placeRemove all unwanted characters from a string buffer, in-place 上的 code-review.SE 上的半相关 cmets
        【解决方案14】:

        这是一个可能更快也可能不会更快的版本,但它处理 NULL 指针...

        int digit_exists_in(const char *s)
        {
            if (!s)
                return (0);
            while (*s)
                if (isdigit(*s++))
                    return (1);
            return (0);
        }
        

        【讨论】:

          【解决方案15】:

          内存预取

          如果您的字符串很长,请让您的编译器执行此操作,或者手动展开循环并在每个缓存行中放入一条或两条内存预取指令。

          这样,当 CPU 正在扫描时,内存控制器可以拉入下一行数据。

          如果您在创建字符串时保存了字符串的长度,则可以跳过对 NUL 字节的所有检查,这意味着您可以展开循环以在更大的块中进行操作并减少比较和分支操作的数量,尽管使用当前的分支预测器,老实说并没有太大的区别。

          即使有出色的 CPU 分支预测器,如果每次循环都必须检查循环计数器以确定何时进行内存预取,那么循环也会减慢速度,因此在这种情况下展开仍然很有帮助。

          分析反馈

          为了获得最佳性能,CPU 确实需要正确提示分支,这就是分析反馈非常方便的地方。否则编译器只是在做一些有根据的猜测。

          【讨论】:

            【解决方案16】:

            获取测试程序并将我的分析器扔给它会产生以下结果。

                  Count      %   % with           Time   Statement
                                  child
            -----------------------------------------------------------------------------------------------
                                                         int test_isdigit(char *s)   
                 20,000    0.0    0.0          2,160.4   {   
            199,990,000   13.2    9.5     14,278,460.7       while (*s)  
                                                             {   
            199,980,000   69.8   49.9     75,243,524.7            if (isdigit(*s)) return 1;  
            199,970,000   17.0   12.1     18,312,971.5            s++;    
                                                             }   
            
                 10,000    0.0    0.0          1,151.4       return 0;   
                                                         }   
            
                                                         int test_range(char *s)     
                 20,000    0.0    0.0          1,734.2   {   
            199,990,000   33.6    9.4     14,114,309.7       while (*s)  
                                                             {
            199,980,000   32.2    9.0     13,534,938.6           if ((*s >= '0') && 
                                                                    (*s <= '9')) return 1;   
            199,970,000   34.2    9.5     14,367,161.9           s++;    
                                                             }   
            
                 10,000    0.0    0.0          1,122.2       return 0;   
                                                         }   
            
                                                         int test_strcspn(char *s)   
                 20,000    0.2    0.0          1,816.6   {   
                 20,000   99.8    0.6        863,113.2       return s[strcspn(s, "0123456789")] 
                                                                      == '0'; 
                                                         }   
            

            strcspn 做得很好。查看它的 asm 代码,我看到它构建了一个大小为 256 的位图,根据搜索字符设置位,然后处理字符串。

            每次调用都会在堆栈上构建一次位图。

            另一种方法是构建和保留位图,并且每次都重复使用它。

            另一种方法是使用以下技术并行执行操作 克里斯·史密斯谈到了。

            目前 strcspn 就足够了。

            【讨论】:

              最近更新 更多