9

I am solving this problem, in which they ask for the index of the first Fibonacci number of 1000 digits, and my first idea was something similar to:

BigInteger x = 1;
BigInteger y = 1;
BigInteger tmp = 0;

int currentIndex = 2;
while (x.NoOfDigits < 1000)
{
    tmp = x + y;
    y = x;
    x = tmp;
    currentIndex++;
}
return currentIndex;

However, as far as I can tell, there is no method for counting the number of digits of a BigInteger. Is this true? One way of circumventing it is to use the .ToString().Length method of a BigInteger, but I'm told that string processing is slow.

A BigInteger also has a .ToByteArray(), and I thought of converting a BigInteger to a byte array and checking the length of that array - but I don't think that this uniquely determines the number of digits in the BigInteger.

For what it's worth, I implemented another way of solving it, which is manually storing the Fibonacci numbers in array, and which stops as soon as the array is full, and I compared this to the .ToString-based method, which is about 2.5 times slower, but the first method takes 0.1 second, which also seems like a long time.

Edit: I've tested the two suggestions in the answers below (the one with BigInteger.Log and the one with MaxLimitMethod). I get the following run times:

  • Original method: 00:00:00.0961957
  • StringMethod: 00:00:00.1535350
  • BigIntegerLogMethod: 00:00:00.0387479
  • MaxLimitMethod: 00:00:00.0019509

Program

using System;
using System.Collections.Generic;
using System.Numerics;
using System.Diagnostics;

class Program
{
    static void Main(string[] args)
    {
        Stopwatch clock = new Stopwatch();
        clock.Start();
        int index1 = Algorithms.IndexOfNDigits(1000);
        clock.Stop();
        var elapsedTime1 = clock.Elapsed;
        Console.WriteLine(index1);
        Console.WriteLine("Original method: {0}",elapsedTime1);
        Console.ReadKey();

        clock.Reset();
        clock.Start();
        int index2 = Algorithms.StringMethod(1000);
        clock.Stop();
        var elapsedTime2 = clock.Elapsed;
        Console.WriteLine(index2);
        Console.WriteLine("StringMethod: {0}", elapsedTime2);
        Console.ReadKey();

        clock.Reset();
        clock.Start();
        int index3 = Algorithms.BigIntegerLogMethod(1000);
        clock.Stop();
        var elapsedTime3 = clock.Elapsed;
        Console.WriteLine(index3);
        Console.WriteLine("BigIntegerLogMethod: {0}", elapsedTime3);
        Console.ReadKey();

        clock.Reset();
        clock.Start();
        int index4 = Algorithms.MaxLimitMethod(1000);
        clock.Stop();
        var elapsedTime4 = clock.Elapsed;
        Console.WriteLine(index4);
        Console.WriteLine("MaxLimitMethod: {0}", elapsedTime4);
        Console.ReadKey();


    }
}

static class Algorithms
{
    //Find the index of the first Fibonacci number of n digits
    public static int IndexOfNDigits(int n)
    {
        if (n == 1) return 1;
        int[] firstNumber = new int[n];
        int[] secondNumber = new int[n];

        firstNumber[0] = 1;
        secondNumber[0] = 1;
        int currentIndex = 2;

        while (firstNumber[n-1] == 0)
        {
            int carry = 0, singleSum = 0;
            int[] tmp = new int[n]; //Placeholder for the sum
            for (int i = 0; i<n; i++)
            {
                singleSum = firstNumber[i] + secondNumber[i];
                if (singleSum >= 10) carry = 1;
                else carry = 0;

                tmp[i] += singleSum % 10;
                if (tmp[i] >= 10)
                {
                    tmp[i] = 0;
                    carry = 1;
                }
                int countCarries = 0;
                while (carry == 1)
                {
                    countCarries++;
                    if (tmp[i + countCarries] == 9)
                    {
                        tmp[i + countCarries] = 0;
                        tmp[i + countCarries + 1] += 1;
                        carry = 1;
                    }
                    else
                    {
                        tmp[i + countCarries] += 1;
                        carry = 0;
                    }
                }
            }

            for (int i = 0; i < n; i++ )
            {
                secondNumber[i] = firstNumber[i];
                firstNumber[i] = tmp[i];
            }
            currentIndex++;
        }
        return currentIndex;
    }

    public static int StringMethod(int n)
    {
        BigInteger x = 1;
        BigInteger y = 1;
        BigInteger tmp = 0;
        int currentIndex = 2;

        while (x.ToString().Length < n)
        {
            tmp = x + y;
            y = x;
            x = tmp;
            currentIndex++;
        }
        return currentIndex;
    }

    public static int BigIntegerLogMethod(int n)
    {
        BigInteger x = 1;
        BigInteger y = 1;
        BigInteger tmp = 0;
        int currentIndex = 2;

        while (Math.Floor(BigInteger.Log10(x) + 1) < n)
        {
            tmp = x + y;
            y = x;
            x = tmp;
            currentIndex++;
        }
        return currentIndex;
    }

