今天,我正式开启了「代码随想录」的刷题之旅!第一章就是数据结构的基石——数组。说实话,数组既熟悉又陌生,总觉得它很简单,但深究起来,无论是内存原理还是各种花式操作,都大有文章。
按照计划,今天的核心任务是掌握数组的理论基础,并死磕几道经典的 LeetCode 题目。我的目标很明确:不仅要AC,更要理解通透,尤其是二分查找和双指针这两种神级技巧。
一、热身:数组理论基础
开动之前,我先花时间阅读了 Carl 哥关于数组理论的文章。数组在内存中是连续存储的,这个特性是它能够支持快速随机访问(通过索引)的根本原因,也是二分查找等算法高效执行的前提。
- 学习感悟: 以前觉得数组简单,但现在意识到,理解它的内存模型对于写出高性能代码至关重要。
- 理论指南: 代码随想录-数组理论基础
二、主线任务:啃下三道硬骨头
1. LeetCode 704. 二分查找
这是今天的重头戏。二分查找听起来简单,但细节是魔鬼,尤其是区间的开闭问题,很容易让人头晕。今天的目标就是要把 左闭右闭 [left, right] 和 左闭右开 [left, right) 两种写法都彻底掌握,形成肌肉记忆。
- 资源包:
- 题目链接: https://leetcode.cn/problems/binary-search/
- 文章讲解: 代码随想录-704.二分查找
- 视频讲解: B站视频-二分查找
我的实现 1:左闭右闭 [left, right]
这种方式最直观,while 循环条件是 left <= right,因为当 left == right 时,区间 [left, left] 依然有效。
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 定义搜索区间 [left, right]
while left <= right:
mid = (right - left) // 2 + left
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return -1
我的实现 2:左闭右开 [left, right)
这种方式下 right 指向的是开区间边界,所以 while 循环条件是 left < right。当 nums[mid] > target 时,right 要更新为 mid,因为新的搜索区间是 [left, mid),不包含 mid。
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) # 定义搜索区间 [left, right)
while left < right:
mid = (right - left) // 2 + left
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid
else:
return mid
return -1
2. LeetCode 27. 移除元素
这道题是双指针的入门经典。题目要求原地移除元素,并返回新数组的长度。
- 学习感悟: 先写一遍暴力解法,确实能锻炼代码实现能力,但效率太低。双指针法才是精髓,只用一次遍历就解决了问题,时间复杂度从 O ( n 2 ) O(n^2) O(n2) 降到了 O ( n ) O(n) O(n),太优雅了!
- 资源包:
- 题目链接: https://leetcode.cn/problems/remove-element/
- 文章讲解: 代码随想录-27.移除元素
- 视频讲解: B站视频-移除元素
我的实现 1:快慢指针法 (推荐)
我用了快慢指针,slow 指针指向下一个非目标元素要填充的位置,fast 指针负责探索整个数组。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# l 是慢指针,i 是快指针
l = 0
for i in range(len(nums)):
if nums[i] != val:
nums[l] = nums[i]
l += 1
return l
(注:上面这种写法可以当成“栈”或“循环不变量”来看,是最高效、最通用的解法)
我的实现 2:暴力解法
用 while 循环和 del 操作,代码很直接,但在列表/数组中删除元素是个耗时操作。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
i = 0
while i < len(nums):
if nums[i] == val:
del nums[i] # 列表删除操作,时间复杂度较高
else:
i += 1
return len(nums)
我的实现 3:双指针优化(头尾指针)
这种方法也很有趣,当 left 指针找到目标值 val 时,就把它和 right 指针指向的元素交换,然后 right 指针前移。这避免了数据的整体移动。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
# 从左边找第一个等于 val 的元素
while left <= right and nums[left] != val:
left += 1
# 从右边找第一个不等于 val 的元素
while left <= right and nums[right] == val:
right -= 1
# 交换并移动指针
if left < right:
nums[left] = nums[right]
left += 1
right -= 1
return left
3. LeetCode 977. 有序数组的平方
这道题完美地展示了双指针思想的威力。一个非递减数组,平方后最大的数只可能出现在原数组的两端。
- 学习感悟: 利用头尾两个指针,比较它们平方后的大小,然后从后往前填充新数组。思路清晰,代码简洁,一次遍历搞定!
- 资源包:
我的实现:双指针(头尾指针)
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
n = len(nums)
result = [0] * n
left, right = 0, n - 1
k = n - 1 # 指向 result 数组的末尾
while left <= right:
lsq = nums[left] ** 2
rsq = nums[right] ** 2
if lsq < rsq:
result[k] = rsq
right -= 1
else:
result[k] = lsq
left += 1
k -= 1
return result
三、支线任务:选做挑战
感觉状态不错,我顺手把计划中选做的两道题也解决了。它们都是二分查找的变种,很好的练习。
1. LeetCode 35. 搜索插入的位置
题目描述: 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
我的实现
这题的逻辑和二分查找几乎一样,唯一的区别在于如果没找到,left 指针最终停留的位置就是应该插入的位置。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (right - left) // 2 + left
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return left
2. LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置
题目描述: 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。
我的实现
这题的精髓在于构造一个巧妙的二分查找函数,用它来分别寻找 target 的起始位置和 target + 1 的起始位置,从而确定 target 的结束边界。非常巧妙!
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
# 寻找大于等于 target 的第一个数
def search_first_ge(nums: List[int], target: int):
left, right = 0, len(nums) - 1
while left <= right:
mid = (right - left) // 2 + left
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
start = search_first_ge(nums, target)
# 如果起始位置越界,或者该位置的值不等于 target,说明不存在
if start == len(nums) or nums[start] != target:
return [-1, -1]
else:
# 寻找 target + 1 的起始位置,再减 1,就是 target 的结束位置
end = search_first_ge(nums, target + 1) - 1
return [start, end]
总结
今天收获满满!对数组的理解更深了一层,对二分查找的两种写法和双指针的多种应用场景都变得更加熟练。果然,踏踏实实地刷题、总结、记录是进步最快的方式。

195

被折叠的 条评论
为什么被折叠?



