文章目录
- [69. x 的平方根](#69. x 的平方根)
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 < 2147483647
46341*46341 = 2147488281 > 2147483647
算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 二分查找 | O(log n) | O(1) | 最优解法,稳定可靠 |
| 牛顿迭代 | O(log n) | O(1) | 收敛快,精度高 |
| 位运算 | O(log n) | O(1) | 利用二进制特性 |
| 袖珍计算器 | O(1) | O(1) | 使用数学函数,不推荐 |
注:二分查找是最推荐的方法
算法流程图
主算法流程(二分查找)
是 否 否 是 是 否 是 否 开始: 输入x x < 2? 返回x 初始化left=0, right=x left <= right? 返回right 计算mid = left + right / 2 mid*mid == x? 返回mid mid*mid < x? left = mid + 1 right = mid - 1
牛顿迭代法流程
是 否 是 否 开始: 输入x x < 2? 返回x 初始化r = x 计算新值: r' = r + x/r / 2 r' < r? r = r' 返回r
位运算优化流程
否 是 是 否 开始: 输入x 初始化res=0, bit=1<<15 bit > 0? 返回res temp = res + bit temp*temp <= x? res = temp bit >>= 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:二分查找(最优解法)
go
func mySqrt(x int) int {
if x < 2 {
return x
}
left, right := 0, x
for 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:牛顿迭代法
go
func mySqrt(x int) int {
if x < 2 {
return x
}
r := x
for 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:位运算优化
go
func mySqrt(x int) int {
if x < 2 {
return x
}
res := 0
// 从2^15开始,因为sqrt(2^31) ≈ 2^15.5
bit := 1 << 15
for bit > 0 {
temp := res + bit
if temp <= x/temp {
res = temp
}
bit >>= 1
}
return res
}
核心思想:
- 从高位到低位逐位确定
- 利用平方根的二进制特性
- 避免乘法溢出
技巧4:袖珍计算器(数学公式)
go
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² = 2147395600
46341² = 2147488281 > 2147483647
常见错误与陷阱
错误1:溢出问题
go
// ❌ 错误:mid*mid可能溢出
if mid*mid <= x {
left = mid + 1
}
// ✅ 正确:使用除法避免溢出
if mid <= x/mid {
left = mid + 1
}
错误2:二分查找边界错误
go
// ❌ 错误:返回left
for left <= right {
// ...
}
return left // 错误!
// ✅ 正确:返回right
for left <= right {
// ...
}
return right // right是最后一个满足条件的值
错误3:牛顿迭代不收敛
go
// ❌ 错误:可能死循环
for r != (r + x/r)/2 {
r = (r + x/r) / 2
}
// ✅ 正确:使用大于判断
for r > x/r {
r = (r + x/r) / 2
}
错误4:right初始值太大
go
// ❌ 效率低:right太大
right := x // x可能很大
// ✅ 优化:right可以更小
right := min(x, 46340) // sqrt(2^31) ≈ 46340
实战技巧总结
- 二分查找模板:左闭右闭区间,返回right
- 溢出处理:用除法代替乘法判断
- 牛顿迭代:快速收敛,但需要处理整数除法
- 位运算优化:从高位到低位逐位确定
- 边界检查:特殊处理0和1
- 优化右边界:右边界可以设为min(x, 46340)
进阶扩展
扩展1:保留小数位的平方根
go
// 计算平方根并保留n位小数
func sqrtWithPrecision(x float64, precision int) float64 {
if x < 0 {
return 0
}
r := x
for 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次方根
go
// 计算整数n次方根
func nthRoot(x, n int) int {
if x < 2 || n < 2 {
return x
}
left, right := 0, x
for left <= right {
mid := left + (right-left)/2
// 计算mid^n
pow := 1
for 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算法)
go
// 快速计算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寻路
代码实现
本题提供了四种不同的解法,重点掌握二分查找方法。
测试结果
| 测试用例 | 二分查找 | 牛顿迭代 | 位运算 | 数学公式 |
|---|---|---|---|---|
| 完全平方数 | ✅ | ✅ | ✅ | ✅ |
| 非完全平方数 | ✅ | ✅ | ✅ | ✅ |
| 边界测试 | ✅ | ✅ | ✅ | ✅ |
| 大数测试 | ✅ | ✅ | ✅ | ✅ |
核心收获
- 二分查找:在有序空间中高效搜索
- 牛顿迭代:利用导数快速逼近解
- 溢出处理:用除法代替乘法避免溢出
- 位运算优化:利用二进制特性加速
应用拓展
- 数值计算库实现
- 图形学算法基础
- 优化算法(牛顿法)
- 游戏物理引擎
完整题解代码
go
package main
import (
"fmt"
"math"
)
// =========================== 方法一:二分查找(最优解法) ===========================
// mySqrt 二分查找
// 时间复杂度:O(log n)
// 空间复杂度:O(1)
func mySqrt(x int) int {
if x < 2 {
return x
}
left, right := 0, x
for 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 := x
for 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.5
bit := 1 << 15
for bit > 0 {
temp := res + bit
if 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 int
expect 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 int
expect int
}, fn func(int) int) {
passCount := 0
for 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 := 8
fmt.Printf("输入: x = %d\n\n", x)
// 二分查找过程
fmt.Println("方法一:二分查找过程")
fmt.Printf(" 初始: left=0, right=%d\n", x)
left, right := 0, x
step := 1
for left <= right {
mid := left + (right-left)/2
midSquare := mid * mid
fmt.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 := x
step = 1
fmt.Printf(" 初始值: r = %d\n", r)
for r > x/r {
newR := (r + x/r) / 2
fmt.Printf(" 步骤%d: r=%d, x/r=%d, 新r=(%d+%d)/2=%d\n",
step, r, x/r, r, x/r, newR)
r = newR
step++
}
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 := 100
fmt.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 := 2147483647
fmt.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))
}