剑指Offer_#16_数值的整数次方

剑指Offer_#16_数值的整数次方

Contents

题目

实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。

示例 1:
输入: 2.00000, 10
输出: 1024.00000

示例 2:
输入: 2.10000, 3
输出: 9.26100

示例 3:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25
 
说明:
-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。

思路分析

边界条件

首先需要注意几个边界条件:

  • 指数是0,直接返回1
  • 指数是负数,转换为底数的倒数求正数次幂
  • 底数是0,直接返回0
  • 底数是0 且 指数是负数,报错,或者返回一个确定值,比如0,不能计算0的倒数,会导致异常。

方法1:循环求幂

这一题的简单思路是,循环exponent轮,每次向结果累乘一个base,复杂度是O(n)。
根据评论区,这种方式会超时,所以直接不予考虑。

方法2:快速幂

快速幂算法来源于二分法。例如我们想要求解x16,可以先求解x8,然后计算x8 * x8即可。x8又可由x4求得...依次类推,是一个递归问题。
递归问题就是求解xn,可以分解为递归子问题,需要根据奇偶分类讨论:

  • 当n为偶数:
  • 当n为奇数:

算法具体的过程可以参考如下图解
快速幂算法图解
在整个循环过程中,

  • 幂次n每次降低1倍(也可以表示为右移一位),底数x每次平方。
  • 结果值res初始值为1,每逢n为奇数,则将当前的x乘入res:
    • xn * res的值是一直不变的,但是其中的两个部分xn和res都是一直在改变的,xn从初始值变为1,res从1变为要求的xn
    • 当n变为0,循环结束。

一个不太好理解的地方:
看似在n为偶数的时候,res值没有进行变化,其实不然,n为偶数的时候,x值依然会平方,在下一次遇到n为奇数时,就会乘到res当中。
因为无论如何,总是会遇到n为奇数,也就是倒数第二轮循环时,n必然是1,即使之前一直没有遇到奇数,不断累乘得到的x会在最后一次乘入res当中。

解答

class Solution {
    public double myPow(double x, int n) {
        if(x == 0) return 0;
        if(n == 0) return 1;
        long b = n;//防止对n取负数的时候,超过int表示最大值
        double res = 1;
        if(b < 0){
            b = -b;
            x = 1 / x;
        }
        while(b != 0){
            if((b & 1) == 1) res = res * x;
            x *= x;
            b = b >> 1;//注意不能写成b >> 1,必须要有赋值符号;这里也可以写无符号右移>>>
        }
        return res;
    }
}

复杂度分析

时间复杂度:
空间复杂度:

解答2

尝试递归写法,大部分case都可以通过,但是对于输入为x=0.00001,n=2147483647时会出现执行时间超限。(2147483647是int类型最大值)

时间超限的写法

class Solution {
    public double myPow(double x, int n) {
        if(x == 0) return 0;
        if(n == 0) return 1;
        long b = n;//防止对n取负数的时候,超过int表示最大值
        double res = 1;
        if(b < 0){//处理指数为负数的情况,转化为指数为正的情况
            b = -b;
            x = 1 / x;
        }
        return myPowHelper(x,b);//在这里,输入的b必然是正数     
    }
    public double myPowHelper(double x, long b){
        if((b & 1) == 1) return myPowHelper(x,b >> 1)*myPowHelper(x,b >> 1)*x;    
        else return myPowHelper(x,b >> 1)*myPowHelper(x,b >> 1);
    }
}

正确写法

class Solution {
    public double myPow(double x, int n) {
        if(x == 0) return 0;//防止底数为0且指数为负的情况
        long b = n;//防止对n取负数的时候,超过int表示最大值
        if(b < 0){//处理指数为负数的情况,转化为指数为正的情况
            b = -b;
            x = 1 / x;
        }
        return myPowHelper(x,b);//在这里,输入的b必然是正数
    }
    public double myPowHelper(double x, long b){
        if(b == 0) return 1;
        if((b % 2) == 0){
            double sqaure_root = myPowHelper(x , b / 2);
            return sqaure_root * sqaure_root;
        }
        else{
            double sqaure_root = myPowHelper(x , b / 2);
            return sqaure_root * sqaure_root * x;
        }
    }
}

我在ide尝试运行两种代码,发现确实第二种是肉眼可见地比第一种快,但是不知道原因。

我研究了一下几个可能影响结果的因素

  1. 对于指数的下降,用右移符号>> vs 用除号/
    无论改成除号还是右移符号,leetcode上提交的运行时间都是差不多的。所以其实用右移或者除号,代码效率差别不大。
  2. 递归出口条件,用b==0 vs 用b==1
    我想了一下,都可以。
    • b==0:b==0的时候,返回1,那么调用b==0子递归函数的父函数必定是b==1, 这个父函数的返回值一定是1*1*x
    • b==1:b==1的时候,返回x,效果跟上边的差不多,只不过少调用一层子递归函数
  3. 递归函数返回之前是否使用中间变量(这里的square_root变量)
    • 区别在于,如果不使用中间变量,直接返回myPowHelper(x,b >> 1)*myPowHelper(x,b >> 1)或者myPowHelper(x,b >> 1)*myPowHelper(x,b >> 1)*x,其实相当于进行了两次递归调用,而每次递归调用都又有31层,复杂度增加了不少。
    • 使用了中间变量square_root存储平方根,那么递归调用过程只有一次,返回平方值的时候,是直接用计算好的结果进行相乘。

结论
在递归函数中,要避免重复调用递归函数进行计算,这是递归代码的优化思路,跟爬楼梯一题是类似的。

复杂度分析

递归写法和迭代写法的复杂度一样。
时间复杂度:
空间复杂度:

总结

  • 这一题看似简单,如果不在leetcode上,直接让我写一个这样的函数,我肯定就用暴力循环写了,但是在leetcode中,暴力循环思路行不通。需要懂得快速幂算法。
  • 学会快速幂算法的思想还不够,这一题的边界条件也需要一个不漏的考虑好:
    • 输入指数为负的情况,底数为0的情况,底数为负 且 指数为0 的情况,指数为0的情况
    • 衍生问题:输入的最小负数,转换为正数时会超出int表示范围,需要设置新的long类型变量来处理
  • 上述一切问题都考虑好了,如果采取递归写法,还要避免出现重复调用递归函数的问题,否则会超出时间限制。

所以其实练习leetcode的最大作用也就是在于学习错综复杂的边界条件的处理,以及代码效率优化的方法。《剑指offer》书中也对此做了详细的阐述,在面试中,我们的代码需要做到既全面又高效,两个条件缺一,就不会令面试官满意。

原文地址:https://www.cnblogs.com/Howfars/p/13175005.html