@[TOC](力扣 50. Pow(x, n) 中等)
前言
这是刷算法题的第十一天,用到的语言是JS
题目:力扣 50. Pow(x, n) (中等)
一、题目内容
实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,xn )。
示例 1:
输入:x = 2.00000, n = 10
输出:1024.00000
示例 2:
输入:x = 2.10000, n = 3
输出:9.26100
示例 3:
输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25
提示:
- -100.0 < x < 100.0
- -231 <= n <= 231-1
- n 是一个整数
- 要么 x 不为零,要么 n > 0 。
- -104 <= xn <= 104
二、解题方法
1. 快速幂运算(使用除模运算)
正常运算
代码如下(实例):
/**
* @param {number} x
* @param {number} n
* @return {number}
*/
var myPow = function (x, n) {
// 有负数,先处理特殊情况
// 如果指数是负数,则 将底数取倒数,并且指数变正
// 即 x^(-n) = 1 / (x^n) = (1/x)^n
if (n === 0) return 1.00000
if (n < 0) {
x = 1 / x
n = -n
}
let ans = 1
if (-100.0 > x || x > 100.0) return 0.00000
if (-(2 ** 31) > n || n > (2 ** 31)) return 0.00000
if (x === 0 && n > 0) return 0.00000
while (n) {
if (n % 2 === 1) ans *= x
x *= x
n = Math.floor(n / 2)
}
return ans
};
2. 快速幂运算(使用按位与运算)
有坑:具体表现为指数右移时的判断
n >>= 1 与 n >>>= 1 的区别:下面会讲
- 进行无符号右移1位,此处不能使用有符号右移(>>)
- 当n为-2^31转换成正数时的二进制位"10000000000000000000000000000000" , 如果采用有符号右移时会取最左侧的数当符号即(1),所以返回的结果是 -1073741824
代码如下(实例):
/**
* @param {number} x
* @param {number} n
* @return {number}
*/
var myPow = function (x, n) {
// 有负数,先处理特殊情况
// 如果指数是负数,则 将底数取倒数,并且指数变正
// 即 x^(-n) = 1 / (x^n) = (1/x)^n
if (n === 0) return 1.00000
if (n < 0) {
x = 1 / x
n = -n
}
let ans = 1
if (-100.0 > x || x > 100.0) return 0.00000
if (-(2 ** 31) > n || n > (2 ** 31)) return 0.00000
if (x === 0 && n > 0) return 0.00000
// 错误用法
// while (n) {
// if (n & 1) ans *= x
// x *= x
// n >>= 1 // 符号右移
// }
// 正确用法
while (n) {
if (n & 1) ans *= x
x *= x
n >>>= 1 // 无符号右移
//进行无符号右移1位,此处不能使用有符号右移(>>)
//当n为-2^31转换成正数时的二进制位"10000000000000000000000000000000" , 如果采用有符号右移时会取最左侧的数当符号即(1),所以返回的结果是 -1073741824
}
return ans
};
3. 官方题解
3.1 前言
本题的方法被称为「快速幂算法」,有递归和迭代两个版本。这篇题解会从递归版本的开始讲起,再逐步引出迭代的版本。
当指数 n 为负数时,我们可以计算 x−n 再取倒数得到结果,因此我们只需要考虑 n 为自然数的情况。
3.2 方法一:快速幂 + 递归
「快速幂算法」的本质是分治算法。举个例子,如果我们要计算 x64,我们可以按照:
x → x 2 → x 4 → x 8 → x 16 → x 32 → x 64 x→x^2 →x^4 →x^8 →x^{16} →x^{32} →x^{64} x→x2→x4→x8→x16→x32→x64
的顺序,从 x x x 开始,每次直接把上一次的结果进行平方,计算 6 次就可以得到 x 64 x^{64} x64 的值,而不需要对 x x x 乘 63 次 x x x。
再举一个例子,如果我们要计算 x 77 x^{77} x77 ,我们可以按照:
x → x 2 → x 4 → x 9 → x 19 → x 38 → x 77 x→x^2 →x^4 →x^9 →x^{19} →x^{38} →x^{77} x→x2→x4→x9→x19→x38→x77
的顺序,在 x → x 2 , x 2 → x 4 , x 19 → x 38 x→x^2,x^2→x^4,x^{19}→x^{38} x→x2,x2→x4,x19→x38 这些步骤中,我们直接把上一次的结果进行平方,而在 x 4 → x 9 , x 9 → x 38 , x 38 → x 77 x^4 →x^9, x^{9} →x^{38}, x^{38} →x^{77} x4→x9,x9→x38,x38→x77 这些步骤中,我们把上一次的结果进行平方后,还要额外乘一个 x x x。
直接从左到右进行推导看上去很困难,因为在每一步中,我们不知道在将上一次的结果平方之后,还需不需要额外乘 x。但如果我们从右往左看,分治的思想就十分明显了:
-
当我们要计算 x n x^n xn时,我们可以先递归地计算出 y = x ⌊ n / 2 ⌋ y=x^{⌊n/2⌋} y=x⌊n/2⌋ ,其中 ⌊ a ⌋ ⌊a⌋ ⌊a⌋ 表示对 a 进行下取整;
-
根据递归计算的结果,如果 n 为偶数,那么 x n = y 2 x^n = y^2 xn=y2;如果 n 为奇数,那么 x n = y 2 x x^n = y^2x xn=y2x
-
递归的边界为 n = 0 n=0 n=0,任意数的 0 次方均为 1。
由于每次递归都会使得指数减少一半,因此递归的层数为 O ( l o g n ) O(log n) O(logn),算法可以在很快的时间内得到结果。
// C++
class Solution {
public:
double quickMul(double x, long long N) {
if (N == 0) {
return 1.0;
}
double y = quickMul(x, N / 2);
return N % 2 == 0 ? y * y : y * y * x;
}
double myPow(double x, int n) {
long long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/powx-n/solutions/238559/powx-n-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
:
时间复杂度: O ( l o g n ) O(logn) O(logn),即为递归的层数。
:
空间复杂度: O ( l o g n ) O(logn) O(logn),即为递归的层数。这是由于递归的函数调用会使用栈空间。
链接:力扣本题官方题解
来源:力扣(LeetCode)
3.3 方法二:快速幂 + 迭代
由于递归需要使用额外的栈空间,我们试着将递归转写为迭代。在方法一中,我们也提到过,从左到右进行推导是不容易的,因为我们不知道是否需要额外乘 x。但我们不妨找一找规律,看看哪些地方额外乘了 x x x,并且它们对答案产生了什么影响。
我们还是以 x 77 x^{77} x77作为例子: x → x 2 → x 4 → x 9 → x 19 → x 38 → x 77 x→x^2 →x^4 →x^9 →x^{19} →x^{38} →x^{77} x→x2→x4→x9→x19→x38→x77 并且把需要额外乘 x x x 的步骤打上了 + + + 标记。可以发现:
- x 38 → + x 77 x^{38}→^+ x^{77} x38→+x77中额外乘的 x x x 在 x 77 x^{77} x77中贡献了 x x x;
- x 9 → + x 19 x^{9}→^+ x^{19} x9→+x19中额外乘的 x x x 在之后被平方了 2 2 2次,因此在 x 77 x^{77} x77中贡献了 x 2 2 = x 4 x^{2^2} = x^4 x22=x4;
- x 4 → + x 9 x^{4}→^+ x^{9} x4→+x9中额外乘的 x x x 在之后被平方了 3 3 3次,因此在 x 77 x^{77} x77中贡献了 x 2 6 = x 64 x^{2^6} = x^{64} x26=x64;
下面的代码给出了详细的注释:
// C++
class Solution {
public:
double quickMul(double x, long long N) {
double ans = 1.0;
// 贡献的初始值为 x
double x_contribute = x;
// 在对 N 进行二进制拆分的同时计算答案
while (N > 0) {
if (N % 2 == 1) {
// 如果 N 二进制表示的最低位为 1,那么需要计入贡献
ans *= x_contribute;
}
// 将贡献不断地平方
x_contribute *= x_contribute;
// 舍弃 N 二进制表示的最低位,这样我们每次只要判断最低位即可
N /= 2;
}
return ans;
}
double myPow(double x, int n) {
long long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/powx-n/solutions/238559/powx-n-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
:
时间复杂度: O ( l o g n ) O(logn) O(logn),即为对 n 进行二进制拆分的时间复杂度。
:
空间复杂度: O ( 1 ) O(1) O(1)。
链接:力扣本题官方题解
来源:力扣(LeetCode)
三、符号右移" >>= " 与 无符号右移" >>>= " 的区别
在 JavaScript 中,>>=
是一种位移赋值运算符,表示将一个数向右位移指定的位数,并将结果赋值给该数。>>>
是无符号右位移运算符,它会将符号位(最高位)视为 0,因此用于处理负数时会有不同的结果。
1. 符号右移(>>
)与无符号右移(>>>
)的区别
-
>>
: 符号右移运算符,它会保持符号位(最高位)不变,对于负数来说,会用 1 填充空位。例如,-8
(11111111111111111111111111111000
)右移 1 位会变成-4
(11111111111111111111111111111100
)。 -
>>>
: 无符号右移运算符,会用 0 填充空位,无论原数是负数还是正数。这导致在右移负数时,结果会是一个大的正数。例如,-8
的无符号右移 1 位,结果将是2147483644
(00111111111111111111111111111100
)。
2. >>=
的用途
>>=
是将右移操作与赋值结合的简写形式。例如:
let x = 8; // 二进制表示为 1000
x >>= 1; // 向右位移 1 位
console.log(x); // 输出: 4,二进制为 0100
let y = -8; // 二进制表示为 11111111111111111111111111111000 (32位)
y >>= 1; // 向右位移 1 位
console.log(y); // 输出: -4,二进制为 11111111111111111111111111111100
而对于无符号右移的示例:
let z = -8; // 二进制表示为 11111111111111111111111111111000 (32位)
z >>>= 1; // 无符号右位移 1 位
console.log(z); // 输出: 2147483644,二进制为 00111111111111111111111111111100
3. 总结
>>=
是一种位移赋值操作符,用于将数向右移位并赋值。- 适用于进行位运算时,尤其在处理与二进制数字有关的算法(如加密、压缩等)时非常有用。
- 区别于符号右移
>>
,无符号右移>>>
不保留符号,可以否定负数的符号位。
理解这些运算符对理解 JavaScript 的位运算有非常重要的作用,尤其在需要直接操作底层二进制数据时。
四、可以用>>>= 永久性代替>>=吗
在 JavaScript 中,>>=
和 >>>=
的作用虽然相似,但它们无法彼此完全替代,因为它们处理符号的方式不同。
下面是详细的解释:
1. 何时使用 >>=
或 >>>=
?
- 如果你需要保持负数的符号,并且希望结果也为负数,应该使用
>>=
。 - 如果你希望对数字进行无符号右移,且希望即使是负数也得到正数结果,应该使用
>>>=
。
2. 结论
- 不能用
>>>=
永久性代替>>=
,因为它们的算法及结果不同。 - 根据所需的数值结果来选择合适的操作符:
- 使用
>>=
时结果保持符号(适用于有符号整数)。 - 使用
>>>=
时结果不保持符号(适用于无符号整数)。
- 使用
因此,选择 >>=
还是 >>>=
取决于你的具体需求和你希望如何处理符号。