如何有效地检索数字的第一个十进制数字

Posted

技术标签:

【中文标题】如何有效地检索数字的第一个十进制数字【英文标题】:How to retrieve the first decimal digit of number efficiently 【发布时间】:2013-06-30 18:53:58 【问题描述】:

一个明显的解决方案是:

int n = 2134;
while(n > 9)
    n /= 10;

这需要线性时间。我们能做得更快吗?

这是否比线性时间快:

char s[100];
sprintf(s, "%d", n);
n = s[0]-'0';

还有哪些其他方式(效率是首要考虑因素)? 我见过this,只是我只需要找到第一个数字。 (另外,我不明白答案)。

【问题讨论】:

您的第二个示例可能应该使用sprintf 而不是scanf? 格式化 I/O 肯定比除法慢很多(数量级)。但是你对它进行了基准测试吗?另外,sscanf() 不应该真的是snprintf() 吗? “我不明白答案”不是发布新问题的好理由,除非该答案实际上很糟糕。事实并非如此,它或多或少是 digits = floor(log10(n)); firstDigits = n/10^digits; 以及将 doubles 强制转换为整数类型的必要条件。 你能想出一个在数字的对数中不是线性的“第一位”的定义吗?! 关于算法和复杂性的一个有趣的事情是,您可以为同一个精确算法声明不同的渐近复杂性。您说除法方法是线性,或者如果您认为位数是有限的(在sizeof(int)==4 位于最多 10 个十进制数字)。只是说 linear 听起来可能比实际情况更糟...... 【参考方案1】:

