【算法】 递归实战应用:从暴力迭代到快速幂的优化之路

递归的应用

  • 导读
  • [一、50. `Pow(x, n)`------快速幂](#一、50. Pow(x, n)——快速幂)
    • [1.1 题目介绍](#1.1 题目介绍)
    • [1.2 解题思路](#1.2 解题思路)
    • [1.3 编写代码](#1.3 编写代码)
      • [1.3.1 定义函数](#1.3.1 定义函数)
      • [1.3.2 寻找递归基](#1.3.2 寻找递归基)
      • [1.3.3 寻找递进关系](#1.3.3 寻找递进关系)
      • [1.3.4 组合优化](#1.3.4 组合优化)
    • [1.4 代码测试](#1.4 代码测试)
  • 结语

导读

大家好,很高兴又和大家见面啦!!!

在上一篇内容中,我们通过经典的汉诺塔问题 深入探讨了递归算法的核心思想。汉诺塔问题完美展示了如何将复杂问题分解为相似的子问题,通过递归调用优雅解决 。这种"分而治之"的思维不仅是理解递归的关键,更是算法设计的精髓所在。

今天,我们将继续递归的探索之旅,但这次我们的战场转向了一个看似简单却暗藏玄机的问题------如何高效计算幂运算

当我们面对LeetCode第50题《Pow(x, n)》时,朴素的迭代方法在指数范围达到 − 2 31 ≤ n ≤ 2 31 − 1 -2^{31} \leq n \leq 2^{31} - 1 −231≤n≤231−1 时会显得力不从心。

但正如汉诺塔问题教会我们的,递归的魅力在于它能将复杂问题化繁为简。让我们一同看看,递归思维如何将幂运算的时间复杂度从 O ( n ) O(n) O(n) 优化到 O ( log ⁡ n ) O(\log n) O(logn),体验算法优化带来的思维飞跃!

一、50. Pow(x, n)------快速幂

1.1 题目介绍

相关标签 :递归、数学
题目难度 :中等
题目描述

实现 pow(x, n) ,即计算 x x x 的整数 n n n 次幂函数(即, 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

提示

  • 00.0 < x < 100.0 00.0 < x < 100.0 00.0<x<100.0
  • − 2 31 ≤ n ≤ 2 31 − 1 -2^{31} \leq n \leq 2^{31} - 1 −231≤n≤231−1
  • n n n 是一个整数
  • 要么 x x x 不为零,要么 n > 0 n > 0 n>0 。
  • 1 0 4 ≤ x n ≤ 1 0 4 10^4 \leq x^n \leq 10^4 104≤xn≤104

1.2 解题思路

库函数 pow(x, n)是用于求取 x n x^n xn ,现在我们要实现这么一个库函数,最简单的方法就是------迭代:

c 复制代码
double Pow(int x, int n) {
	double ans = 1.0;
	for (int i = 0; i < n; i++) {
		ans *= x;
	}
	return ans;
}

但是如果直接使用该代码,在 − 2 31 ≤ n ≤ 2 31 − 1 -2^{31} \leq n \leq 2^{31} - 1 −231≤n≤231−1 该范围下,代码是 100 % 100\% 100% 超时的。

因此为了更好的解决该问题规模下的 Pow(x, n) 的模拟实现,这里我们需要学习一个算法------快速幂

快速幂Exponentiation by Squaring )是一种高效计算幂运算的算法 ,特别适用于计算大整数幂(如 a n a^n an )的情况。

它的核心思想将指数进行二进制分解,通过不断平方和乘法来减少计算次数 ,将时间复杂度从朴素的 O ( n ) O(n) O(n) 优化到 O ( log ⁡ n ) O(\log n) O(logn)。

简单的理解就是当我们要计算 x n x^n xn ,我们就需要将其转化为求 x i ∗ x j x^i * x^j xi∗xj ,其中 n , i , j n, i, j n,i,j 这三者之间的关系为:

  • 当 n n n 为偶数时, i = = j i == j i==j
  • 当 n n n 为奇数时, j = = i + 1 j == i + 1 j==i+1

我们在整个计算的过程中,通过不断的将指数 n n n 进行二分,来达到快速求解的目的。

就比如我们要计算 2 8 2^8 28 ,其对应的递归树为:
2^8 2^4 2^4 2^2 2^2 2^1 2^1

整个过程中,我们实际上需要计算的 4 4 4 次,即可求出最终的结果;

但是如果我们要采用迭代的方式实现的话,我们则需要执行 8 8 8 次 ∗ 2 *2 ∗2;

若指数 n n n 为奇数,我们则可以将其转化为: x i ∗ x i ∗ x x^i * x^i * x xi∗xi∗x,也就是说,我们每次进行分解的都是偶次幂,而对应的奇次幂,我们只需要在获取的偶次幂的结果的基础上再乘上一个 x x x 即可解决;

如我们要就算 2 1 7 2^17 217 ,其对应的递归树为:
2^17 2^8 2^9 =
2^8 * 2 2^4 2^4 2^2 2^2 2^1 2^1

接下来我们就来实现这一过程;

1.3 编写代码

1.3.1 定义函数

pow 这个库函数中,只存在两个参数:

  • int x:需要求解的底数 x x x
  • int n:需要求解的指数 n n n

函数的返回值应该是一个 double 指,因此对应的函数定义为:

c 复制代码
double Pow(int x, int n) {
	
}

1.3.2 寻找递归基

在数的幂运算中,下面几个值是不需要进行外计算,便可直接得出结果:

  • x 1 = = x x ^ 1 == x x1==x
  • x 0 = = 1 x ^ 0 == 1 x0==1
  • x − 1 = = 1 x x ^{-1} == \frac{1}{x} x−1==x1

因此我们可以将这三个值作为函数的递归基:

c 复制代码
if (n == -1) {
	return 1 / x;
}
if (n == 0) {
	return 1;
}
if (n == 1) {
	return x;
}

当然我们也可以将 ±2 也作为函数基,这里就看个人的选择;

1.3.3 寻找递进关系

在快速幂中,函数的每次递进都是以 n / 2 n / 2 n/2 的方式进行,即快速幂的递推公式为:

  • 当 n n % 2 == 0 n 时, x n = x n 2 ∗ x n 2 x^n = x^{\frac{n}{2}} * x^{\frac{n}{2}} xn=x2n∗x2n
  • 当 n n % 2 == 1 n 时, x n = x n 2 ∗ x n 2 ∗ x x^n = x^{\frac{n}{2}} * x^{\frac{n}{2}} * x xn=x2n∗x2n∗x

对应的递归代码为:

c 复制代码
	double tmp = Pow(x, n / 2);
	if (n % 2 == 0) {
		return tmp * tmp;
	}
	return tmp * tmp * x;

1.3.4 组合优化

现在我们就已经完成了代码的初步框架:

c 复制代码
double Pow(int x, int n) {
	if (n == -1) {
		return 1 / x;
	}
	if (n == 0) {
		return 1;
	}
	if (n == 1) {
		return x;
	}
	double tmp = Pow(x, n / 2);
	if (n % 2 == 0) {
		return tmp * tmp;
	}
	return tmp * tmp * x;
}

但此时的代码并不完整,我们还需要处理 n < 0 n < 0 n<0 的情况;

特殊情况处理

当 n < 0 n < 0 n<0 时,我们在计算 x n x^n xn 时,实际上就算的是 1 x − n \frac{1}{x^{-n}} x−n1,因此我们不如直接在进行幂运算之前,直接预先处理 n < 0 n < 0 n<0 的情况:

c 复制代码
	if (n < 0) {
		x = 1 / x;
		n = -n;
	}

但是如果直接这样处理,那么当 n = = − 2 31 n == -2^{31} n==−231 时,经过该处理后,此时 n = = 2 31 n == 2 ^{31} n==231 ,这时就会出现整型溢出的情况;

那如何避免这个问题呢?

这里有一个小技巧,我们可以通过进行强制类型转换来将 int 强制转换为 long long

当然,在此题中,我们可以将我们写的递归函数与 leetcode 中给定的接口分开,并且在调用 Pow(x, n) 之前,我们直接通过变量 long long exp 来接收参数 n 的值,之后再对 exp 进行预处理,最后再进行函数调用:

c 复制代码
double myPow(double x, int n) {
	long long exp = n;
	if (n < 0) {
		x = 1 / x;
		exp *= -1;
	}
	return Pow(x, exp);
}

当经过该预处理后,在 Pow(x, n) 的调用中,指数 n 一定大于 0 ,因此我们就可以去掉递归基:

c 复制代码
	if (n == -1) {
		return 1 / x;
	}

参数优化

从题目给定的函数接口我们可以看到,原参数 x 的类型为 double 类型,因此我们所写的函数参数类型也应该与之同步,即,将 int 修改为 double,完整代码如下:

c 复制代码
double Pow(double x, long long n) {
	if (n == 0) {
		return 1;
	}
	if (n == 1) {
		return x;
	}
	double tmp = Pow(x, n / 2);
	if (n % 2 == 0) {
		return tmp * tmp;
	}
	return tmp * tmp * x;
}

double myPow(double x, int n) {
	long long exp = n;
	if (n < 0) {
		x = 1 / x;
		exp *= -1;
	}
	return Pow(x, exp);
}

1.4 代码测试

下面我们就在 leetcode 中测试一下对应的代码:

此时我们就已经实现了 快速幂 的算法实现。这里有朋友可能会说,你这都已经定义了 n == 0 为递归基,那是否就不需要 n == 1 作为递归基了?

这个问题的答案是,可以省略 n == 1 作为递归基,但是我这里加上是为了减少一次递归调用,即当 n > 0 n > 0 n>0 时,函数只需要递进到 n == 1 即可结束;

如果省略了该递归基,那么函数则需要递进到 n == 0 才能结束;

当然,具体是只使用 n == 0 作为递归基,还是同时使用 n == 0n == 1 作为递归基,就看个人的编码习惯了;

结语

通过今天对快速幂算法的深入探讨,我们再次见证了递归思维在解决复杂问题时的强大威力。从汉诺塔问题的分治思想 ,到快速幂的二分策略 ,递归始终贯穿着"化繁为简"的核心哲学。

快速幂算法 不仅是一个高效的数学工具,更是算法优化思维的完美体现。它教会我们,在面对看似简单的问题时,深入思考其内在规律往往能带来数量级的性能提升。从朴素的 O ( n ) O(n) O(n) 迭代到精妙的 O ( log ⁡ n ) O(\log n) O(logn)递归,这种思维跃迁正是算法学习的精髓所在。

更重要的是,我们在实现过程中考虑的边界情况处理整数溢出防范等细节,体现了工程实践中不可或缺的严谨态度。这些经验对于解决实际开发中的复杂问题具有重要的借鉴意义。

递归的世界远不止于此,在接下来的内容中,我们将继续探索递归在更多经典问题中的应用,如二叉树的遍历回溯算法等,进一步深化对这种强大编程范式的理解。

下篇预告:递归在数据结构中的应用------二叉树的深度优先遍历

互动与分享

  • 点赞👍 - 您的认可是我持续创作的最大动力

  • 收藏⭐ - 方便随时回顾这些重要的基础概念

  • 转发↗️ - 分享给更多可能需要的朋友

  • 评论💬 - 欢迎留下您的宝贵意见或想讨论的话题

感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!

相关推荐
Miraitowa_cheems1 小时前
LeetCode算法日记 - Day 101: 最长公共子序列
数据结构·算法·leetcode·深度优先·动态规划
DuHz1 小时前
基于信号分解的FMCW雷达相互干扰抑制——论文阅读
论文阅读·算法·汽车·信息与通信·毫米波雷达
('-')1 小时前
《从根上理解MySQL》第一章学习笔记
笔记·学习·mysql
徐行tag2 小时前
RLS(递归最小二乘)算法详解
人工智能·算法·机器学习
d111111111d2 小时前
STM32外设学习-串口发送数据-接收数据(笔记)
笔记·stm32·学习
南方的狮子先生2 小时前
【C++】C++文件读写
java·开发语言·数据结构·c++·算法·1024程序员节
Alex艾力的IT数字空间3 小时前
完整事务性能瓶颈分析案例:支付系统事务雪崩优化
开发语言·数据结构·数据库·分布式·算法·中间件·php
玖剹3 小时前
二叉树递归题目(一)
c语言·c++·算法·leetcode
ChoSeitaku3 小时前
线代强化NO6|矩阵|例题|小结
算法·机器学习·矩阵