zoukankan      html  css  js  c++  java
  • 剑指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》书中也对此做了详细的阐述,在面试中,我们的代码需要做到既全面又高效,两个条件缺一,就不会令面试官满意。

  • 相关阅读:
    【转】分治法求最大值最小值
    JPA基本实体映射学习
    android权限
    Servlet实现基本文件上传
    Flask项目部署(nginx+supervisor+uwsgi)
    DevExpress学习笔记(三)XtraGrid
    DevExpress学习笔记(五)-以拖拽的方式构建树(TreeList)
    使用ICE建立C++与C#的通讯
    带有Wallet的impdp导入过程
    调用ODAC报BadImageFormatException异常的解决方法
  • 原文地址:https://www.cnblogs.com/Howfars/p/13175005.html
Copyright © 2011-2022 走看看