一些处理器有指令可以非常快速地计算出一个数字的“大小”(参见http://en.wikipedia.org/wiki/Leading_zero_count)。这可以用来快速选择 10 的幂,然后除以它,而不是重复除以 10。

假设您有一个函数clz,它计算数字二进制表示(0...32)中前导零位的数量。然后,您可以使用一个查找表,为每个前导零数提供 10 的适当幂。

uint32_t powers_of_10[33] = 
    1000000000, 1000000000,
    100000000, 100000000, 100000000,
    10000000, 10000000, 10000000,
    1000000, 1000000, 1000000, 1000000,
    100000, 100000, 100000,
    10000, 10000, 10000,
    1000, 1000, 1000, 1000,
    100, 100, 100,
    10, 10, 10,
    1, 1, 1, 1, 1
;

int CalcFirstDecimalDigit(uint32_t x)

    int leading_zeros = clz(x);
    x /= powers_of_10[leading_zeros];
    if (x >= 10)
        return 1;
    else
        return x;

【讨论】:

对于支持此类操作的处理器来说,这似乎是最快的解决方案。 非常小的建议:使用uint32_t powers_of_10[33] 恕我直言 - 查找表并不快。通常,它们需要访问 RAM,并且充其量是在缓存中 - 这仍然比在寄存器中工作要慢得多。【参考方案2】:

例如对于 32 位无符号:

第 1 步:(通过二分查找)确定该值位于以下哪个区间:

0 .. 9
10 .. 99
100 .. 999
1000 .. 9999
10000 .. 99999
100000 .. 999999
1000000 .. 9999999
10000000 .. 99999999
100000000 .. 999999999
1000000000 .. 4294967295

最多进行 4 次比较

第 2 步:

比以一个除法计算前导数。

【讨论】:

您可以对Step 1的二分查找进行硬编码,避免循环,提高速度。【参考方案3】:

我很确定sprintf(我认为是这样)会明显变慢。您可以进行一些优化以减少除法操作的数量(这是几乎所有处理器上最慢的指令之一)。

所以人们可以这样做:

 while(n > 10000)
   n /= 1000;

 while(n >= 9)
   n /= 10;

当然,如果速度真的很重要的话。

【讨论】:

这似乎就是libdivide 的本意。 如果速度那么很重要,那么对十次方 1, 10, 100,... 等进行二分搜索可能会更快。之后你只需要一个部门。您可以将二进制搜索硬编码为一系列测试和跳转,最好使用汇编语言。 好吧,对于 32 位,您将在第一个循环中执行最多 3 次迭代。对于 64 位数字来说,减少除法的数量变得更有意义。 while(n >= 9) ,对于 n=9,输出为 0 作为 9/10=0 ,而不是 9。应该是 while(n>=10) 。【参考方案4】:

您的第二个示例应使用sprintf。无论如何,它不能更快​​,因为整个数字都被打印出来了,因此所有数字都被搜索了。

链接的问题/答案使用对数属性:对于多个 x 数字,它的以 10 为底的对数介于 xx+1 之间。但是,由于浮点错误,这种方法在某些情况下并不能真正正常工作。另外,请考虑浮点运算比整数运算慢的事实。

因此,最简单的解决方案也更快。

【讨论】:

嗯,对数解不可能更快,因为它使用了对数函数。这比现代处理器上的任何浮点运算都要昂贵得多。所以,是的,这个小循环肯定比 sprintflog() 快,甚至比 MrSmith42 的答案还要快。【参考方案5】:

您可以在 O(1) 恒定时间内完成此操作,但会消耗大量内存。这是相同的旧时间/内存权衡。

您可以创建一个包含 2^31 个条目(有符号整数)的查找表,每个条目 4 位(使用 4 位您可以将数字的第一个数字 1-9 编码为十进制表示)。

然后您可以使用 int 访问查找表并获取 O(1) 中的第一个数字。 查找表将占用 2^31 * 4 位 -> 1024 MB

这是我能想到的最快的方法……

【讨论】:

-1 表示内存膨胀,由于内存延迟而抵消了任何性能优势。 @cmaster “内存膨胀”很明显,并且显示了如何达到 O(1)。这种方法在部分案例中非常实用。假设 int 范围被限制为 0...1000。这样的桌子大小是可行的。通常当需要高性能例程时,存在这样的限制。我不希望 OP 列出所有条件 - 只是主要条件。这种方法虽然对 GP 库没有用处,但可能只是 OP 或未来审阅者的门票。 对,我只是想证明一个人可以很容易地用记忆交换时间 另外,使用重复划分的天真方法在空间中是 O(1)。 @chux:什么是OP?什么是全科医生? 我知道用内存换取性能的可能性,但这不是这样做的地方。正如其他答案所指出的,您可以在不触及 L1 缓存的情况下分四步找到完整整数范围的正确答案,这肯定比等待内存子系统传递答案要快。选择更小的数字范围实际上会削弱你的论点,因为它只需要两步就可以达到 9999 的范围。这不是人们应该把内存扔到的那种问题。【参考方案6】:

这是二分搜索的一种变体。就像二进制搜索一样,它是 O(log n)。是否更快取决于您进行整数除法的速度。

if (n >= 100000000)
    n /= 100000000
if (n >= 10000)
    n /= 10000
if (n >= 100)
    n /= 100
if (n >= 10)
    n /= 10

该方法很容易扩展到更大范围的整数。

【讨论】:

【参考方案7】:

你可以这样做:

//Shashank Jain
#include<iostream>
#include<cmath>
using namespace std;
int main()

    int num,fdigit;
    cin>>num;
    if(num<0)
        num*=-1;
    int l=log10(num); // l = (length of number -1)

    fdigit=num/pow(10,l);

    cout<<fdigit<<endl;
    return 0;

【讨论】:

谢谢。但它有时并不真正起作用。请参阅 Mihai Maruseac 的上述答案。【参考方案8】:
int FirstDigit ( int Number ) 

    // to obtain the <number of digits -1> use this math formula:

    int DigitsInNumber = (int) lg(Number);           

    long TenExpon = pow(10, DigitsInNumber);

    return (Number / TenExpon);                //first digit

还有:lg(n) = ln(n) / ln(10);

【讨论】:

浮点计算不精确;舍入错误可能导致 10 次方的结果不正确(例如,10000 在我的机器上产生不正确的结果)。幸运的是,它很容易修复。 (由于不接受编辑而重命名变量,仅使用“改进”格式)【参考方案9】:

您的第一个解决方案(假设已知 n >= 0)几乎是最优的,我认为它只能通过使用内联汇编语言来大幅改进。但这只有在处理数百万个此类数字时才值得。

您的第二个解决方案是——我怎样才能把它很好地表达出来? ——更像是一种 Java 风格的方法:性能?拉迪达,谁在乎……

【讨论】:

【参考方案10】:

    for(int i=0; i<n; i++)
      
        e=5; //Specify the number of digits in the number OR Exponential value of 10
        while(e>=0)
           
            tenp=pow(10,e); //#include <math.h>
            if(arr[i]/tenp!=0)
            
                q[i][e]=arr[i]/tenp%10;
            
            e--;
        
    

但是,这段代码的复杂度应该是 O(n^2),这是不可取的。

【讨论】:

【参考方案11】:

另一种解决方案: 以 BCD 格式存储所有值,大端。访问第一个半字节以获得第一个数字

Es.

Decimal value: 265
BCD format: 0010-0110-0101

【讨论】:

【参考方案12】:

首先制作一个保存数字的双变量。然后将该数除以 10 循环使该数连续丢失一位数,直到成为一位数。将 double 变量转换为 int 以将其四舍五入。结果将是之前的第一个十进制数字。

代码将以 O(log(n)) 运行。

#include <iostream>

using namespace std;

int main() 
    double a;
    cin >> a;
    while (true) 
        a /= 10;
        if (a < 10) 
            a = int(a);
            cout << a;
            break;
        
    
    return 0; 

【讨论】:

【参考方案13】:

如果你的数字是 x9x8x7x6x5x4x3x2x1 那么你只需除以 10^8 所以你需要:找到有多少位数的最佳方法。 你可以使用二分搜索/

【讨论】:

以上是关于如何有效地检索数字的第一个十进制数字的主要内容,如果未能解决你的问题,请参考以下文章

LSB和MSB

10进制转2进制不够时在哪一位补0

如何计算大数之间除法的第一个十进制数字?

405. 数字转换为十六进制数

405. 数字转换为十六进制数

K-进制数