1. 算法训练营首日实战解析
今天开始算法训练营的第一天,我们直接进入三个经典题目的实战:704二分查找、27移除元素和977有序数组的平方。这三个题目看似基础,但涵盖了算法学习中最关键的几个思维模式。我自己在刷题初期就曾在这几个题目上反复栽跟头,后来才明白它们其实是检验是否真正理解数组操作的标准试题。
2. 704. 二分查找深度剖析
2.1 问题本质与边界陷阱
二分查找的题目描述很简单:给定一个升序数组和一个目标值,返回该目标值的索引,不存在则返回-1。但就是这个看似简单的题目,让无数初学者在边界条件上栽跟头。我见过最常见的错误就是while循环的条件写成left <= right还是left < right分不清,以及right = mid还是right = mid - 1的犹豫。
关键要理解搜索区间的定义:
- 左闭右闭区间
[left, right]:此时while条件应为left <= right,因为left == right时区间仍然有效 - 左闭右开区间
[left, right):此时while条件应为left < right,因为left == right时区间已无效
python复制# 左闭右闭写法
def search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return -1
2.2 实战中的易错点
-
整数溢出问题:计算mid时使用
(left + right) // 2在极端情况下可能溢出,更安全的写法是left + (right - left) // 2 -
边界更新逻辑:一定要保持区间定义的一致性。如果初始区间是
[0, len(nums)-1],那么后续更新也必须是闭区间操作 -
循环终止条件:忘记处理空数组的情况,导致索引越界
经验之谈:建议固定使用一种区间定义(推荐左闭右闭),并在代码注释中明确标注区间类型,避免混淆
3. 27. 移除元素的双指针艺术
3.1 暴力解法的局限性
题目要求原地移除所有等于val的元素,返回新长度。最直观的想法是发现目标值后,将后面所有元素前移一位。这种方法时间复杂度O(n²),在大数据量时性能堪忧。
python复制# 暴力解法
def removeElement(nums, val):
i = 0
length = len(nums)
while i < length:
if nums[i] == val:
for j in range(i+1, length):
nums[j-1] = nums[j]
length -= 1
else:
i += 1
return length
3.2 快慢指针的精妙之处
快指针扫描整个数组,慢指针标记有效位置。当快指针遇到非val元素时,将其复制到慢指针位置,然后两者同时前进;遇到val元素时只移动快指针。
python复制def removeElement(nums, val):
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
这种解法将时间复杂度降到了O(n),空间复杂度O(1),是处理数组原地修改问题的经典范式。
3.3 边界情况处理
- 空数组输入:直接返回0
- 全部元素都需要移除:慢指针始终不移动
- 首尾元素匹配:需要验证快指针是否越界
调试技巧:在纸上画出快慢指针移动轨迹,特别关注循环终止时指针的位置关系
4. 977. 有序数组的平方的三种解法
4.1 暴力排序法
最直接的思路是先平方再排序,但这样没有利用输入数组已排序的特性,时间复杂度O(nlogn)。
python复制def sortedSquares(nums):
return sorted([x*x for x in nums])
4.2 双指针最优解
由于原数组有序,平方后的最大值只可能出现在两端。我们可以使用头尾指针比较绝对值,将较大值的平方放入结果数组末尾。
python复制def sortedSquares(nums):
n = len(nums)
result = [0] * n
left, right = 0, n - 1
for i in range(n-1, -1, -1):
if abs(nums[left]) > abs(nums[right]):
result[i] = nums[left] ** 2
left += 1
else:
result[i] = nums[right] ** 2
right -= 1
return result
这种方法时间复杂度O(n),空间复杂度O(n)(用于存储结果),是最优解法。
4.3 变种问题思考
如果允许使用额外空间,也可以先找到正负分界点,然后像合并两个有序数组那样操作。这种方法代码稍复杂,但有助于理解归并排序的思想。
5. 首日训练总结与提升建议
经过这三个题目的训练,我们应该建立起几个重要的算法思维:
- 循环不变量思想:在二分查找中明确区间定义并始终保持一致
- 双指针技巧:快慢指针处理原地修改,头尾指针处理有序数组问题
- 边界条件意识:空数组、全匹配、首尾元素等特殊情况必须考虑
建议在每道题目AC后,尝试以下进阶练习:
- 二分查找:实现查找左边界和右边界的版本
- 移除元素:尝试用交换法减少赋值操作次数
- 有序数组平方:思考如果存在重复元素时如何优化
最后分享一个调试小技巧:对于数组问题,可以准备几个特殊测试用例:
- 空数组
- 单元素数组
- 全相同元素数组
- 首尾元素特殊的数组
- 大规模随机数组(验证性能)