计算位数 - 哪种方法最有效?

时间:2012-03-15 13:48:20

标签: c time-complexity

有多种解决方案可以找到给定数字中的数字位数。

例如:

方法-1:

int findn(int num)
{
    char snum[100];
    sprintf(snum, "%d", num);
    return strlen(snum);
}

方法-2:

int findn(int num)
{
    if (num == 0) return 1;
    int n = 0;
    while(num) {
        num /= 10;
        n++;
    }
    return n;
}

方法-3:

int findn(int num)
{
    /* math.h included */
    return (int) log10(num) + 1;
}

问题是 - 什么是最有效的方法?我知道方法-2是O(n)但是方法1和方法3呢?如何找到库函数的运行时复杂性?

13 个答案:

答案 0 :(得分:21)

以下效率更高:

int findn(int num)
{
   if ( num < 10 )
      return 1;
   if ( num < 100 )
      return 2;
   //continue until max int
}

您可以通过二进制搜索进一步优化这一点,但这样做会有点过分。

答案 1 :(得分:11)

目前看来,接受且得到最高度认可的答案(仍然)对于否定数字不正确。如果回答者需要花时间来测试它发现它已被打破了负数,他可能会浪费更多的时间而不是机器只需使用snprintf,即

int count_digits(int arg) {
    return snprintf(NULL, 0, "%d", arg) - (arg < 0);
}

我们不再是20世纪80年代了;像我们一样停止编码。我是一个C级标准狂热者,我在这里给出的最喜欢的答案是Tao Feng's answer ......但即使这样也没有进入为什么它是目前为止最有效的答案;在这个答案中,我打算通过考虑以下内容来表明他的答案可以进一步改善:

  • 程序员的工作效率比代码效率更重要,因为与几分钟的运行时间相比,编写和测试新函数几乎肯定会花费更多的时间。
  • 重用其他程序常用的相同标准库函数(可能)将这些标准库保存在CPU缓存中。缓存未命中(例如,当您的代码需要从RAM复制到CPU)最多可以花费50条CPU指令,更不用说其他代码可能最终导致另一次缓存未命中,无论如何都要将snprintf放回缓存中。
  • 消除存储要求可能会带来额外的优化。

以下介绍了妨碍您提高工作效率的微观优化。由于您在答案中提供的信息不足,所以没有人回答当前的问题可以提供任何证明做出以下假设:

  • 当我们优化时,我们需要找到完整解决方案中最重要的瓶颈(您的程序旨在解决的问题)。这里有两种可能性:A)您想要计算要分配的字节数,以便存储包含这些数字的字符串; B)你只想计算踢的数字或其他数字。稍后会详细介绍。现在,重要的是要意识到你可能正在谈论解决方案的一部分这部分可能不是最重要的瓶颈
  • 您正在使用的编译器,您正在使用的操作系统以及您正在使用的计算机(包括RAM速度,因为我们中的一些人正在引入可能因缓慢内存而不是快速内存而受到更多影响的缓存未命中)影响最显着的瓶颈。有些编译器与其他编译器不同,并且会为某些操作系统,CPU等优化某些代码片段而不是其他编译器。

您可以通过衡量瓶颈来避免微观优化,即通过分析(“基准测试”)系统上的每个解决方案,假设他们甚至可以正确解决您的问题。如果解决方案无法解决问题,那么它不是解决方案,因此不应该考虑......如果正确完成,这应该消除微优化。有些编译器甚至提供了智能的配置文件引导优化,它通常通过重新组织缓存位置的分支和对象,并自动执行来削减20-30%。

我已经涵盖了计数位数,我认为这肯定会回答你的问题,但有些情况下你认为你需要在时计算数字,并且能够消除计算数字的开销,这可能是非常理想的优化,无论是在工作时间的工时。

