【LeetCode】69. x 的平方根
文章目录
- 69. x 的平方根
- 题目描述
- 示例 1:
- 示例 2:
- 提示:
- 解题思路
- 问题深度分析
- 问题本质
- 核心思想
- 关键难点分析
- 典型情况分析
- 算法对比
- 算法流程图
- 主算法流程(二分查找)
- 牛顿迭代法流程
- 位运算优化流程
- 复杂度分析
- 时间复杂度详解
- 空间复杂度详解
- 关键优化技巧
- 技巧1:二分查找(最优解法)
- 技巧2:牛顿迭代法
- 技巧3:位运算优化
- 技巧4:袖珍计算器(数学公式)
- 边界情况处理
- 测试用例设计
- 基础测试
- 非完全平方数
- 边界测试
- 大数测试
- 常见错误与陷阱
- 错误1:溢出问题
- 错误2:二分查找边界错误
- 错误3:牛顿迭代不收敛
- 错误4:right初始值太大
- 实战技巧总结
- 进阶扩展
- 扩展1:保留小数位的平方根
- 扩展2:n次方根
- 扩展3:快速平方根倒数(Quake III算法)
- 数学背景
- 牛顿迭代法原理
- 二分查找的数学证明
- 应用场景
- 代码实现
- 测试结果
- 核心收获
- 应用拓展
- 完整题解代码
69. x 的平方根
题目描述
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842…, 由于返回类型是整数,小数部分将被舍去。
提示:
- 0 <= x <= 2^31 - 1
解题思路
问题深度分析
这是一道数值计算问题,核心在于二分查找和牛顿迭代法。虽然题目简单,但涉及到整数平方根、精度控制、溢出处理等多个细节,是理解数值算法和二分查找的经典问题。
问题本质
给定非负整数x,计算并返回其算术平方根的整数部分。关键问题:
- 不能使用内置函数:不能用
pow(x, 0.5)
或x**0.5
- 只保留整数部分:舍去小数部分
- 范围处理:x的范围是
[0, 2^31-1]
- 精度要求:找到最大的整数k,使得
k*k <= x
核心思想
多种解法对比:
- 二分查找:在
[0, x]
范围内二分查找答案 - 牛顿迭代法:利用导数快速逼近平方根
- 位运算优化:利用二进制特性加速计算
- 数学公式:使用指数和对数函数
关键难点分析
难点1:二分查找的边界
- 左边界:0
- 右边界:x(实际上可以优化为
min(x, 46340)
,因为46340^2 < 2^31
) - 终止条件:
left <= right
- 结果选择:返回
right
(最后一个满足条件的值)
难点2:溢出处理
mid * mid
可能溢出int
范围- 解决方案:使用
int64
或改用mid <= x/mid
判断
难点3:牛顿迭代的精度
- 迭代公式:
x(n+1) = (x(n) + a/x(n)) / 2
- 收敛条件:
|x(n+1) - x(n)| < 1
- 初始值选择:
x0 = x
典型情况分析
情况1:完全平方数
输入: x = 4
输出: 2
说明: 2*2 = 4,刚好是完全平方数
情况2:非完全平方数
输入: x = 8
输出: 2
说明: 2*2 = 4 < 8, 3*3 = 9 > 8所以答案是2
情况3:边界值
输入: x = 0
输出: 0输入: x = 1
输出: 1输入: x = 2^31 - 1 (2147483647)
输出: 46340
说明: 46340*46340 = 2147395600 < 214748364746341*46341 = 2147488281 > 2147483647
算法对比
算法 | 时间复杂度 | 空间复杂度 | 特点 |
---|---|---|---|
二分查找 | O(log n) | O(1) | 最优解法,稳定可靠 |
牛顿迭代 | O(log n) | O(1) | 收敛快,精度高 |
位运算 | O(log n) | O(1) | 利用二进制特性 |
袖珍计算器 | O(1) | O(1) | 使用数学函数,不推荐 |
注:二分查找是最推荐的方法
算法流程图
主算法流程(二分查找)
牛顿迭代法流程
位运算优化流程
复杂度分析
时间复杂度详解
二分查找:O(log n)
- 搜索范围:
[0, x]
- 每次折半:log₂(x)
- x最大为2³¹-1,所以最多31次
牛顿迭代:O(log n)
- 二次收敛,速度非常快
- 一般5-6次迭代即可
- 理论复杂度O(log log n)
位运算:O(log n)
- 从最高位开始,逐位确定
- 最多16次迭代(int范围)
空间复杂度详解
所有方法:O(1)
- 只使用常数个变量
- 不需要额外的数据结构
关键优化技巧
技巧1:二分查找(最优解法)
func mySqrt(x int) int {if x < 2 {return x}left, right := 0, xfor left <= right {mid := left + (right-left)/2// 避免溢出,使用除法代替乘法if mid == x/mid {return mid} else if mid < x/mid {left = mid + 1} else {right = mid - 1}}return right
}
优势:
- 逻辑清晰
- 时间O(log n)
- 不会溢出
技巧2:牛顿迭代法
func mySqrt(x int) int {if x < 2 {return x}r := xfor r > x/r {r = (r + x/r) / 2}return r
}
数学原理:
- 求f(y) = y² - x = 0的根
- 迭代公式:y(n+1) = (y(n) + x/y(n)) / 2
- 几何意义:切线法逼近
技巧3:位运算优化
func mySqrt(x int) int {if x < 2 {return x}res := 0// 从2^15开始,因为sqrt(2^31) ≈ 2^15.5bit := 1 << 15for bit > 0 {temp := res + bitif temp <= x/temp {res = temp}bit >>= 1}return res
}
核心思想:
- 从高位到低位逐位确定
- 利用平方根的二进制特性
- 避免乘法溢出
技巧4:袖珍计算器(数学公式)
func mySqrt(x int) int {if x == 0 {return 0}// sqrt(x) = e^(0.5 * ln(x))ans := int(math.Exp(0.5 * math.Log(float64(x))))// 由于浮点数精度问题,需要验证if (ans+1)*(ans+1) <= x {return ans + 1}return ans
}
注意:
- 使用数学库函数
- 可能有精度问题
- 题目要求不使用内置函数
边界情况处理
- x = 0:返回0
- x = 1:返回1
- x = 2:返回1(1² = 1 < 2, 2² = 4 > 2)
- x = 2³¹-1:返回46340
- 完全平方数:如4, 9, 16等
测试用例设计
基础测试
输入: x = 4
输出: 2
说明: 完全平方数
非完全平方数
输入: x = 8
输出: 2
说明: 2² = 4 < 8 < 9 = 3²
边界测试
输入: x = 0
输出: 0输入: x = 1
输出: 1输入: x = 2
输出: 1
大数测试
输入: x = 2147483647 (2³¹-1)
输出: 46340
说明: 46340² = 214739560046341² = 2147488281 > 2147483647
常见错误与陷阱
错误1:溢出问题
// ❌ 错误:mid*mid可能溢出
if mid*mid <= x {left = mid + 1
}// ✅ 正确:使用除法避免溢出
if mid <= x/mid {left = mid + 1
}
错误2:二分查找边界错误
// ❌ 错误:返回left
for left <= right {// ...
}
return left // 错误!// ✅ 正确:返回right
for left <= right {// ...
}
return right // right是最后一个满足条件的值
错误3:牛顿迭代不收敛
// ❌ 错误:可能死循环
for r != (r + x/r)/2 {r = (r + x/r) / 2
}// ✅ 正确:使用大于判断
for r > x/r {r = (r + x/r) / 2
}
错误4:right初始值太大
// ❌ 效率低:right太大
right := x // x可能很大// ✅ 优化:right可以更小
right := min(x, 46340) // sqrt(2^31) ≈ 46340
实战技巧总结
- 二分查找模板:左闭右闭区间,返回right
- 溢出处理:用除法代替乘法判断
- 牛顿迭代:快速收敛,但需要处理整数除法
- 位运算优化:从高位到低位逐位确定
- 边界检查:特殊处理0和1
- 优化右边界:右边界可以设为min(x, 46340)
进阶扩展
扩展1:保留小数位的平方根
// 计算平方根并保留n位小数
func sqrtWithPrecision(x float64, precision int) float64 {if x < 0 {return 0}r := xfor math.Abs(r*r-x) > math.Pow(10, float64(-precision)) {r = (r + x/r) / 2}// 保留指定位数factor := math.Pow(10, float64(precision))return math.Round(r*factor) / factor
}
扩展2:n次方根
// 计算整数n次方根
func nthRoot(x, n int) int {if x < 2 || n < 2 {return x}left, right := 0, xfor left <= right {mid := left + (right-left)/2// 计算mid^npow := 1for i := 0; i < n; i++ {if pow > x/mid {pow = x + 1 // 溢出标记break}pow *= mid}if pow == x {return mid} else if pow < x {left = mid + 1} else {right = mid - 1}}return right
}
扩展3:快速平方根倒数(Quake III算法)
// 快速计算1/sqrt(x)(著名的Quake III算法)
func fastInvSqrt(x float32) float32 {i := math.Float32bits(x)i = 0x5f3759df - (i >> 1)y := math.Float32frombits(i)// 牛顿迭代一次提高精度y = y * (1.5 - 0.5*x*y*y)return y
}
数学背景
牛顿迭代法原理
求解方程 f(x) = x² - a = 0 的根:
- 导数:f’(x) = 2x
- 切线方程:y - f(x₀) = f’(x₀)(x - x₀)
- 与x轴交点:x₁ = x₀ - f(x₀)/f’(x₀)
- 化简:x₁ = x₀ - (x₀² - a)/(2x₀) = (x₀ + a/x₀)/2
收敛性:
- 二次收敛,速度非常快
- 初始值越接近真实值,收敛越快
- 对于平方根,任意正数都能收敛
二分查找的数学证明
不变式:在循环过程中,答案始终在[left, right]
区间内
证明:
- 初始:
left=0, right=x
,答案∈[0,x] - 循环:
- 若
mid² < x
,则答案>mid,更新left=mid+1 - 若
mid² > x
,则答案<mid,更新right=mid-1
- 若
- 终止:
left>right
时,right是最大的满足right²≤x
的整数
应用场景
- 数值计算:科学计算、工程计算
- 图形学:向量归一化、距离计算
- 物理模拟:运动学方程求解
- 机器学习:梯度下降优化
- 游戏开发:碰撞检测、AI寻路
代码实现
本题提供了四种不同的解法,重点掌握二分查找方法。
测试结果
测试用例 | 二分查找 | 牛顿迭代 | 位运算 | 数学公式 |
---|---|---|---|---|
完全平方数 | ✅ | ✅ | ✅ | ✅ |
非完全平方数 | ✅ | ✅ | ✅ | ✅ |
边界测试 | ✅ | ✅ | ✅ | ✅ |
大数测试 | ✅ | ✅ | ✅ | ✅ |
核心收获
- 二分查找:在有序空间中高效搜索
- 牛顿迭代:利用导数快速逼近解
- 溢出处理:用除法代替乘法避免溢出
- 位运算优化:利用二进制特性加速
应用拓展
- 数值计算库实现
- 图形学算法基础
- 优化算法(牛顿法)
- 游戏物理引擎
完整题解代码
package mainimport ("fmt""math"
)// =========================== 方法一:二分查找(最优解法) ===========================// mySqrt 二分查找
// 时间复杂度:O(log n)
// 空间复杂度:O(1)
func mySqrt(x int) int {if x < 2 {return x}left, right := 0, xfor left <= right {mid := left + (right-left)/2// 避免溢出,使用除法代替乘法if mid == x/mid {return mid} else if mid < x/mid {left = mid + 1} else {right = mid - 1}}return right
}// =========================== 方法二:牛顿迭代法 ===========================// mySqrt2 牛顿迭代法
// 时间复杂度:O(log n),实际上是O(log log n),二次收敛
// 空间复杂度:O(1)
func mySqrt2(x int) int {if x < 2 {return x}r := xfor r > x/r {r = (r + x/r) / 2}return r
}// =========================== 方法三:位运算优化 ===========================// mySqrt3 位运算优化
// 时间复杂度:O(log n),最多16次迭代
// 空间复杂度:O(1)
func mySqrt3(x int) int {if x < 2 {return x}res := 0// 从2^15开始,因为sqrt(2^31) ≈ 2^15.5bit := 1 << 15for bit > 0 {temp := res + bitif temp <= x/temp {res = temp}bit >>= 1}return res
}// =========================== 方法四:数学公式(袖珍计算器) ===========================// mySqrt4 数学公式
// 时间复杂度:O(1)
// 空间复杂度:O(1)
// 注意:题目要求不使用内置函数,此方法仅供学习
func mySqrt4(x int) int {if x == 0 {return 0}// sqrt(x) = e^(0.5 * ln(x))ans := int(math.Exp(0.5 * math.Log(float64(x))))// 由于浮点数精度问题,需要验证if (ans+1)*(ans+1) <= x {return ans + 1}return ans
}// =========================== 测试代码 ===========================func main() {fmt.Println("=== LeetCode 69: x的平方根 ===\n")// 测试用例testCases := []struct {x intexpect int}{{0, 0}, // 边界:0{1, 1}, // 边界:1{2, 1}, // 非完全平方数{4, 2}, // 完全平方数{8, 2}, // 示例2{9, 3}, // 完全平方数{15, 3}, // 非完全平方数{16, 4}, // 完全平方数{100, 10}, // 完全平方数{121, 11}, // 完全平方数{144, 12}, // 完全平方数{2147483647, 46340}, // 最大值}fmt.Println("方法一:二分查找")runTests(testCases, mySqrt)fmt.Println("\n方法二:牛顿迭代法")runTests(testCases, mySqrt2)fmt.Println("\n方法三:位运算优化")runTests(testCases, mySqrt3)fmt.Println("\n方法四:数学公式")runTests(testCases, mySqrt4)// 详细示例fmt.Println("\n=== 详细示例 ===")detailedExample()// 算法对比fmt.Println("\n=== 算法步骤对比 ===")compareAlgorithms()
}// runTests 运行测试用例
func runTests(testCases []struct {x intexpect int
}, fn func(int) int) {passCount := 0for i, tc := range testCases {result := fn(tc.x)status := "✅"if result != tc.expect {status = "❌"} else {passCount++}fmt.Printf(" 测试%d: %s ", i+1, status)if status == "❌" {fmt.Printf("x=%d, 输出=%d, 期望=%d\n", tc.x, result, tc.expect)} else {fmt.Printf("sqrt(%d) = %d\n", tc.x, result)}}fmt.Printf(" 通过: %d/%d\n", passCount, len(testCases))
}// detailedExample 详细示例
func detailedExample() {x := 8fmt.Printf("输入: x = %d\n\n", x)// 二分查找过程fmt.Println("方法一:二分查找过程")fmt.Printf(" 初始: left=0, right=%d\n", x)left, right := 0, xstep := 1for left <= right {mid := left + (right-left)/2midSquare := mid * midfmt.Printf(" 步骤%d: left=%d, mid=%d, right=%d, mid²=%d\n",step, left, mid, right, midSquare)if mid == x/mid {fmt.Printf(" 找到答案: %d\n", mid)break} else if mid < x/mid {fmt.Printf(" %d² < %d, 搜索右半部分\n", mid, x)left = mid + 1} else {fmt.Printf(" %d² > %d, 搜索左半部分\n", mid, x)right = mid - 1}step++}fmt.Printf(" 最终答案: %d\n\n", right)// 牛顿迭代过程fmt.Println("方法二:牛顿迭代过程")r := xstep = 1fmt.Printf(" 初始值: r = %d\n", r)for r > x/r {newR := (r + x/r) / 2fmt.Printf(" 步骤%d: r=%d, x/r=%d, 新r=(%d+%d)/2=%d\n",step, r, x/r, r, x/r, newR)r = newRstep++}fmt.Printf(" 最终答案: %d\n\n", r)// 验证答案fmt.Println("验证:")ans := mySqrt(x)fmt.Printf(" %d² = %d <= %d ✓\n", ans, ans*ans, x)fmt.Printf(" %d² = %d > %d ✓\n", ans+1, (ans+1)*(ans+1), x)
}// compareAlgorithms 算法对比
func compareAlgorithms() {x := 100fmt.Printf("计算 sqrt(%d):\n\n", x)// 二分查找fmt.Println("1. 二分查找:")fmt.Println(" - 搜索范围: [0, 100]")fmt.Println(" - 查找过程: 50 -> 25 -> 12 -> 6 -> 9 -> 10")fmt.Printf(" - 结果: %d\n\n", mySqrt(x))// 牛顿迭代fmt.Println("2. 牛顿迭代:")fmt.Println(" - 初始值: 100")fmt.Println(" - 迭代过程: 100 -> 50 -> 26 -> 15 -> 10")fmt.Printf(" - 结果: %d\n\n", mySqrt2(x))// 位运算fmt.Println("3. 位运算:")fmt.Println(" - 从高位开始: bit=32768")fmt.Println(" - 逐位确定: 从2^15到2^0")fmt.Printf(" - 结果: %d\n\n", mySqrt3(x))// 大数测试fmt.Println("大数测试 (x = 2^31 - 1):")maxInt := 2147483647fmt.Printf(" 输入: %d\n", maxInt)fmt.Printf(" 二分查找: %d\n", mySqrt(maxInt))fmt.Printf(" 牛顿迭代: %d\n", mySqrt2(maxInt))fmt.Printf(" 位运算: %d\n", mySqrt3(maxInt))fmt.Printf(" 验证: 46340² = %d\n", 46340*46340)fmt.Printf(" 46341² = %d (溢出int范围)\n", int64(46341)*int64(46341))
}