How to check if an integer is a power of 3?

前端 未结 23 1128
没有蜡笔的小新
没有蜡笔的小新 2020-12-02 08:15

I saw this question, and pop up this idea.

23条回答
  •  暖寄归人
    2020-12-02 08:38

    I'm surprised at this. Everyone seems to have missed the fastest algorithm of all.

    The following algorithm is faster on average - and dramatically faster in some cases - than a simple while(n%3==0) n/=3; loop:

    bool IsPowerOfThree(uint n)
    {
      // Optimizing lines to handle the most common cases extremely quickly
      if(n%3 != 0) return n==1;
      if(n%9 != 0) return n==3;
    
      // General algorithm - works for any uint
      uint r;
      n = Math.DivRem(n, 59049, out r); if(n!=0 && r!=0) return false;
      n = Math.DivRem(n+r, 243, out r); if(n!=0 && r!=0) return false;
      n = Math.DivRem(n+r,  27, out r); if(n!=0 && r!=0) return false;
      n += r;
      return n==1 || n==3 || n==9;
    }
    

    The numeric constants in the code are 3^10, 3^5, and 3^3.

    Performance calculations

    In modern CPUs, DivRem is a often single instruction that takes a one cycle. On others it expands to a div followed by a mul and an add, which would takes more like three cycles altogether. Each step of the general algorithm looks long but it actually consists only of: DivRem, cmp, cmove, cmp, cand, cjmp, add. There is a lot of parallelism available, so on a typical two-way superscalar processor each step will likely execute in about 4 clock cycles, giving a guaranteed worst-case execution time of about 25 clock cycles.

    If input values are evenly distributed over the range of UInt32, here are the probabilities associated with this algorithm:

    • Return in or before the first optimizing line: 66% of the time
    • Return in or before the second optimizing line: 89% of the time
    • Return in or before the first general algorithm step: 99.998% of the time
    • Return in or before the second general algorithm step: 99.99998% of the time
    • Return in or before the third general algorithm step: 99.999997% of the time

    This algorithm outperforms the simple while(n%3==0) n/=3 loop, which has the following probabilities:

    • Return in the first iteration: 66% of the time
    • Return in the first two iterations: 89% of the time
    • Return in the first three iterations: 97% of the time
    • Return in the first four iterations: 98.8% of the time
    • Return in the first five iterations: 99.6% of the time ... and so on to ...
    • Return in the first twelve iterations: 99.9998% of the time ... and beyond ...

    What is perhaps even more important, this algorithm handles midsize and large powers of three (and multiples thereof) much more efficiently: In the worst case the simple algorithm will consume over 100 CPU cycles because it will loop 20 times (41 times for 64 bits). The algorithm I present here will never take more than about 25 cycles.

    Extending to 64 bits

    Extending the above algorithm to 64 bits is trivial - just add one more step. Here is a 64 bit version of the above algorithm optimized for processors without efficient 64 bit division:

    bool IsPowerOfThree(ulong nL)
    {
      // General algorithm only
      ulong rL;
      nL = Math.DivRem(nL, 3486784401, out rL); if(nL!=0 && rL!=0) return false;
      nL = Math.DivRem(nL+rL,   59049, out rL); if(nL!=0 && rL!=0) return false;
      uint n = (uint)nL + (uint)rL;
      n = Math.DivRem(n,   243, out r); if(n!=0 && r!=0) return false;
      n = Math.DivRem(n+r,  27, out r); if(n!=0 && r!=0) return false;
      n += r;
      return n==1 || n==3 || n==9;
    
    }
    

    The new constant is 3^20. The optimization lines are omitted from the top of the method because under our assumption that 64 bit division is slow, they would actually slow things down.

    Why this technique works

    Say I want to know if "100000000000000000" is a power of 10. I might follow these steps:

    1. I divide by 10^10 and get a quotient of 10000000 and a remainder of 0. These add to 10000000.
    2. I divide by 10^5 and get a quotient of 100 and a remainder of 0. These add to 100.
    3. I divide by 10^3 and get a quotient of 0 and a remainderof 100. These add to 100.
    4. I divide by 10^2 and get a quotient of 1 and a remainder of 0. These add to 1.

    Because I started with a power of 10, every time I divided by a power of 10 I ended up with either a zero quotient or a zero remainder. Had I started out with anything except a power of 10 I would have sooner or later ended up with a nonzero quotient or remainder.

    In this example I selected exponents of 10, 5, and 3 to match the code provided previously, and added 2 just for the heck of it. Other exponents would also work: There is a simple algorithm for selecting the ideal exponents given your maximum input value and the maximum power of 10 allowed in the output, but this margin does not have enough room to contain it.

    NOTE: You may have been thinking in base ten throughout this explanation, but the entire explanation above can be read and understood identically if you're thinking in in base three, except the exponents would have been expressed differently (instead of "10", "5", "3" and "2" I would have to say "101", "12", "10" and "2").

提交回复
热议问题