    public static int MaxLimitMethod(int n)
    {
        BigInteger maxLimit = BigInteger.Pow(10, n - 1);
        BigInteger x = 1;
        BigInteger y = 1;
        BigInteger tmp = 0;
        int currentIndex = 2;

        while (x.CompareTo(maxLimit) < 0)
        {
            tmp = x + y;
            y = x;
            x = tmp;
            currentIndex++;
        }
        return currentIndex;
    }
}
HowDoICSharply
  • 547
  • 1
  • 7
  • 20
  • Can you get the size in base 10 by computing `log_10(number)` and ceil the result? Edit: I meant to it this way: http://stackoverflow.com/a/1489928/5296568 . However, be carefull if you really want to compute the `log_10` of big numbers. String processing *may* be faster. Do benchmarks to confirm this. – Maximilian Gerhardt Dec 02 '15 at 20:31
  • @MaximilianGerhardt Thanks. The only Log10 I can find is Math.Log10, which takes a double as input, and I seem to get numerical problems, when I convert my BigInteger to a double. Maybe I can implement my own Log10. – HowDoICSharply Dec 02 '15 at 20:37
  • 4
    No, there's a static function `BigInteger.Log(BigInteger num, double base)` right here: (https://msdn.microsoft.com/de-de/library/dd268364%28v=vs.110%29.aspx). You should also take a note of the other answers posted in the question above, but above *all* things what you should do is **benchmark** everything. The results can be suprising. Maybe, `.ToString().Length` **is** the fastest method. – Maximilian Gerhardt Dec 02 '15 at 20:41
  • @HowDoICSharply The simple way to compute the number of digits is to just keep dividing by 10 until the number is less than 10. The number of times you divided + 1 is the number of digits. – Kyle Dec 02 '15 at 20:42
  • 1
    @Kyle--that doesn't sound very efficient. On a 1000 digit number, you'd have 1000 divisions just to find the length. An alternative way addressing the problem, and in my opinion the fastest (though benchmark tests would be needed), would be to instead of checking the number of digits, set a constant for 10^n, where n is your number of digits (n = 1000), and perform the operations until the sum is greater than that constant. – Russ Dec 02 '15 at 20:48
  • @MaximilianGerhardt thanks, I didn't look the right place. I ran all the suggestions in the comments, together with my original method and the .ToString().Length version. I've edited my post. – HowDoICSharply Dec 02 '15 at 22:29

3 Answers3

9

Provided that x > 0

int digits = (int)Math.Floor(BigInteger.Log10(x) + 1);

will get the number of digits.

Out of curiosity, I tested the

int digits = x.ToString().Length; 

approach. For 100 000 000 iterations, it's 3 times slower than the Log10 solution.

PHeiberg
  • 29,411
  • 6
  • 59
  • 81
  • Thanks, this was exactly what I was looking for. – HowDoICSharply Dec 02 '15 at 22:31
  • Only 3 times as slow? That is not bad for a routine that must divide by 10 so often. – Rudy Velthuis Dec 04 '15 at 12:13
  • 4
    Warning - this method is not always correct. (int)Math.Floor(BigInteger.Log10(1000) + 1) is 3 but the expected result is 4. The reason is that BigInteger.Log10 only provides approximate results. – wize Mar 24 '20 at 16:31
7

Expanding on my comment--instead of testing based on number of digits, test based on exceeding a constant that has the upper limit of the problem:

public static int MaxLimitMethod(int n)
    {
        BigInteger maxLimit = BigInteger.Pow(10, n);
        BigInteger x = 1;
        BigInteger y = 1;
        BigInteger tmp = 0;
        int currentIndex = 2;

        while (x.CompareTo(maxLimit) < 0)
        {
            tmp = x + y;
            y = x;
            x = tmp;
            currentIndex++;
        }
        return currentIndex;
    }

This should result in a significant performance increase.

Rhyous
  • 6,510
  • 2
  • 44
  • 50
Russ
  • 4,091
  • 21
  • 32
  • Thanks. I've tested your solution. For some reason, I don't see an increased performance. It appears to be comparable to the log-method. – HowDoICSharply Dec 02 '15 at 22:31
  • 1
    Wait a minute, I didn't realize the Stopwatch worked like an actual stopwatch. I have to reset it before I start it again. Your method is by far the fastest. I've edited my post to reflect the new times. – HowDoICSharply Dec 02 '15 at 23:54
  • Yep. It's an order of magnitude faster--because there is no math involved in figuring out if you've gone far enough or not. – Russ Dec 03 '15 at 13:46
3

UPDATE:

This is an even quicker method on .NET 5 (since GetBitLength() is required):

private static readonly double exponentConvert = Math.Log10(2);
private static readonly BigInteger _ten = 10;

public static int CountDigits(BigInteger value)
{
    if (value.IsZero)
        return 1;

    value = BigInteger.Abs(value);

    if (value.IsOne)
        return 1;

    long numBits = value.GetBitLength();

    int base10Digits = (int)(numBits * exponentConvert).Dump();
    var reference = BigInteger.Pow(_ten, base10Digits);

    if (value >= reference)
        base10Digits++;

    return base10Digits;
}

The slowest part of this algorithm for large values is the BigInteger.Pow() operation. I have optimized the CountDigits() method in Singulink.Numerics.BigIntegerExtensions with a cache that holds powers of 10, so check that out if you are interested in the fastest possible implementation. It caches powers up to exponents of 1023 by default but if you want to trade memory usage for faster performance on even larger values you can increase the max cached exponent by calling BigIntegerPowCache.GetCache(10, maxSize) where maxSize = maxExponent + 1.

On an i7-3770 CPU, this library takes 350ms to get the digit count for 10 million BigInteger values (single-threaded) when the digit count <= the max cached exponent.

ORIGINAL ANSWER:

The accepted answer is unreliable, as indicated in the comments. This method works for all numbers:

private static int CountDigits(BigInteger value)
{    
    if (value.IsZero)
        return 1;
        
    value = BigInteger.Abs(value);
    
    if (value.IsOne)
        return 1;
    
    int exp = (int)Math.Ceiling(BigInteger.Log10(value));
    var test = BigInteger.Pow(10, exp);
    
    return value >= test ? exp + 1 : exp;
}
Mike Marynowski
  • 3,156
  • 22
  • 32