P1: 704. 二分查找
题目核心要求
在升序、无重复 的整型数组 nums 中查找目标值 target:
- 存在则返回对应下标;
- 不存在返回 -1。
示例
- 输入:
nums = [-1,0,3,5,9,12], target = 9→ 输出:4 - 输入:
nums = [-1,0,3,5,9,12], target = 2→ 输出:-1
核心思路:循环不变量(区间定义)
二分查找的核心是明确区间定义(循环不变量),所有边界处理必须贴合该定义。主流两种区间定义对应两种写法,时间复杂度均为 O(log n),空间复杂度 O(1)。
写法1:左闭右闭 [left, right](C++ 版本)
核心规则
- 循环条件:
while (left <= right)(left == right时区间仍有效,包含1个元素); - 边界更新:
nums[mid] > target→right = mid - 1(排除已验证的 mid);nums[mid] < target→left = mid + 1(排除已验证的 mid)。
完整代码
cpp
#include <vector>
using namespace std;
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 左闭右闭,right指向最后一个元素
while (left <= right) {
// 防溢出:等价于 (left+right)/2,避免大数相加越界
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid; // 找到目标,返回下标
}
}
return -1; // 未找到目标
}
};
写法2:左闭右开 [left, right)(C++ 版本)
核心规则
- 循环条件:
while (left < right)(left == right时区间为空,无效); - 边界更新:
nums[mid] > target→right = mid(右区间开,天然排除 mid);nums[mid] < target→left = mid + 1(左区间闭,排除 mid)。
完整代码
cpp
#include <vector>
using namespace std;
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 左闭右开,right指向数组长度(越界位置)
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
};
关键注意点
- mid 计算 :优先用
left + (right - left)/2,避免(left+right)/2导致整数溢出; - 循环条件 :左闭右闭用
<=,左闭右开用<,需严格贴合区间定义; - 边界更新 :左闭右闭需
mid±1,左闭右开仅右边界更新为mid。
P2: 27. 移除元素
题目核心要求
在原地 移除数组 nums 中所有值等于 val 的元素,返回移除后数组的新长度:
- 仅使用 O(1) 额外空间,直接修改输入数组;
- 元素顺序可改变,无需考虑新长度后的数组元素。
示例
- 输入:
nums = [3,2,2,3], val = 3→ 输出:2(前2个元素为 [2,2]); - 输入:
nums = [0,1,2,2,3,0,4,2], val = 2→ 输出:5(前5个元素为 [0,1,3,0,4])。
核心解法
数组元素在内存中连续存储,无法直接删除,只能通过覆盖实现"移除",以下是3种核心解法(优先掌握双指针法)。
解法1:暴力解法(效率低,仅作理解)
核心思路
两层循环:外层遍历数组,找到等于 val 的元素;内层将该元素后方所有元素向前移动一位,覆盖目标值。
- 时间复杂度:O(n²),空间复杂度:O(1)。
C++ 代码
cpp
#include <vector>
using namespace std;
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 找到目标值,后续元素前移
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 前移后当前下标需回退,避免漏检
size--; // 数组有效长度减1
}
}
return size;
}
};
解法2:快慢双指针法(最优,推荐)
核心思路
单循环完成两层循环的工作,定义两个指针:
- 快指针:遍历数组,寻找非 val 的元素(新数组的有效元素);
- 慢指针:指向新数组的下标,接收快指针找到的有效元素。
- 时间复杂度:O(n),空间复杂度:O(1)。
C++ 代码
cpp
#include <vector>
using namespace std;
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0; // 慢指针:新数组下标
for (int fast = 0; fast < nums.size(); fast++) { // 快指针:遍历原数组
if (nums[fast] != val) { // 找到有效元素,覆盖慢指针位置
nums[slow++] = nums[fast];
}
}
return slow; // 慢指针最终值 = 新数组长度
}
};
总结
- 数组"移除"元素的本质是覆盖,而非真正删除;
- 快慢双指针法是最优解(O(n) 时间,O(1) 空间),需熟练掌握;
- 暴力解法仅作理解,实际开发中优先使用双指针法。
P3: 977. 有序数组的平方
题目核心要求
给一个非递减排序 的整数数组 nums,返回由每个数字的平方组成的新数组,要求新数组也按非递减顺序排序。
示例
- 输入:
nums = [-4,-1,0,3,10]→ 输出:[0,1,9,16,100]; - 输入:
nums = [-7,-3,2,3,11]→ 输出:[4,9,9,49,121]。
核心解法
解法1:暴力排序法(基础思路,效率较低)
核心思路
- 遍历数组,将每个元素替换为自身的平方;
- 调用排序函数对平方后的数组排序。
- 时间复杂度:O(n + nlogn)(遍历O(n) + 排序O(nlogn)),空间复杂度:O(1)(原地修改)。
C++ 代码
cpp
#include <vector>
#include <algorithm> // sort函数头文件
using namespace std;
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
// 1. 每个元素平方
for (int i = 0; i < nums.size(); i++) {
nums[i] *= nums[i];
}
// 2. 排序
sort(nums.begin(), nums.end());
return nums;
}
};
解法2:双指针法(最优,推荐)
核心思路
数组本身有序,负数平方后可能成为最大值,因此平方后的最大值一定出现在数组两端(最左/最右)。利用双指针从两端向中间遍历,新数组从后往前填充最大值:
- 左指针
i:指向数组起始位置; - 右指针
j:指向数组末尾位置; - 结果数组指针
k:从末尾开始,依次存放当前最大的平方值。 - 时间复杂度:O(n)(仅一次遍历),空间复杂度:O(n)(需要额外存储结果数组)。
C++ 代码
cpp
#include <vector>
using namespace std;
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int n = nums.size();
vector<int> result(n, 0); // 初始化等长结果数组
int i = 0, j = n - 1, k = n - 1; // i左指针,j右指针,k结果数组指针
while (i <= j) { // 注意i<=j,需处理最后一个元素
int leftSquare = nums[i] * nums[i];
int rightSquare = nums[j] * nums[j];
if (leftSquare > rightSquare) { // 左指针平方更大,存入结果数组
result[k--] = leftSquare;
i++;
} else { // 右指针平方更大,存入结果数组
result[k--] = rightSquare;
j--;
}
}
return result;
}
};
总结
- 暴力法思路简单但效率低,仅适合理解基础逻辑;
- 双指针法利用数组"有序"的特性,将时间复杂度优化至O(n),是最优解;
- 双指针法的核心逻辑:平方后的最大值必在数组两端,从后往前填充结果数组。
209. 长度最小的子数组
题目核心要求
给定由正整数组成的数组 nums 和正整数 target,找出和 ≥ target 的最短连续子数组,返回其长度;若无符合条件的子数组,返回 0。
示例
- 输入:
target = 7, nums = [2,3,1,2,4,3]→ 输出:2(最短子数组为 [4,3])。
核心解法
解法1:暴力解法(超时,仅作理解)
核心思路
两层循环枚举所有连续子数组:
- 外层循环固定子数组起始位置
i; - 内层循环从
i开始累加,直到和 ≥target,记录当前子数组长度。
- 时间复杂度:O(n²)(数据量较大时超时),空间复杂度:O(1)。
C++ 代码
cpp
#include <vector>
#include <climits> // INT32_MAX 头文件
using namespace std;
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = INT32_MAX; // 初始化为极大值
for (int i = 0; i < nums.size(); i++) { // 子数组起始位置
int sum = 0;
for (int j = i; j < nums.size(); j++) { // 子数组终止位置
sum += nums[j];
if (sum >= target) { // 找到符合条件的子数组
result = min(result, j - i + 1);
break; // 最短子数组,无需继续累加
}
}
}
return result == INT32_MAX ? 0 : result; // 无符合条件则返回0
}
};
解法2:滑动窗口(最优,推荐)
核心思路
用单循环+双指针实现"窗口"的滑动,将时间复杂度降至 O(n):
- 右指针
j:遍历数组,作为窗口终止位置,累加元素和; - 左指针
i:窗口起始位置,当窗口和 ≥target时,右移左指针缩小窗口,同时更新最短长度。
- 核心逻辑:窗口和满足条件时,尽可能缩小窗口以找到最短长度。
- 时间复杂度:O(n)(每个元素最多被左右指针各访问一次),空间复杂度:O(1)。
C++ 代码
cpp
#include <vector>
#include <climits>
using namespace std;
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 窗口内元素和
int i = 0; // 窗口起始位置
for (int j = 0; j < nums.size(); j++) { // j为窗口终止位置
sum += nums[j];
// 窗口和满足条件,缩小窗口找最短长度
while (sum >= target) {
int subLen = j - i + 1; // 当前窗口长度
result = min(result, subLen);
sum -= nums[i++]; // 左指针右移,缩小窗口
}
}
return result == INT32_MAX ? 0 : result;
}
};
总结
- 暴力解法思路简单但效率低(O(n²)),无法通过大数据量测试;
- 滑动窗口是最优解(O(n)),核心是"右指针扩展窗口、左指针收缩窗口",通过单循环实现双指针移动;
- 滑动窗口的关键:窗口和满足条件时,尽可能缩小窗口以找到最短长度。
59. 螺旋矩阵II
题目核心要求
给定正整数 n,生成一个 n×n 的正方形矩阵,矩阵包含 1 到 n² 的所有元素,且元素按顺时针螺旋顺序排列,返回该矩阵。
示例
- 输入:
3→ 输出:[[1,2,3],[8,9,4],[7,6,5]]。
核心解法(模拟填充,循环不变量原则)
核心思路
本题无复杂算法,核心是模拟顺时针填充矩阵的过程,关键要遵守「左闭右开」的循环不变量原则,避免边界条件混乱:
- 按「上行从左到右 → 右列从上到下 → 下行从右到左 → 左列从下到上」的顺序,由外向内逐圈填充;
- 每圈填充时,每条边都遵循左闭右开(拐角处留给下一条边填充);
- 若
n为奇数,矩阵中心位置需单独赋值。
- 时间复杂度:O(n²)(遍历填充整个矩阵),空间复杂度:O(1)(仅额外使用少量变量)。
C++ 代码
cpp
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 初始化n×n矩阵
int startx = 0, starty = 0; // 每圈填充的起始位置
int loop = n / 2; // 循环圈数(偶数n填n/2圈,奇数最后单独处理中心)
int mid = n / 2; // 奇数n的中心位置
int count = 1; // 填充的数值
int offset = 1; // 控制每条边的填充长度(每圈收缩1位)
int i, j;
while (loop--) {
i = startx;
j = starty;
// 1. 填充上行:从左到右(左闭右开)
for (; j < n - offset; j++) res[i][j] = count++;
// 2. 填充右列:从上到下(左闭右开)
for (; i < n - offset; i++) res[i][j] = count++;
// 3. 填充下行:从右到左(左闭右开)
for (; j > starty; j--) res[i][j] = count++;
// 4. 填充左列:从下到上(左闭右开)
for (; i > startx; i--) res[i][j] = count++;
// 下一圈起始位置内移1位
startx++;
starty++;
// 下一圈每条边的填充长度收缩1位
offset++;
}
// 奇数n,单独填充中心位置
if (n % 2) res[mid][mid] = count;
return res;
}
};
总结
- 解题核心是遵守「左闭右开」的循环不变量原则,统一边界处理规则,避免逻辑混乱;
- 按「上→右→下→左」的顺序逐圈填充,每圈结束后起始位置内移、填充长度收缩;
- 奇数阶矩阵需单独处理中心位置,偶数阶矩阵无需额外操作。