1. 数组题目整体设计与思路拆解
1.1 为什么刷算法要从数组开始
数组这个数据结构,看起来简单到不能再简单——连续内存、按下标访问、O(1)随机读取,C语言课第一节就讲完了。但你要是真去刷过题就会明白,数组恰恰是算法面试里最容易被轻视的"隐形杀手"。二分查找的边界条件能让你调一晚上bug,移除元素的原地操作考验的是你对内存布局的理解,滑动窗口的双指针写法稍不留神就越界。
代码随想录把数组放在整个刷题路径的第一天,不是因为它简单,而是因为它是构建算法思维的第一块基石。数组题考的不是语法,是三个底层能力:区间控制能力(你能不能用一个循环不变量把边界锁死)、指针调度能力(双指针、快慢指针、左右指针本质都是对线性结构的坐标管理)、空间意识(原地操作意味着你不能随便开新数组,每一次移动都要算清楚代价)。这三个能力一旦在数组阶段练扎实了,后面链表、字符串、哈希表这些专题都能省一大半力气。
我见过太多人一上来就刷动态规划,结果连状态转移的"前一个位置"都搞不清楚,就是因为数组的基本功没到位。数组是唯一一个你可以在一个下午内把所有经典套路全过一遍的专题,性价比极高。代码随想录day1的安排,本质上就是让你在最快的时间内建立"区间+指针+模拟"这三板斧,后续所有数据结构题目都是在这些底子上加复杂度。
1.2 数组题目的三条主线:查找、移动、模拟
把数组题归类之后你会发现,无论LeetCode上题目怎么变,核心套路就三条线。
第一条线是查找,代表题是二分查找(704)。它的本质是你已经知道数据是有序的,需要利用有序性把O(n)的线性扫描降成O(log n)的折半搜索。难点不是"折半"本身,而是区间的定义——你维护的left和right是闭区间还是开区间?这决定了每一次mid的比较之后,left和right的更新方式必须保持区间语义不变化。这就是代码随想录反复强调的"循环不变量"思想,它贯穿所有二分变种题。
第二条线是移动,代表题是移除元素(27)。它的本质是在不额外申请内存的情况下,把满足条件的元素"搬运"到数组前段。最容易想到的方法是暴力双重循环——每删一个元素,后面的整体前移一位,时间复杂度O(n²)。而双指针(快慢指针)把一次遍历里的"找目标"和"前移元素"两个动作合并,时间复杂度直接降到O(n)。表面上看是优化,实际上是在训练你"用一个指针控制遍历节奏、另一个指针控制写入位置"的能力,这个能力在链表快慢指针、滑动窗口里都通用。
第三条线是模拟,代表题是螺旋矩阵(59)。这种题目没有算法难度,纯粹考验你能不能把"转圈填充"这个过程拆成一个个循环不变式,用四个边界变量(top、bottom、left、right)守住四条边,在循环里逐层收缩。模拟题的训练目标很明确:把复杂过程拆解成简单步骤,并保证每一步的边界条件不重不漏。真实业务里的报表生成、图像处理、网格遍历大量依赖这种能力。
代码随想录day1选的是704(二分查找)和27(移除元素),恰好对应查找和移动两条主线。把这两个题吃透,等于同时拿下了区间思维和双指针思维,再看其他数组题会轻松非常多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心方法拆解:二分查找与双指针的底层逻辑
2.1 二分查找的区间定义:左闭右闭 vs 左闭右开
很多初学者二分查找写不对,根本原因是区间语义混乱。你初始化left = 0, right = nums.size() - 1,那么你维护的就是一个左闭右闭区间 [left, right];如果你初始化right = nums.size(),那维护的就是左闭右开区间 [left, right)。这两个写法都能跑通,但必须在while循环和边界更新时保持一致,混着用必然死循环或者漏判。
代码随想录标准模板用的是左闭右闭:
cpp复制int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1; // 注意:左闭右闭
while (left <= right) { // 左闭右闭需要 <=
int mid = left + (right - left) / 2; // 防止溢出
if (nums[mid] > target) {
right = mid - 1; // 因为mid已经查过了,右边界收缩到mid-1
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
这里几个关键点你得想明白,不然背模板也没用。
第一个是 while (left <= right) 的条件。左闭右闭区间里,left == right 意味着区间里还有一个元素(比如 [1, 1] 表示index=1这一个元素),它有可能是target,所以必须用 <= 把它也查一遍。如果你写成 left < right,那么在区间收缩到只剩一个元素时,循环就跳出了,而这个元素可能正是target,结果就是漏判。
第二个是 right = mid - 1 而不是 right = mid。因为在左闭右闭区间里,nums[mid]已经被比较过了,既然它大于target,它绝不可能是目标元素,所以搜索区间应该是 [left, mid-1],如果你写成 [left, mid],等于把一个已经排除的元素又放回去,可能导致死循环(虽然这道题里不一定死循环,但在其他变种题里是个隐患)。
第三个是 mid = left + (right - left) / 2,而不是 (left + right) / 2。原因很朴实:当left和right都很大时,left + right可能超过int的最大值,造成整数溢出,这是一个经典bug。用减法计算差值再除以2,是写二分查找的必备习惯。
左闭右开是另一种写法:
cpp复制int search(vector<int>& nums, int target) {
int left = 0, right = nums.size(); // 左闭右开
while (left < right) { // 左闭右开用 <
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid; // 右开区间,mid也排除了
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
左闭右开区间 [left, right) 表示right本身不在搜索范围内,所以当nums[mid] > target时,新的搜索范围是 [left, mid),因为mid已经被排除,所以 right = mid(这个mid本身是新的右边界,不包含)。while条件用 < 是因为 left == right 时区间为空。如果你把左闭右闭模板里的 <= 和 mid-1 照搬到左闭右开里,就会漏掉边界元素。
我的建议是:选定一种区间定义,永远用那一种。代码随想录标准模板用的是左闭右闭,我个人也更推荐这个,因为它是人类直觉最自然的选法,调试时更容易推理。面试时就用这一种,别在两种写法之间反复横跳。
2.2 双指针法的本质:快慢指针的分工协作
移除元素这道题,题目要求原地删除所有值
