1. 数组基础与二分查找入门
作为一名从零开始刷LeetCode的开发者,我深知数组作为最基本的数据结构,是算法学习的必经之路。今天我将分享自己第一天刷题的心得体会,重点解析704二分查找、24移除元素和977有序数组平方这三道经典题目。
数组在内存中是连续存储的,这个特性决定了它的随机访问时间复杂度是O(1)。但插入和删除操作可能需要移动大量元素,时间复杂度为O(n)。理解这个底层原理对解题至关重要。
1.1 704 二分查找详解
二分查找是算法学习中的"Hello World",但很多初学者容易忽略细节。标准的二分查找要求数组必须是有序的,这是使用二分的前提条件。
c复制int search(int* nums, int numsSize, int target){
int left = 0;
int right = numsSize - 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 < right,这样可以确保检查到所有可能的元素 - 计算mid时使用
left + (right - left)/2而不是(left + right)/2,防止整数溢出 - 边界更新时是
mid + 1和mid - 1,而不是直接用mid,避免死循环
提示:二分查找的变种很多,比如查找第一个/最后一个等于目标值的位置,这时需要调整判断条件和边界更新逻辑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 双指针技巧实战
双指针是解决数组问题的利器,24题移除元素就是典型应用。我最初尝试暴力解法,时间复杂度O(n²),后来优化为双指针的O(n)解法,效率提升明显。
2.1 24 移除元素的快慢指针实现
快慢指针的核心思想是:快指针用于遍历数组,慢指针用于记录有效元素的位置。当快指针指向的元素不等于目标值时,将其复制到慢指针位置,然后两个指针都前进;否则仅快指针前进。
c复制int removeElement(int* nums, int numsSize, int val){
int slow = 0;
for(int fast = 0; fast < numsSize; fast++){
if(nums[fast] != val){
nums[slow++] = nums[fast];
}
}
return slow;
}
实际编码时我踩过几个坑:
- 初始时slow应该为0而不是1
- 返回的是slow而不是slow+1,因为slow已经指向了最后一个有效元素的下一个位置
- 在C语言中,数组大小需要通过指针参数返回,这点与某些语言不同
2.2 双指针的变种应用
双指针不仅可用于同向移动,还可以对向移动。比如在有序数组中找两数之和,就可以使用左右指针从两端向中间逼近。这种技巧在后续的题目中会经常遇到。
3. 977 有序数组平方的优化解法
这道题看似简单,但直接平方后排序的时间复杂度是O(nlogn)。利用数组已排序的特性,可以优化到O(n)。
3.1 双指针从两端向中间遍历
由于负数平方后会变成正数,最大的平方值一定出现在数组的两端。因此可以从两端向中间遍历,比较两端的平方值,将较大的放入结果数组的末尾。
c复制int* sortedSquares(int* nums, int numsSize, int* returnSize){
*returnSize = numsSize;
int* result = (int*)malloc(numsSize * sizeof(int));
int left = 0, right = numsSize - 1;
int index = numsSize - 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;
}
这里有几个关键点:
- 需要动态分配结果数组的内存
- 使用index从后往前填充结果数组
- 循环条件是
left <= right,确保处理所有元素
4. 常见问题与调试技巧
在实际编码过程中,我遇到了几个典型问题,这里分享解决方法:
4.1 数组越界问题
在二分查找中,如果right初始化为numsSize而不是numsSize-1,可能会导致访问越界。这种错误在C语言中不会立即报错,但会导致不可预知的行为。
调试方法:
- 在循环开始和结束时打印左右指针的值
- 检查mid的计算是否可能导致溢出
- 使用assert断言检查数组访问是否合法
4.2 内存管理问题
在977题中,需要动态分配内存。常见错误包括:
- 忘记释放内存(在LeetCode环境中可以忽略)
- 分配的内存大小不足
- 返回局部变量的地址
解决方法:
- 使用valgrind等工具检查内存泄漏
- 仔细计算需要的内存大小
- 确保返回的指针生命周期足够长
4.3 边界条件处理
很多错误发生在边界条件下,比如:
- 空数组输入
- 所有元素都需要移除
- 目标值不在数组中
测试用例建议:
- 空数组
- 单元素数组
- 全为目标值的数组
- 不包含目标值的数组
- 正常情况下的数组
5. 算法效率分析
理解算法的时间复杂度和空间复杂度是刷题的重要部分:
-
二分查找:
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
-
移除元素(双指针法):
- 时间复杂度:O(n)
- 空间复杂度:O(1)
-
有序数组平方(双指针法):
- 时间复杂度:O(n)
- 空间复杂度:O(n)(需要额外空间存储结果)
在实际面试中,面试官不仅关心你是否能解决问题,更关心你是否能分析不同解法的优劣,并选择最优方案。
6. 格式化输出补充说明
在原始材料中提到的printf格式化输出,虽然与算法关系不大,但在调试时非常有用:
c复制// 调试时打印数组
for(int i = 0; i < numsSize; i++){
printf("%3d ", nums[i]); // 每个数字占3位
if((i+1) % 10 == 0) printf("\n"); // 每10个换行
}
常用格式说明符:
%5d:右对齐,宽度5%-5d:左对齐,宽度5%02d:不足两位前面补零%.2f:保留两位小数
掌握这些格式化技巧可以让调试输出更清晰,方便发现问题。
7. 后续学习建议
完成这三道题目后,我建议可以继续练习以下相关题目巩固知识:
-
二分查找变种:
-
- 搜索插入位置
-
- 在排序数组中查找元素的第一个和最后一个位置
-
-
双指针应用:
-
- 移动零
-
- 比较含退格的字符串
-
- 盛最多水的容器
-
-
数组操作:
-
- 轮转数组
-
- 删除有序数组中的重复项
-
刷题时建议:
- 先自己思考,不要直接看答案
- 写出所有可能的解法并分析复杂度
- 记录下自己的错误和改正过程
- 定期复习做过的题目
算法学习是个长期过程,保持每天至少一道题的节奏,坚持几个月就会看到明显进步。我在实际工作中发现,这些基础算法思想确实能帮助写出更高效的代码。
