1.数组理论基础
数组是存放在连续内存空间上的相同类型数据的集合。
需要注意:
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
由于数组在内存空间的地址是连续的,所以在删除或者增添元素的时候,难免要移动其他元素的地址。
所以数组的元素不能删除,只能覆盖。
2.题目
704. 二分查找(二分)
题目描述:
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间
思路
注意到:1.数组为有序数组 2.数组中无重复元素。若有重复元素,则使用二分查找法返回的元素下标可能不是唯一的。
在实现二分查找算法时,处理边界条件是关键之一。通常有两种主要的方式来定义搜索区间:左闭右闭(即[left, right]
)和左闭右开(即[left, right)
)。不同的区间定义会影响循环的终止条件以及如何更新左右边界的逻辑。
左闭右闭([left, right]
)
- 初始化:
left = 0
,right = nums.length - 1
- 循环条件:使用
while (left <= right)
。这里包括了left == right
的情况,因为在左闭右闭的情况下,right
位置的元素也是有效的,需要被检查。 - 边界更新:
-
- 如果目标值大于中间值,则更新
left = mid + 1
。 - 如果目标值小于中间值,则更新
right = mid - 1
。
- 如果目标值大于中间值,则更新
使用这种定义时,由于初始right
设为数组的最大索引,且每次迭代都可能检查到left == right
的位置,因此循环条件应该允许这种情况发生,即left <= right
。
左闭右开([left, right)
)
- 初始化:
left = 0
,right = nums.length
- 循环条件:使用
while (left < right)
。因为right
是开区间,不包含在搜索范围内,所以当left == right
时意味着没有更多元素可以搜索。 - 边界更新:
-
- 如果目标值大于中间值,则更新
left = mid + 1
。 - 如果目标值小于中间值,则更新
right = mid
(注意这里不是mid - 1
,因为right
是开区间)。
- 如果目标值大于中间值,则更新
在这种情况下,right
实际上是超出数组最后一个有效索引的一个值,表示搜索区间的终点不可达。因此,当left
与right
相等时,表示所有可能的位置都已检查完毕,这时循环应当终止,故使用left < right
作为条件。
理解这些边界条件的关键在于明确每次迭代后新的搜索区间的含义,并确保不会遗漏任何可能含有目标值的部分。选择哪种方式更多取决于个人偏好或具体问题的要求,但重要的是要一致地应用所选的方式。无论选择哪种方式,都要保证循环结束后所有的元素都被正确检查过。
题解code
//第一种写法:左开右开 [left,right]
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
while(l <= r)
{
int mid = (l + r) / 2; //也可以写mid = (right - left) / 2 + left;防止溢出
if(nums[mid] > target) r = mid - 1;
else if(nums[mid] < target) l = mid + 1;
else return mid;
}
return -1;
}
};
//第二种写法:左闭右开 [left,right)
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size();
while(l < r)
{
int mid = (l + r) / 2;
if(nums[mid] > target) r = mid; //(注意这里没有减1,因为right是开区间)
else if(nums[mid] < target) l = mid + 1;
else return mid;
}
return -1;
}
};
相关题目
27. 移除元素(双指针)
题目描述:
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,_,_]
解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,_,_,_]
解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
你不需要考虑数组中超出新长度后面的元素。
思路
我们可以采用双指针方法来解决这个问题,以达到O(n)的时间复杂度和O(1)的空间复杂度。具体来说,使用两个指针——快指针(fast)遍历整个数组,慢指针(slow)记录下一个非val
元素应该放置的位置。当快指针遇到非val
元素时,将其值赋给慢指针位置,并将慢指针向前移动一位。这样,当快指针遍历完数组时,慢指针的位置即为新数组的长度,且[0, slow)区间内的元素就是去除val
后的结果。
题解code
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];
slow++;
}
}
return slow;
}
};
相关题目
977.有序数组的平方(双指针)
题目描述:
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
- 输入:nums = [-4,-1,0,3,10]
- 输出:[0,1,9,16,100]
- 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]
示例 2:
- 输入:nums = [-7,-3,2,3,11]
- 输出:[4,9,9,49,121]
思路
考虑到输入数组是有序的,但是包含了负数,其平方后的结果可能比未平方前的任何数都要大。因此,直接对原数组元素进行平方然后排序并不是最优解。更高效的方法是从数组的两端向中间遍历,比较两端元素平方后的大小,并将较大的值放入结果数组的末尾,这样可以在线性时间内完成任务。
题解code
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int n = nums.size();
int left = 0, right = n - 1;
vector<int> res(n,0);
for(int i = n - 1;i >= 0;--i)
{
if(abs(nums[left]) > abs(nums[right]))
{
res[i] = nums[left] * nums[left];
left++;
}
else
{
res[i] = nums[right] * nums[right];
right--;
}
}
return res;
}
};
209.长度最小的子数组(滑动窗口双指针)
题目描述:
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的 子数组 [nums l, nums l+1, ..., nums r-1, nums r]
,并返回其长度。如果不存在符合条件的子数组,返回 0
。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
思路
此问题可以通过滑动窗口的方法来解决。具体步骤如下:
- 初始化两个指针
left
和right
,分别代表滑动窗口的左右边界,初始时都指向数组的第一个元素。 - 维护一个变量
sum
来存储当前窗口内所有元素的和,另一个变量minLen
存储满足条件的最小子数组长度,初始化为无穷大。 - 移动右指针扩大窗口,直到窗口内的元素和不小于
target
。 - 当找到一个满足条件的子数组后,尝试通过移动左指针缩小窗口,并更新
minLen
。 - 重复上述过程,直到右指针遍历完整个数组。
题解code
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int minLen = INT_MAX;
int left = 0;
int sum = 0;
for(int right = 0;right < n;++right)
{
sum += nums[right];
while(sum >= target)
{
minLen = min(minLen, right - left + 1);
sum -= nums[left];
left++;
}
}
return minLen == INT_MAX ? 0 : minLen;
}
};
- 该算法的时间复杂度为 O(n),因为每个元素最多被访问两次(一次由右指针,另一次由左指针)。
- 不要认为for里放一个while就以为是O(n^2), 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
相关题目
59.螺旋矩阵II(模拟矩阵)
题目描述:
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
思路
通过模拟螺旋填充过程来解决。我们可以定义四个边界(上、下、左、右),然后按层遍历,依次填充每一层的顶部、右侧、底部和左侧,直到整个矩阵被填满。
这里一圈下来,我们要画每四条边,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
题解code
//写法一
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> matrix(n,vector<int>(n));
int val = 1,left = 0,right = n - 1,top = 0,bottom = n - 1;
while(left <= right && top <= bottom)
{
for(int col = left;col <= right;++col)
{
matrix[top][col] = val++;
}
top++;
for(int row = top;row <= bottom;++row)
{
matrix[row][right] = val++;
}
right--;
if(top <= bottom)
{
for(int col = right;col >= left;--col)
{
matrix[bottom][col] = val++;
}
bottom--;
}
if(left <= right)
{
for(int row = bottom;row >= top;--row)
{
matrix[row][left] = val++;
}
left++;
}
}
return matrix;
}
};
//写法二:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j; j < n - offset; j++) {
res[i][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
相关题目
区间和(前缀和)
题目描述:
给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。
输入描述
第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间,直至文件结束。
输出描述
输出每个指定区间内元素的总和。
输入示例
5
1
2
3
4
5
0 1
1 3
输出示例
3
9
数据范围:
0 < n <= 100000
思路
简单前缀和
题解code
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n;
scanf("%d",&n);
vector<int> arr(n + 1,0);
vector<int> preSum(n + 1,0);
for(int i = 0;i < n;++i)
{
scanf("%d",&arr[i]);
if(i == 0) preSum[i] = arr[i];
else preSum[i] = preSum[i-1] + arr[i];
}
int l,r;
while(scanf("%d%d",&l,&r) != EOF)
{
printf("%d\n",preSum[r] - preSum[l-1]);
}
return 0;
}
开发商购买土地(前缀和)
题目描述:
在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。
现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。
然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。
为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。
注意:区块不可再分。
【输入描述】
第一行输入两个正整数,代表 n 和 m。
接下来的 n 行,每行输出 m 个正整数。
输出描述
请输出一个整数,代表两个子区域内土地总价值之间的最小差距。
【输入示例】
3 3 1 2 3 2 1 3 1 2 3
【输出示例】
0
【提示信息】
如果将区域按照如下方式划分:
1 2 | 3 2 1 | 3 1 2 | 3
两个子区域内土地总价值之间的最小差距可以达到 0。
【数据范围】:
- 1 <= n, m <= 100;
- n 和 m 不同时为 1。
思路
前缀和
题解code
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main()
{
int n,m;
cin >> n >> m;
vector<vector<int>> arr(n,vector<int>(m,0));
int sum = 0;
for(int i = 0;i < n;++i)
{
for(int j = 0;j < m;++j)
{
cin >> arr[i][j];
sum += arr[i][j];
}
}
int res = INT_MAX;
int rowSum = 0, colSum = 0;
for(int i = 0;i < n;++i)
{
for(int j = 0;j < m;++j)
{
rowSum += arr[i][j];
if(j == m - 1) res = min(res,abs((sum - rowSum) - rowSum));
}
}
for(int j = 0;j < m;++j)
{
for(int i = 0;i < n;++i)
{
colSum += arr[i][j];
if(i == n - 1) res = min(res,abs((sum - colSum) - colSum));
}
}
cout << res << '\n';
return 0;
}
3.总结
一、核心解题技巧
1.二分查找法
-
- 适用场景:有序数组的查找问题(如704.二分查找、35.搜索插入位置)12
- 关键点:
-
-
- 区间定义:左闭右闭区间(
left<=right
,更新时right=mid-1
) vs 左闭右开区间(left<right
,更新时right=mid
) - 防溢出计算:中间值计算用
mid = left + (right-left)/2
代替(left+right)/2
- 拓展应用:查找元素边界(如34题),通过两次二分分别找左界和右界
- 区间定义:左闭右闭区间(
-
2.双指针法
-
- 快慢指针:用于原地修改数组(如27.移除元素),快指针扫描元素,慢指针记录保留位置
- 左右指针:处理有序数组的对称操作(如977.有序数组平方),比较两端元素平方后逆序填充结果数组
- 对撞指针:在有序数组中找两数之和(如167题),通过调整左右指针逼近目标
3.滑动窗口法
-
- 适用场景:连续子数组问题(如209.长度最小子数组)
- 实现要点:
-
-
- 窗口起始位置动态调整:当子数组和≥目标值时收缩左边界
- 时间复杂度从暴力解法的O(n²)优化到O(n)
-
4.暴力优化策略
-
- 哈希表替代遍历:如两数之和问题(1题),用哈希表存储已遍历元素,将查找时间从O(n)降到O(1)
- 逆向填充数组:合并两个有序数组时(88题),从后向前填充避免覆盖未比较元素
二、易错点及注意事项
1.边界条件处理
-
- 循环终止条件(如
left<=right
还是left<right
) - 数组越界检查(如访问
nums[mid]
前需确保mid
在有效索引范围) - 空数组/单元素数组的特殊处理(如88题中nums1长度为0的情况)
- 循环终止条件(如
2.时间复杂度优化
-
- 避免嵌套循环:如移除元素暴力解法时间复杂度O(n²),双指针优化为O(n)
- 空间换时间:哈希表解法相比暴力法显著提升效率
3.原地操作技巧
-
- 覆盖代替删除:数组删除元素本质是覆盖(如27题)
- 逆序处理避免冲突:合并有序数组时从后向前填充
4.特殊题型应对
-
- 螺旋矩阵:坚持循环不变量原则,每圈按"左上→右上→右下→左下→左上"顺序处理,注意每边处理采用左闭右开区间
- 含退格字符串比较:逆序双指针模拟退格操作,减少空间复杂度
三、经典题目
题目 | 核心技巧 | 力扣编号 |
二分查找 | 二分法区间定义 | 704 |
移除元素 | 快慢双指针 | 27 |
有序数组的平方 | 左右双指针 | 977 |
长度最小的子数组 | 滑动窗口 | 209 |
螺旋矩阵II | 循环不变量与边界控制 | 59 |
两数之和 | 哈希表优化 | 1 |
合并两个有序数组 | 逆向双指针 | 88 |
比较含退格的字符串 | 逆序双指针 | 844 |