1. 数组基础与核心操作解析
数组作为最基本的数据结构之一,是算法入门必须掌握的基石。在C/C++中,数组是一片连续的内存空间,每个元素通过索引直接访问,这种特性使得它的读取时间复杂度为O(1)。但在插入和删除操作时,需要移动后续元素,导致时间复杂度升至O(n)。
关键理解:数组的物理存储连续性是其高效随机访问的基础,也是插入/删除低效的根源
实际应用中,我们经常遇到数组的几种基础操作模式:
- 遍历操作:最基本的for循环,注意现代语言更推荐使用迭代器或范围for语法
cpp复制// 传统索引遍历
for(int i=0; i<nums.size(); ++i) {
cout << nums[i] << " ";
}
// C++11范围for
for(auto num : nums) {
cout << num << " ";
}
- 查找操作:线性查找O(n) vs 二分查找O(logn)
- 插入操作:需要考虑扩容和元素移动
- 删除操作:通常采用逻辑删除+物理删除结合
在算法题中,数组类问题常考察以下几个核心能力:
- 对数组索引和边界的精确控制
- 双指针技巧的灵活运用
- 原地修改数组的能力
- 多维数组的空间想象能力
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二分查找的深度剖析
二分查找看似简单,但实际编码时边界条件极易出错。其核心思想是通过不断缩小搜索范围来快速定位目标,前提是数组必须有序。
2.1 标准二分查找实现
经典二分查找有三种常见写法,区别主要在于边界条件处理:
cpp复制// 写法一:左闭右闭区间
int binarySearch(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) return mid;
else if(nums[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
// 写法二:左闭右开区间
int binarySearch2(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while(left < right) {
int mid = left + (right - left)/2;
if(nums[mid] == target) return mid;
else if(nums[mid] < target) left = mid + 1;
else right = mid;
}
return -1;
}
易错点:mid计算使用(left+right)/2可能导致整数溢出,应使用left+(right-left)/2
2.2 二分查找的变种问题
实际算法题中,纯二分查找较少,更多是变种问题:
- 查找第一个/最后一个等于目标值的位置
cpp复制// 查找第一个等于target的位置
int firstEqual(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;
else left = mid + 1;
}
return (left < nums.size() && nums[left] == target) ? left : -1;
}
- 查找第一个大于等于目标值的位置
- 旋转排序数组中的搜索
- 寻找峰值元素
我在实际刷题中发现,二分查找的bug主要来自:
- 循环条件不明确(<=还是<)
- 左右边界更新错误(mid+1还是mid)
- 返回值选择不当
3. 双指针技巧实战
双指针是处理数组问题的利器,特别适合需要原地修改数组的场景。常见的有快慢指针和左右指针两种模式。
3.1 移除元素问题
LeetCode 27题是经典案例:给你一个数组nums和一个值val,你需要原地移除所有数值等于val的元素,并返回新数组的长度。
暴力解法时间复杂度O(n^2):
cpp复制int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for(int i = 0; i < size; ) {
if(nums[i] == val) {
for(int j = i; j < size-1; j++) {
nums[j] = nums[j+1];
}
size--;
} else {
i++;
}
}
return size;
}
双指针优化至O(n):
cpp复制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;
}
3.2 有序数组的平方
LeetCode 977题:给定一个按非递减顺序排序的整数数组nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。
直接排序法O(nlogn):
cpp复制vector<int> sortedSquares(vector<int>& nums) {
for(auto& num : nums) num *= num;
sort(nums.begin(), nums.end());
return nums;
}
双指针法O(n):
cpp复制vector<int> sortedSquares(vector<int>& nums) {
int n = nums.size();
vector<int> res(n);
int left = 0, right = n - 1;
for(int k = n - 1; k >= 0; k--) {
if(abs(nums[left]) > abs(nums[right])) {
res[k] = nums[left] * nums[left];
left++;
} else {
res[k] = nums[right] * nums[right];
right--;
}
}
return res;
}
双指针技巧的核心在于:
- 确定指针移动的条件
- 处理指针相遇的边界情况
- 选择合适的位置存放结果
4. 多维数组与进阶技巧
4.1 二维数组的特殊遍历
螺旋矩阵是经典问题,需要控制好四个边界:
cpp复制vector<int> spiralOrder(vector<vector<int>>& matrix) {
if(matrix.empty()) return {};
int top = 0, bottom = matrix.size()-1;
int left = 0, right = matrix[0].size()-1;
vector<int> res;
while(true) {
// 从左到右
for(int i = left; i <= right; i++) res.push_back(matrix[top][i]);
if(++top > bottom) break;
// 从上到下
for(int i = top; i <= bottom; i++) res.push_back(matrix[i][right]);
if(--right < left) break;
// 从右到左
for(int i = right; i >= left; i--) res.push_back(matrix[bottom][i]);
if(--bottom < top) break;
// 从下到上
for(int i = bottom; i >= top; i--) res.push_back(matrix[i][left]);
if(++left > right) break;
}
return res;
}
4.2 滑动窗口技巧
虽然标题未提及,但作为数组重要技巧值得补充。用于解决子数组/子串问题,如最小覆盖子串、长度最小的子数组等。
cpp复制int minSubArrayLen(int target, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0;
int left = 0;
for(int right = 0; right < nums.size(); right++) {
sum += nums[right];
while(sum >= target) {
result = min(result, right - left + 1);
sum -= nums[left++];
}
}
return result == INT32_MAX ? 0 : result;
}
4.3 数组去重的几种方式
根据热词补充常见去重方法:
- 排序+双指针:先排序,再用类似移除元素的方法
cpp复制vector<int> removeDuplicates(vector<int>& nums) {
if(nums.empty()) return {};
sort(nums.begin(), nums.end());
int slow = 0;
for(int fast = 1; fast < nums.size(); fast++) {
if(nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
}
nums.resize(slow+1);
return nums;
}
- 哈希表法:利用unordered_set去重
- STL unique:配合erase使用
在实际工程中,数组处理还需要考虑:
- 大数组的内存管理
- 缓存友好性
- SIMD指令优化
- 并行化处理
经过系统训练后,我发现数组类问题的解决关键在于:
- 先明确问题是否允许修改原数组
- 判断是否需要先排序
- 选择适合的指针策略(单指针、双指针、多指针)
- 处理好边界条件和特殊输入(空数组、全相同元素等)
数组作为基础数据结构,其相关算法构成了更复杂算法的基础。建议初学者在每道题完成后,用纸笔模拟运行过程,这对理解指针移动和边界条件非常有帮助。
