LeetCode 解題思路:69. Sqrt(x)(平方根)
掌握二分搜尋法求解整數平方根的技巧
目錄
LeetCode 解題思路:Sqrt(x)(平方根)
題目描述
給定一個非負整數 x
,返回 x
的平方根,結果向下取整到最接近的整數。返回的整數必須是非負的。
限制:你不能使用任何內建的指數函數或運算子。
- 例如,在 C++ 中不能使用
pow(x, 0.5)
- 在 Python 中不能使用
x ** 0.5
範例
範例 1:
輸入:x = 4
輸出:2
解釋:4 的平方根是 2,所以返回 2
範例 2:
輸入:x = 8
輸出:2
解釋:8 的平方根是 2.82842...,向下取整後返回 2
限制條件
0 <= x <= 2³¹ - 1
核心概念
1. 問題本質分析
這道題本質上是要找到一個最大的整數 ans
,使得 ans² <= x
。
換句話說,我們要找到滿足以下條件的最大整數:
ans * ans <= x
(ans + 1) * (ans + 1) > x
2. 為什麼適合用二分搜尋?
關鍵洞察:平方根函數是單調遞增的!
如果 a < b
,那麼 √a < √b
。這種單調性使得二分搜尋成為理想的解法。
視覺化理解
尋找 √8 的過程:
數軸: 0---1---2---3---4---5---6---7---8
↑ ↑ ↑
left mid right
檢查 mid = 4: 4² = 16 > 8,太大了!
更新 right = mid - 1 = 3
數軸: 0---1---2---3
↑ ↑ ↑
left mid right
檢查 mid = 1: 1² = 1 < 8,可能的答案
更新 ans = 1, left = mid + 1 = 2
數軸: 2---3
↑ ↑
left right
mid
檢查 mid = 2: 2² = 4 < 8,可能的答案
更新 ans = 2, left = mid + 1 = 3
數軸: 3
↑
left/right/mid
檢查 mid = 3: 3² = 9 > 8,太大了!
更新 right = mid - 1 = 2
結束:left > right,返回 ans = 2
解法一:二分搜尋法(推薦)
思路
- 設定搜尋範圍
[0, x]
- 不斷縮小範圍,找到最大的數使其平方不超過
x
- 記錄所有可能的答案,返回最後一個有效答案
程式碼實現
class Solution:
def mySqrt(self, x: int) -> int:
# 特殊情況處理
if x < 2:
return x
left, right = 0, x
ans = 0
while left <= right:
# 防止溢位的中點計算方式
mid = left + (right - left) // 2
# 計算 mid 的平方
square = mid * mid
if square <= x:
# mid 可能是答案,記錄下來
ans = mid
# 嘗試找更大的數
left = mid + 1
else:
# mid 太大了,縮小範圍
right = mid - 1
return ans
優化版本:縮小搜尋範圍
class Solution:
def mySqrt(self, x: int) -> int:
if x < 2:
return x
# 優化:√x <= x/2 當 x >= 4
left, right = 2, x // 2
while left <= right:
mid = left + (right - left) // 2
square = mid * mid
if square > x:
right = mid - 1
elif square < x:
left = mid + 1
else:
# 找到精確的平方根
return mid
# right 就是答案
return right
執行過程詳解
讓我們詳細追蹤 x = 8
的執行過程:
初始狀態:
x = 8
left = 2, right = 4 (因為 8 // 2 = 4)
步驟 1:
mid = 2 + (4 - 2) // 2 = 3
square = 3 * 3 = 9
9 > 8,太大了
right = 3 - 1 = 2
步驟 2:
left = 2, right = 2
mid = 2
square = 2 * 2 = 4
4 < 8,可以
left = 2 + 1 = 3
步驟 3:
left = 3, right = 2
left > right,結束循環
返回 right = 2
解法二:牛頓迭代法(Newton’s Method)
數學原理
牛頓迭代法是求解方程 f(x) = 0
的經典方法。對於求平方根,我們要解的方程是:
f(t) = t² - x = 0
牛頓迭代公式:
t_{n+1} = t_n - f(t_n) / f'(t_n)
= t_n - (t_n² - x) / (2 * t_n)
= (t_n + x / t_n) / 2
視覺化理解
求 √8 的過程:
初始猜測: t₀ = 8
t₁ = (8 + 8/8) / 2 = 4.5
t₂ = (4.5 + 8/4.5) / 2 ≈ 3.14
t₃ = (3.14 + 8/3.14) / 2 ≈ 2.84
t₄ = (2.84 + 8/2.84) / 2 ≈ 2.83
...
收斂到 √8 ≈ 2.828...
程式碼實現
class Solution:
def mySqrt(self, x: int) -> int:
if x < 2:
return x
# 初始猜測值
t = x
# 當 t² > x 時繼續迭代
while t * t > x:
# 牛頓迭代公式
t = (t + x // t) // 2
return t
浮點數版本(更精確)
class Solution:
def mySqrt(self, x: int) -> int:
if x < 2:
return x
# 使用浮點數提高精度
t = float(x)
epsilon = 1e-10 # 精度要求
while abs(t * t - x) > epsilon:
t = (t + x / t) / 2
return int(t)
解法三:暴力法(不推薦,但有教育意義)
思路
從 1 開始逐一檢查,直到找到第一個平方大於 x
的數。
程式碼實現
class Solution:
def mySqrt(self, x: int) -> int:
if x < 2:
return x
for i in range(1, x + 1):
if i * i > x:
return i - 1
return x # 不會執行到這裡
為什麼不推薦?
時間複雜度是 O(√x),對於大數字會很慢。例如 x = 2147483647
,需要檢查約 46340 次。
解法四:位元操作法(進階)
思路
利用平方根的位元特性,從高位到低位逐位確定答案。
程式碼實現
class Solution:
def mySqrt(self, x: int) -> int:
if x < 2:
return x
# 找到最高位
# 對於 32 位整數,平方根最多 16 位
ans = 0
bit = 1 << 16 # 從 2^16 開始
while bit > 0:
ans |= bit
# 如果 ans² > x,撤銷這一位
if ans * ans > x:
ans ^= bit
bit >>= 1
return ans
常見錯誤與陷阱
錯誤 1:整數溢位
# ❌ 錯誤!可能溢位
def mySqrt(self, x: int) -> int:
left, right = 0, x
while left <= right:
mid = (left + right) // 2 # 當 x 很大時可能溢位
# ...
正確做法:
# ✅ 防止溢位
mid = left + (right - left) // 2
錯誤 2:邊界條件處理不當
# ❌ 錯誤!沒有處理 x = 0 或 1
def mySqrt(self, x: int) -> int:
left, right = 1, x # x = 0 時會出錯
# ...
錯誤 3:返回錯誤的答案
# ❌ 錯誤!應該返回向下取整的結果
def mySqrt(self, x: int) -> int:
# ... 二分搜尋 ...
return mid # 應該返回 ans 或 right
錯誤 4:牛頓法的精度問題
# ❌ 錯誤!整數除法會損失精度
def mySqrt(self, x: int) -> int:
t = x
while t * t > x:
t = (t + x / t) // 2 # 應該是 (t + x // t) // 2
進階思考
1. 求精確的浮點數平方根
def sqrt_float(x: float, precision: float = 1e-10) -> float:
if x < 0:
raise ValueError("不能計算負數的平方根")
if x == 0:
return 0
# 二分搜尋法
left, right = 0.0, max(1.0, x)
while right - left > precision:
mid = (left + right) / 2
if mid * mid > x:
right = mid
else:
left = mid
return (left + right) / 2
2. 求立方根
def cubicRoot(x: int) -> int:
if x == 0:
return 0
# 處理負數
sign = 1 if x > 0 else -1
x = abs(x)
left, right = 0, x
ans = 0
while left <= right:
mid = left + (right - left) // 2
cube = mid * mid * mid
if cube <= x:
ans = mid
left = mid + 1
else:
right = mid - 1
return ans * sign
3. 求 n 次方根
def nthRoot(x: int, n: int) -> int:
if n == 1:
return x
if x == 0:
return 0
left, right = 0, x
ans = 0
while left <= right:
mid = left + (right - left) // 2
# 計算 mid^n,注意防止溢位
power = 1
for _ in range(n):
if power > x // mid: # 防止溢位
power = x + 1
break
power *= mid
if power <= x:
ans = mid
left = mid + 1
else:
right = mid - 1
return ans
4. 判斷是否為完全平方數
def isPerfectSquare(num: int) -> bool:
if num < 0:
return False
root = mySqrt(num)
return root * root == num
複雜度分析
解法一:二分搜尋
- 時間複雜度:O(log x)
- 空間複雜度:O(1)
解法二:牛頓迭代法
- 時間複雜度:O(log x),收斂速度極快(二次收斂)
- 空間複雜度:O(1)
解法三:暴力法
- 時間複雜度:O(√x)
- 空間複雜度:O(1)
解法四:位元操作
- 時間複雜度:O(log x)
- 空間複雜度:O(1)
相關題目推薦
- LeetCode 367: Valid Perfect Square - 判斷完全平方數
- LeetCode 633: Sum of Square Numbers - 平方數之和
- LeetCode 50: Pow(x, n) - 計算 x 的 n 次方
- LeetCode 372: Super Pow - 超級次方
- LeetCode 279: Perfect Squares - 完全平方數
- LeetCode 1539: Kth Missing Positive Number - 二分搜尋應用
實戰技巧總結
1. 二分搜尋模板
def binarySearch(condition):
left, right = min_value, max_value
result = default_value
while left <= right:
mid = left + (right - left) // 2
if condition(mid):
result = mid # 記錄可能的答案
# 根據題目調整搜尋方向
left = mid + 1 # 或 right = mid - 1
else:
# 調整另一個方向
right = mid - 1 # 或 left = mid + 1
return result
2. 防止溢位技巧
# 計算中點
mid = left + (right - left) // 2
# 比較平方
if mid > x // mid: # 等價於 mid * mid > x,但防止溢位
# ...
# 累乘時檢查
product = 1
for _ in range(n):
if product > MAX_VALUE // factor:
# 會溢位
break
product *= factor
3. 調試技巧
def mySqrt(self, x: int) -> int:
print(f"計算 √{x}")
if x < 2:
return x
left, right = 2, x // 2
iterations = 0
while left <= right:
mid = left + (right - left) // 2
square = mid * mid
print(f"迭代 {iterations}: [{left}, {right}], mid={mid}, mid²={square}")
if square > x:
right = mid - 1
elif square < x:
left = mid + 1
else:
print(f"找到精確平方根: {mid}")
return mid
iterations += 1
print(f"返回向下取整結果: {right}")
return right
4. 完整測試案例
def test_mySqrt():
test_cases = [
(0, 0),
(1, 1),
(2, 1),
(3, 1),
(4, 2),
(5, 2),
(8, 2),
(9, 3),
(10, 3),
(15, 3),
(16, 4),
(17, 4),
(100, 10),
(101, 10),
(2147483647, 46340) # 最大測試案例
]
solution = Solution()
for x, expected in test_cases:
result = solution.mySqrt(x)
status = "✅" if result == expected else "❌"
print(f"{status} √{x} = {result} (期望: {expected})")
效能比較
import time
def benchmark_methods(x):
methods = [
("二分搜尋", mySqrt_binary),
("牛頓迭代", mySqrt_newton),
("位元操作", mySqrt_bit)
]
for name, func in methods:
start = time.time()
result = func(x)
elapsed = time.time() - start
print(f"{name}: {result}, 耗時: {elapsed:.6f}秒")
總結
這道平方根問題展示了多種經典演算法:
- 二分搜尋:最通用、最容易理解的方法
- 牛頓迭代法:數學優雅、收斂快速
- 位元操作:利用位元特性的巧妙方法
- 暴力法:雖然低效,但有助於理解問題
關鍵要點:
- 理解問題本質:找最大的數使其平方不超過 x
- 選擇合適的演算法:二分搜尋是最平衡的選擇
- 注意邊界條件:特別是 0 和 1
- 防止整數溢位:使用安全的中點計算
- 返回正確結果:向下取整
練習建議
基礎練習:
- 手動模擬二分搜尋過程
- 實現不同版本的解法
- 處理各種邊界情況
進階練習:
- 實現浮點數版本
- 嘗試其他數值方法(如割線法)
- 優化牛頓法的初始猜測
擴展練習:
- 實現其他根號運算(立方根、n次方根)
- 解決相關的數學問題
- 研究快速平方根倒數演算法
應用練習:
- 在圖形計算中使用(距離計算)
- 在物理模擬中應用
- 優化遊戲引擎中的計算
記住:數學問題往往有多種優雅的解法,選擇最適合場景的那一個!