LeetCode 50. Pow(x, n):从暴力法到快速幂的优化之路

0 阅读6分钟

LeetCode 中经典的幂运算题目——50. Pow(x, n)。这道题看似简单,只需计算 x 的 n 次幂,但隐藏着从“暴力求解”到“高效优化”的核心思路,也是面试中常考的基础算法题,适合新手入门理解“分治思想”和“迭代优化”。

先明确题目要求:实现 pow(x, n),即计算 x 的整数 n 次幂函数(n 可以是正数、负数或 0),无需考虑大数溢出问题(题目默认测试用例在有效范围内)。

一、暴力法:直观但低效的初始思路

拿到题目,最直观的想法就是“重复乘法”——如果 n 是正数,就把 x 连续乘 n 次;如果 n 是负数,就先计算 x 的 |n| 次幂,再取倒数;如果 n 是 0,直接返回 1(任何数的 0 次幂都是 1)。

对应代码就是题目中给出的 myPow_1,我们来逐行解析其逻辑:

function myPow_1(x: number, n: number): number {
  // 边界处理:x为0时,0的任意正次幂都是0(0的0次幂题目默认返回1,这里已被n===0覆盖)
  if (x === 0) return 0;
  // 边界处理:任何数的0次幂都是1
  if (n === 0) return 1;
  // 处理负指数:将负指数转化为正指数,底数变为倒数
  const X = n >= 0 ? x : 1 / x;
  const N = n >= 0 ? n : -n;
  // 初始化结果为底数(因为要乘N次,初始值为X,再乘N-1次)
  let ans = X;
  // 循环N-1次,累计乘法
  for (let i = 1; i < N; i++) {
    ans *= X;
  }
  return ans;
};

暴力法的优点与局限

优点:逻辑简单、代码易写,上手难度低,适合初学者理解幂运算的本质。

局限:时间复杂度太高。当 n 是一个很大的整数(比如 10^9)时,循环需要执行 10^9 - 1 次,这会导致程序超时(LeetCode 的测试用例中,n 可以达到 ±2^31 - 1,暴力法必然过不了)。

举个例子:计算 pow(2, 1000000),暴力法需要循环 999999 次,而优化后的方法只需几十次计算,效率差距天壤之别。因此,我们需要找到一种更高效的计算方式——快速幂。

二、快速幂:分治思想的高效实现

快速幂的核心思路是**“分而治之”**,将幂运算拆分成更小的子问题,通过“指数折半、底数平方”的方式,减少乘法次数。核心原理如下:

对于任意整数 n,我们可以将其拆分为:

  1. 若 n 是偶数:x^n = (x^2)^(n/2) —— 底数平方,指数折半,只需计算 (x²) 的 (n/2) 次幂;

  2. 若 n 是奇数:x^n = x * (x^2)^((n-1)/2) —— 先多乘一次当前底数,再按偶数的逻辑拆分;

  3. 若 n 是负数:x^n = 1 / x^(-n) —— 转化为正指数的倒数,再按上述逻辑计算。

这样一来,每次计算都能将指数规模缩小一半,时间复杂度从暴力法的 O(n) 优化到 O(log n),即使 n 是 10^9,也只需约 30 次计算,效率极大提升。

迭代版快速幂(myPow_2 解析)

题目中给出的 myPow_2 是快速幂的迭代实现(比递归实现更节省栈空间,避免递归深度过大导致栈溢出),我们逐行拆解逻辑:

function myPow_2(x: number, n: number): number {
  // 边界处理:任何数的0次幂都是1
  if (n === 0) return 1;
  // 结果初始化:初始值为1,后续累计乘法
  let res = 1;
  // 取n的绝对值,统一按正指数处理,最后再处理负指数
  let absN = Math.abs(n);

  // 循环条件:指数折半到0时停止
  while (absN > 0) {
    // 若当前指数是奇数,需要多乘一次当前的底数(对应奇数拆分逻辑)
    if (absN % 2 === 1) {
      res *= x;
    }
    // 底数平方(对应指数折半后的底数更新)
    x *= x;
    // 指数折半(向下取整,处理奇数时(n-1)/2等价于Math.floor(n/2))
    absN = Math.floor(absN / 2);
  }

  // 若n是负指数,返回结果的倒数;否则直接返回结果
  return n > 0 ? res : 1 / res;
};

迭代快速幂的执行流程(举例理解)

以 pow(2, 5) 为例,一步步看代码如何执行:

  1. 初始值:res=1,absN=5,x=2;

  2. 第一次循环(absN=5>0):absN是奇数(5%2=1),res=12=2;x=22=4;absN=Math.floor(5/2)=2;

  3. 第二次循环(absN=2>0):absN是偶数,res不变(还是2);x=4*4=16;absN=Math.floor(2/2)=1;

  4. 第三次循环(absN=1>0):absN是奇数,res=216=32;x=1616=256;absN=Math.floor(1/2)=0;

  5. 循环结束,n=5>0,返回res=32(正确结果:2^5=32)。

再举一个负指数例子:pow(2, -3):

  1. 初始值:res=1,absN=3,x=2;

  2. 循环执行后,res=8(对应2^3=8);

  3. n=-3<0,返回1/8=0.125(正确结果:2^-3=1/8)。

三、两种方法对比总结

方法时间复杂度空间复杂度优点缺点
暴力法(myPow_1)O(n)O(1)逻辑简单、代码易写效率低,n 较大时超时
快速幂(myPow_2)O(log n)O(1)效率极高,适配大指数逻辑稍复杂,需理解分治思想

四、关键注意点与拓展

  1. 边界处理:n=0 时,无论 x 是什么(除了 0^0,题目默认返回 1),都返回 1;x=0 时,若 n>0 返回 0,n<0 会出现数学错误(但题目测试用例大概率不会包含这种情况)。

  2. 负指数处理:核心是“转化为正指数 + 取倒数”,避免直接处理负指数导致逻辑混乱。

  3. 指数折半的细节:Math.floor(absN / 2) 等价于整数除法(absN >> 1,位运算效率更高),可以替换优化,但可读性稍差。

  4. 递归版快速幂:除了迭代实现,也可以用递归实现(分治思想更直观),但递归深度会达到 O(log n),当 n 极大时可能出现栈溢出,因此迭代版更推荐在实际开发中使用。

五、总结

LeetCode 50. Pow(x, n) 的核心是“优化幂运算的效率”,从暴力法的 O(n) 到快速幂的 O(log n),本质是分治思想的应用——将大问题拆分成小问题,通过重复利用中间结果减少计算次数。

对于新手来说,建议先理解暴力法的逻辑,再思考“为什么暴力法效率低”,进而推导快速幂的优化思路,结合例子手动模拟执行流程,就能轻松掌握。这道题不仅是一道算法题,更能帮助我们理解“优化”的本质:不是推翻重来,而是找到问题的规律,用更巧妙的方式减少冗余计算。