求整数幂问题

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 * \displaystyle\frac{n}{2}\);否则 \(n = 2 * \displaystyle\frac{n}{2} + 1\)
  • 因此,\(x^0 = 1\)

\[x^n = \begin{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 \rightarrow x^2 \rightarrow x^4 \rightarrow^{+} x^9 \rightarrow^{+} x^{19} \rightarrow x^{38} \rightarrow^{+} x^{77} \]

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

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

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

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

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

\[x^{8 + 1} \rightarrow x^{16 + 2} \rightarrow x^{32 + 4} \rightarrow 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 = \displaystyle\frac{n}{2} + \displaystyle\frac{n}{2}\);否则,\(n = \displaystyle\frac{n}{2} + \displaystyle\frac{n}{2} + 1\)
  • 因此,\(x^0 = 1\)

\[x^n = \begin{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\)
posted @ 2020-10-03 17:55  模糊计算士  阅读(447)  评论(0编辑  收藏  举报