求整数幂问题

1、问题与分析

问题:

  • 如何使用较少的乘法次数求 (x^{27})
  • 方法:缓存中间结果,避免重复计算

过程演示:

[x^3 = x * x * x, x^9 = x^3 * x^3 * x^3, x^{27} = x^9 * x^9 * x^9 ]

[x^2 = x * x, x^4 = x^2 * x^ 2, x^8 = x^4 * x^4, x^{16} = x^8 * x^8, x^{27} = x^{16} * x^8 * x^2 * x ]

上面的方法利用的其实就是分治思想:

  • 问题的规模是n,把n分解
  • 如果n是偶数,(n = 2 * displaystylefrac{n}{2});否则 (n = 2 * displaystylefrac{n}{2} + 1)
  • 因此,(x^0 = 1)

[x^n = egin{cases} (displaystyle{x^2})^{frac{n}{2}} quad n is not even \ (displaystyle{x^2})^{frac{n}{2}} * x quad otherwise end{cases} ]

  • 最坏情况下,n始终为奇数

2、代码求解

2.1、递归代码

2.2.1、代码

int power(int x, int n)
{
    if (0 == n)
        return 1;
    if (0 == n % 2)
        return power(x * x, n / 2);

    return power(x * x, n / 2) * x;
}

2.1.2、复杂度分析

  • 时间复杂度:(O(logn)),即为递归的层数
  • 空间复杂度:(O(logn)),即为递归的层数。这是由于递归的函数调用会使用栈空间

2.2、改写递归为迭代

2.2.1、代码

int power(int x, int n)
{
    int res = 1;

    if (0 == n)
        return 1;

    for (; n > 0; n = n >> 1)
    {
        if (1 == n % 2)
            res *= x;
        x *= x;
    }
    return res;
}

2.2.2、分析与理解

这个算法该如何理解,我们可以借助LeetCode的官方题解(https://leetcode-cn.com/problems/powx-n/solution/powx-n-by-leetcode-solution/)来分析:

20201003161328

我想了很长时间,令我困惑的点是在每一次迭代时,n为奇数和n为偶数的情况为什么会是这样处理,现在借助这个题解,我们理解起来会容易很多。

我们把一开始的x给剥离出来,它到最后一次迭代时,它的幂一定是不超过n的最大的2的整数次幂,比如,n为77时,那么x最后的幂就是64,n为60时,x最后的幂就是32;我们可以列一个式子:

[① x ightarrow x^2 ightarrow x^4 ightarrow^{+} x^9 ightarrow^{+} x^{19} ightarrow x^{38} ightarrow^{+} x^{77} ]

[② x ightarrow x^2 ightarrow x^4 ightarrow x^8 ightarrow x^{16} ightarrow x^{32} ightarrow x^{64} ]

我们迭代的顺序是从后往前,所以x的值的变化是这样的:

[③ x^{64} ightarrow x^{32} ightarrow x^{16} ightarrow x^8 ightarrow x^{4} ightarrow x^{2} ightarrow x ]

这里解释一下 (x^{9} ightarrow^{+} x^{19}) 中额外乘的 x 在之后被平方了两次,因此在 (x^{77}) 中贡献了 (x^{2^2} = x^4),我们知道,我们的 x 只保存了 x 的2的乘幂次方(即②式中的各个数),而遇到奇数时,多出来的一个 x 所贡献的次数就保存在了res中,我想,大家所疑惑的就是为什么就要恰好保存迭代到那一次时的 x,我们用式子来解释一下:

我们把 (x^9) 中多出来的一个 x 给拆解出来,那么:

[x^{8 + 1} ightarrow x^{16 + 2} ightarrow x^{32 + 4} ightarrow x^{64 + 8} ]

即,在 (x^{77}) 中贡献了 (x^{2^3} = 8) 。类似地,我们把奇数次的迭代时的需要贡献的值保存在 res 中,最后再与最后的 x 的值合并,就得到最后的结果了。

2.2.3、复杂度分析

  • 循环次数为 (logn)
  • 最好情况下的乘法次数为 (logn) 次:n%2 始终为0
  • 最坏情况下的乘法次数为 (2logn) 次:n%2始终为1
  • 算法时间复杂度为 (O(logn))
  • 算法空间复杂度为 (O(1))

3、其他分解方式(扩展)

3.1、分解方式

  • 问题的规模是n,仍然按照n分解
  • 如果n是偶数,(n = displaystylefrac{n}{2} + displaystylefrac{n}{2});否则,(n = displaystylefrac{n}{2} + displaystylefrac{n}{2} + 1)
  • 因此,(x^0 = 1)

[x^n = egin{cases} displaystyle{x^{frac{n}{2}}} * displaystyle{x^{frac{n}{2}}} quad n is not even \ displaystyle{x^{frac{n}{2}}} * displaystyle{x^{frac{n}{2}}} * x quad otherwise end{cases} ]

  • 最坏情况,还是n始终为奇数的情况

3.2、递归代码

3.2.1、代码

int power(int x, int n)
{
    if (0 == n)
        return 1;
    
    if (0 == n % 2)
        return power(x, n / 2) * power(x, n / 2);
    
    return power(x, n / 2) * power(x, n / 2) * n;
}

3.2.2、复杂度分析

  • 显然,复杂度为 (O(n)),这是很坏的算法

3.2.3、解决办法

代码

int power(int x, int n)
{
    int tmp;

    if (0 == n)
        return 1;
    
    tmp = power(x, n / 2);
    if (0 == n % 2)
        return tmp * tmp;

    return tmp * tmp * x;
}
  • 显然,易知:(T(N) = T(frac{N}{2}) + O(1))
  • 故时间复杂度为:(O(N))

下面是在stackexange网站找到的复杂度证明:

20201003173646

4、以3为底(扩展)

20201003174335

递归代码

int power(int x, int n)
{
    if (n == 0)
        return 1;
    if (n == 1)
        return x;
    if (n == 2)
        return x * x;
    
    if (n % 3 == 0)
        return power(x * x * x, n / 3);
    if (n % 3 == 1)
        return power(x * x * x, n / 3) * x;
    
    return power(x * x * x, n / 3) * x * x;
}

时间复杂度分析:

  • (T(N) = logN)

迭代代码

int power(int x, int n)
{
    int res = 1;
    if (n == 0)
        return 1;
    
    for (; n > 0; n /=3, x = x*x*x)
    {
        if (n % 3 == 1) res *= x;
        if (n % 3 == 2) res *= x*x;
    }
    
    return res;
}

时间复杂度分析:

  • (T(N) = T(N / 3) + 4, T(0) = 0)
原文地址:https://www.cnblogs.com/fanlumaster/p/13764947.html