【问题标题】:Counting number of digits in a number in O(1)在 O(1) 中计算数字中的位数
【发布时间】:2012-05-23 15:34:56
【问题描述】:

我需要计算一个数字中的小数位数(例如:4 表示 1002)。我想以 O(1) 的时间复杂度执行此操作,因为代码应在大量数字上进行迭代,从而显着节省 cpu 时间。

我想出了两个解决方案:

  1. 在循环中除以 10,直到数字变为零。循环计数是 答案。但是,显然是 O(n) 时间。
  2. log_base_10(num) + 1

问题:log10 是 O(1) 解吗?我在 x86 机器上使用 glibc 运行代码。这是如何在幕后实现的?并且,有没有更好的解决方案?

【问题讨论】:

  • 数量上限是多少?还是没有这个限制?
  • 解决方案 1 是 O(log n) 而不是 O(n) 并且假设您的号码是 longint (不是大数字),可能比解决方案 2 更快。
  • 您的问题似乎很奇怪。您真的想知道计算复杂度,还是想知道“十进制表示的长度”的最快方法?
  • 不要忘记,O 表示法不会告诉您算法需要多长时间,它只会告诉您执行时间如何随不同大小的输入而变化。确定 O(log n) 算法的实现是否实际上比 O(n) 算法的实现更快的唯一方法是使用分析器。
  • 如果您的数字是固定大小的,那么大 O 表示法毫无意义。正如@Skizz 指出的那样,对于给定的 n 值,O(log n) 算法必须比 O(n) 算法更快,这并没有隐含的原因。

标签: algorithm x86 numbers glibc


【解决方案1】:

假设无符号整数和 Intel 平台(BSR 指令),您可以获得最高设置位。那你就知道了:

2^i <= num < 2^(i+1)

其中inum 的最高设置位。如此简单的查找表(由 i 索引)将您限制为两个可能的十进制数字计数,并且可以通过单个 if 来解决。

但是你真的使用了这么大的数字以至于需要这种不可移植的优化吗?

【讨论】:

    【解决方案2】:

    这看起来像是 Bit Twiddling Hacks 的案例

    unsigned int v; // non-zero 32-bit integer value to compute the log base 10 of 
    int r;          // result goes here
    
    r = (v >= 1000000000) ? 9 : (v >= 100000000) ? 8 : (v >= 10000000) ? 7 : 
        (v >= 1000000) ? 6 : (v >= 100000) ? 5 : (v >= 10000) ? 4 : 
        (v >= 1000) ? 3 : (v >= 100) ? 2 : (v >= 10) ? 1 : 0;
    

    “当输入均匀分布在 32 位值上时,此方法效果很好,因为 76% 的输入被第一次比较捕获,21% 被第二次比较捕获,2% 被第三次捕获,并且以此类推(每次比较将剩余的减少 90%)。因此,平均需要少于 2.6 次操作。”

    【讨论】:

    • 由于上述代码中条件分支的数量,这可能与 fild/fyl2x/fist 接近。
    • @Skizz:我猜有些编译器会选择创建一个无分支的 compare/cmov 链,尤其是在配置文件引导的反馈显示分支预测不佳的情况下。 (如果您进行分支,小数字很常见,因此首先检查它们对于某些用例会更有意义。但如果您的数字均匀分布在 uint32_t 中,它们中的大多数将有 9 位数字,而绝大多数将有8 或 9,因此首先检查较大的数字是有意义的。
    【解决方案3】:

    我不会使用 log 来解决这个问题,因为它涉及双重计算,并且可能会比除以 10 的周期慢。牺牲一些内存和一些时间进行预计算,您可能会在一些整数指令中得到答案.例如预先计算最多 10 000 个数字的位数:

    int num_digits[10000];
    for (int i = 0; i < 10000; ++i) {
      if (i < 10) {
        num_digits[i] = 1;
      } else if (i < 100) {
        num_digits[i] = 2;
      } else if (i < 1000) {
        num_digits[i] = 3;
      }
    }
    

    现在这里是你如何在大约 4 个整数运算中得到一个数字的位数:

    int get(int n) {
      int result = 0;
      while (n > 10000) {
        result += 4;
        n /= 10000;
      }
      return result + num_digits[n];
    }
    

    这当然会为了速度而牺牲内存,但我们知道there is no free lunch

    【讨论】:

      【解决方案4】:

      您需要做的就是:

      1 + floor(log(N)/log(10))
      

      (这在0上不起作用。如果输入浮点数,它将返回小数点左侧的位数,并且仅对大于0.1的浮点数起作用。)

      几乎可以肯定有一条 FPU 指令,不是在原始 x86 中,而是在您的 CPU 支持的扩展中。您可以通过在循环中多次评估 log(N) 来测试这一点。

      这是假设您将数字存储为 int 或 float。如果您使用某个库将数字存储为可变长度数组,如果库预先计算数组的长度(正确的做法),这是 O(1) 时间,否则是 O(N) 时间(坏库)。

      与所有浮点运算一样,如果您真的接近过渡 99-100、999-1000 等,准确性可能是一个问题。正如史蒂夫杰索普在此答案的 cmets 中指出的那样,您可以确定是否高估/低估根据所需的语义是可以的。我什至可以说你有一点余地:如果这些转换数字以某种方式失败,你可以从 N 中添加/减去 0.1 之类的东西(没有那么多数字:你可以自己手动测试它们,看看这是否是必要)。

      【讨论】:

      • 别忘了 log (0) 是未定义的!
      • @Skizz:嗯,也许我应该提一下这个警告,谢谢。
      • log10 比调用 log 两次并除以更好的选择,即使实现可以在编译时计算 log(10)log10 不应该比 log 后跟一个除法差,而且可能会更好,并且可能更准确。当输入是 10 的幂时,准确度是最重要的,因为即使是负方向上的微小误差也会导致误差。
      • @SteveJessop 但没有log10。不过,有一条针对y * log2(x) 的说明,似乎符合要求(使用y = 1/log2(10))。
      • @harold: true(还有一个 x86 指令也可以加载常量 log(10))。 log10 没有 x86 指令,但我不完全清楚我们应该编写 C 还是 x86 程序集。如果是 C,则有一个 log10 函数。想一想,可以想象log(n) / log(10) 对于所有 10 次幂恰好是准确的或高估的,但是对于双打来说,实现会做一些更慢和更准确的事情。在这种情况下,log10 效率会很低。您必须测试准确性,如果结果没问题,那么log10 不再是更好的选择。
      【解决方案5】:
      import java.util.*;
      import java.lang.*;
      class Main
      {
        public static void main (String args[])
        {
          Scanner in = new Scanner (System.in);
          System.out.print ("Enter the number : ");
          int n = in.nextInt ();
          double ans = 1 + Math.floor (Math.log (n) / Math.log (10));
          int intANS = (int) (ans);
          System.out.println ("No. of digits in " + n + " = " + intANS);
        }
      }
      

      //在JAVA中

      【讨论】:

        【解决方案6】:
        class NumberOfDigits{
        
            public static void main(String ar[]){
                int n=99;
                System.out.println(""+(1+Math.floor(Math.log10(n))));
            }
        }
        

        【讨论】:

        • 虽然此代码可以回答问题,但提供有关 如何 和/或 为什么 解决问题的附加上下文将改善答案的长期价值。
        猜你喜欢
        • 1970-01-01
        • 2010-09-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-09-29
        相关资源
        最近更新 更多