1. 数组基础与二分查找入门
第一次接触算法训练营的同学们,今天我们要啃下三块硬骨头:数组基础操作、二分查找算法和双指针技巧。作为程序员的基本功,这些内容看似简单,但真正掌握它们需要大量的思考和练习。
数组是最基础的数据结构之一,它就像一排连续排列的储物柜,每个柜子都有编号(索引),我们可以通过编号快速找到存放的物品(元素)。在C++中,数组的声明是这样的:
cpp复制int arr[5] = {1, 3, 5, 7, 9}; // 声明一个包含5个整数的数组
1.1 二分查找的精髓
二分查找是算法世界里的"分而治之"典范,它能在O(log n)的时间复杂度内找到目标元素。想象你在翻字典找单词,不会从第一页开始一页页翻,而是先翻到中间,根据字母顺序决定往前还是往后查找——这就是二分查找的思想。
标准二分查找的实现有几个关键点:
- 确定搜索区间(左闭右闭还是左闭右开)
- 循环终止条件
- 中间值的计算方式
- 左右边界的更新规则
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;
}
注意:计算中间索引时使用
left + (right - left)/2而非(left + right)/2是为了防止整数溢出。当数组很大时,left和right相加可能会超出整型范围。
1.2 二分查找的变种问题
实际应用中,我们常遇到二分查找的变种问题,比如:
- 查找第一个等于目标值的位置
- 查找最后一个等于目标值的位置
- 查找第一个大于等于目标值的位置
- 查找最后一个小于等于目标值的位置
这些变种问题的关键在于处理等于目标值时如何移动指针。例如,查找第一个等于目标值的位置:
cpp复制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;
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 移除元素的双指针技巧
2.1 暴力解法与优化思路
移除数组中指定元素的最直观方法是暴力解法:发现目标元素后,将后面所有元素前移一位。这种方法时间复杂度为O(n²),效率较低。
cpp复制// 暴力解法
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--; // 因为下标i以后的元素都前移了一位,所以i也要前移
size--; // 数组大小减1
}
}
return size;
}
2.2 双指针法的精妙之处
双指针法可以将时间复杂度降为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. 有序数组的平方与双指针应用
3.1 问题分析与暴力解法
给定一个按非递减顺序排序的整数数组,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。
最直接的思路是先平方再排序:
cpp复制vector<int> sortedSquares(vector<int>& nums) {
for (int i = 0; i < nums.size(); i++) {
nums[i] *= nums[i];
}
sort(nums.begin(), nums.end());
return nums;
}
这种方法的时间复杂度为O(n log n),主要消耗在排序步骤上。对于有序数组,我们有更优的解法。
3.2 双指针的优雅解法
由于原数组是有序的,平方后的最大值只可能出现在数组的两端。我们可以使用双指针从两端向中间遍历:
cpp复制vector<int> sortedSquares(vector<int>& nums) {
int n = nums.size();
vector<int> result(n);
int left = 0, right = n - 1;
int index = n - 1;
while (left <= right) {
int leftSquare = nums[left] * nums[left];
int rightSquare = nums[right] * nums[right];
if (leftSquare > rightSquare) {
result[index--] = leftSquare;
left++;
} else {
result[index--] = rightSquare;
right--;
}
}
return result;
}
这种方法的时间复杂度是O(n),空间复杂度是O(n)(需要额外空间存储结果)。在实际面试中,这种解法往往能展示出你对双指针技巧的深刻理解。
4. 常见问题与调试技巧
4.1 二分查找的边界问题
初学者常犯的错误包括:
- 循环条件是
while(left < right)还是while(left <= right) - 更新边界时是
right = mid还是right = mid - 1 - 如何选择初始的左右边界
我的调试经验是:
- 对于左闭右闭区间,使用
while(left <= right)和right = mid - 1 - 对于左闭右开区间,使用
while(left < right)和right = mid - 初始边界应根据问题实际情况确定,通常为
0和size - 1
4.2 双指针法的变种应用
双指针法不仅适用于移除元素问题,还可以解决:
- 合并两个有序数组
- 移动零到数组末尾
- 两数之和问题
- 反转字符串
掌握这种思想后,你会发现很多数组问题都能迎刃而解。我在实际项目中经常使用双指针优化性能,特别是在处理大型数据集时。
4.3 数组越界问题排查
数组操作中最常见的bug就是越界访问。调试技巧包括:
- 在访问数组元素前检查索引是否有效
- 使用
size()方法而不是硬编码数组长度 - 在循环条件中加入边界检查
- 使用调试器观察指针移动情况
cpp复制// 安全的数组访问示例
if (index >= 0 && index < nums.size()) {
int value = nums[index];
// 处理value
}
5. 实战练习与性能优化
5.1 推荐练习题
为了巩固今天的内容,建议完成以下题目:
-
- 二分查找(基础)
-
- 搜索插入位置(变种)
-
- 在排序数组中查找元素的第一个和最后一个位置(进阶)
-
- 移除元素(基础)
-
- 删除有序数组中的重复项(变种)
-
- 有序数组的平方(综合)
5.2 性能优化技巧
在处理大规模数组时,可以考虑以下优化:
- 使用
reserve()预分配空间减少动态扩容开销 - 对于频繁访问的元素,考虑缓存到局部变量
- 避免在循环中调用
size()方法,提前计算长度 - 使用移动语义而非拷贝来传递大型数组
cpp复制// 优化示例:预分配空间
vector<int> processLargeArray(const vector<int>& input) {
vector<int> result;
result.reserve(input.size()); // 预分配空间
for (int num : input) {
result.push_back(process(num));
}
return result;
}
5.3 实际项目中的应用案例
在我的一个图像处理项目中,需要快速查找特定颜色值。使用二分查找将查询时间从O(n)降到O(log n),显著提升了性能。另一个例子是在处理用户输入过滤时,双指针法帮助我高效地移除了无效数据,同时保持了数据顺序。
数组和双指针技巧是算法基础中的基础,但它们的应用却非常广泛。掌握这些核心思想后,你会发现很多复杂问题都能分解为这些基本操作的组合。建议初学者多写多练,从简单题开始,逐步挑战更复杂的问题。