例如,如果要计算要分配的字节数以存储包含这些数字的字符串,则不应使用任何运行时,因为预处理器宏可用于计算最大位数(或您尝试保存的临时存储空间中的任何字符都将被逻辑中添加的机器代码字节数远远超过我,这对我来说似乎是一笔费用。程序员使用预处理器宏也有好处;相同的宏可用于任何整数类型。 有关此问题的解决方案,请参阅my answerthis question ;毕竟,没有必要重复自己...

答案 2 :(得分:10)

GCC / Clang __builtin_clz()或Microsoft Visual C _BitScanReverse()内在函数在许多机器上编译为单个机器指令。您可以将其用作O(1)解决方案的基础。这是一个32位的实现:

#include <limits.h>
#include <stdint.h>

/* Return the number of digits in the decimal representation of n. */
unsigned digits(uint32_t n) {
    static uint32_t powers[10] = {
        0, 10, 100, 1000, 10000, 100000, 1000000,
        10000000, 100000000, 1000000000,
    };
    static unsigned maxdigits[33] = {
        1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5,
        5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 
    };
    unsigned bits = sizeof(n) * CHAR_BIT - __builtin_clz(n);
    unsigned digits = maxdigits[bits];
    if (n < powers[digits - 1]) {
        -- digits;
    }
    return digits;
}

答案 3 :(得分:7)

我想也许你可以把第一种方法写成

int findn(int num)
{
    char snum[100];    
    return  sprintf(snum, "%d", num);
}

因为sprintf将返回写入的字符数,您可以将调用保存到strlen。

至于效率,我认为这取决于sprintf的实现,你可能需要找到sprintf的来源,看看它是否有效。

答案 4 :(得分:3)

尝试二分搜索。为了清楚起见,我们假设有符号的32位整数。首先,检查是否x < 10000。接下来,取决于答案,x < 100x < 1000000,依此类推。

那是O(log n),其中n是位数。

答案 5 :(得分:3)

这些函数对非正数给出截然不同的结果(最差的是方法3),因此比较它们的时间复杂度具有可疑的价值。我会使用能够在所有情况下提供答案的那个;没有上下文,我们无法知道它是什么(它可能不是方法3)。

方法1,findn(0) == 1findn(-n) == digits in n + 1(由于负号)。

对于方法2,findn(0) == 0findn(-n) == digits in n

对于方法3,findn(0) == INT_MINfindn(-n) == INT_MIN也是如此。

答案 6 :(得分:3)

一个班轮:for(digits = 0; num > 0; digits++) num /= 10;

答案 7 :(得分:2)

我认为sprintf()将使用您的方法2 来打印数字(以确定要打印的字符串的长度,然后打印字符串的每个字符),所以它本来就会慢一些。

3号可能涉及ln()的一些多项式近似,它将涉及更多的1个除法,所以我猜它也会慢一些(here's a fast ln ()实现,仍然涉及浮动划分......所以WAY慢一些。)

所以我的初步猜测是,方法2是要走的路。

请注意,这是解决此问题的一种非常自由的方式。我想用每个函数测试一个很好的旧定时百万次迭代会告诉你结果。但它太过于暴力,不是吗?

请注意,只有方法2会给你真实的结果,其他方法的缺陷必须调整到正确(参见亚伦的回答)。所以只需选择方法2.

答案 8 :(得分:1)

这是我的解决方案...... 它会计数到100位数,或者你知道你想要它的数量。

max_digits = 100

int numdigits(int x) {
  for (int i = 1; i < max_digits; i++) {
    if (x < pow(10, i)) {
      return i;
    }
  }
  return 0;
}

答案 9 :(得分:1)

printf函数将返回成功打印的位数。

int digits,n=898392;

digits=printf("%d",n);
printf("Number of digits:%d",digits);

<强>输出:

898392

位数:6

答案 10 :(得分:0)

使用日志可能是个不错的选择......

  1. 如果目标计算机具有硬件支持
  2. 如果您确定int可以投放到double并且不会丢失精确度。
  3. 示例实施......

    int num_digits(int arg) {
        if (arg == 0) {
            return 1;
        }
    
        arg = abs(arg);
    
        return (int)log10(arg)+1;
    }
    

答案 11 :(得分:0)

log10(x) + 1将计算x > 0的位数,但可能需要另一种方法:

  1. log2(x) = size_of(x) - 1 - clz(x)将计算log2为整数。
  2. log_b(x) = log2(x) / log2(b)将进行基本转换。
  3. log2(10)除数变为以10为底的3
  4. 对于32位整数,log2(x)的分红变为31 - clz(x)

您甚至可以以此来计算十六进制数字或任何其他基数。

uint32_t count_dec_digits(uint32_t n) {
    return 1 + (31 - __builtin_clz(n)) / 3;
}

uint32_t count_hex_digits(uint32_t n) {
    return 1 + (31 - __builtin_clz(n)) / 4;
}

int main() {
    printf("%d", count_dec_digits(1234567)); // 7
    printf("%d", count_hex_digits(0x12345)); // 5
}

答案 12 :(得分:0)

@AShelly 建议的函数变体,用于有符号和无符号 64 位数字,并检查 0(https://godbolt.org/z/cqeKEj4rj,还包含一些测试代码)。

uint8_t digits_u64(uint64_t number) {
    static uint8_t maxdigits[] = {
        1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6,
        7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11,
        12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 15, 15, 15,
        16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 19, 20, 
    };

    static uint64_t powers[] = {
        0U, 1U, 10U, 100U, 1000U, 10000U, 100000U, 1000000U, 10000000U,
        100000000U, 1000000000U, 10000000000U, 100000000000U,
        1000000000000U, 10000000000000U, 100000000000000U,
        1000000000000000U, 10000000000000000U, 100000000000000000U,
        1000000000000000000U, 10000000000000000000U, 
    };

    if (number == 0) {
        return 1;
    }

    unsigned bits = sizeof(number) * CHAR_BIT - __builtin_clzll(number);
    unsigned digits = maxdigits[bits];

    if (number < powers[digits]) {
        --digits;
    }
    
    return digits;
}

uint8_t digits_i64(int64_t number) {
    int add_minus_sign = number < 0U ? 1 : 0;
    if (add_minus_sign) {
        number *= -1;
    }

    return digits_u64(number) + add_minus_sign;
}