作为一名算法工程师,我深知数组是最基础也是最重要的数据结构之一。今天我将分享卡码算法训练营第一天关于数组的三大经典问题:二分查找、移除元素和有序数组的平方。这些题目看似简单,但蕴含着许多值得深入探讨的细节和技巧。
二分查找(Binary Search)是一种在有序数组中查找特定元素的算法。它的核心思想是通过不断缩小搜索范围来快速定位目标元素。每次比较都将搜索范围缩小一半,因此时间复杂度为O(log n),效率远高于线性查找的O(n)。
提示:二分查找的前提是数组必须是有序的,否则算法将无法正常工作。
让我们仔细分析LeetCode 704题的实现代码:
cpp复制class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else if (nums[middle] == target) {
return middle;
} else if (nums[middle] < target) {
left = middle + 1;
}
}
return -1;
}
};
这段代码有几个关键点需要注意:
left <= right而不是left < right,确保能处理边界情况left + ((right - left) / 2)而不是(left + right)/2,防止整数溢出right = middle - 1和left = middle + 1而不是right = middle和left = middle,避免死循环在实际编码中,二分查找容易出现以下几种错误:
调试技巧:
LeetCode 27题要求我们原地移除数组中所有等于给定值的元素,并返回新数组的长度。这里的"原地"意味着不能使用额外的数组空间,必须在原数组上操作。
双指针法是解决这类问题的利器。我们使用两个指针:
cpp复制class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++) {
if (val != nums[fast]) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
};
这个实现的关键点:
这个问题有几个有趣的变种:
LeetCode 977题给定一个非递减顺序排序的整数数组,要求返回每个数字平方后仍按非递减顺序排序的新数组。
最直观的解法是先平方再排序,时间复杂度为O(nlogn)。但利用数组已排序的特性,我们可以用双指针法实现O(n)的时间复杂度。
cpp复制class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int n = nums.size();
vector<int> res(n);
int left = 0, right = n - 1;
int index = n - 1;
while (left <= right) {
int leftSq = nums[left] * nums[left];
int rightSq = nums[right] * nums[right];
if (leftSq > rightSq) {
res[index--] = leftSq;
left++;
} else {
res[index--] = rightSq;
right--;
}
}
return res;
}
};
这个解法的精妙之处在于:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 平方后排序 | O(nlogn) | O(1)或O(n) | 通用解法 |
| 双指针法 | O(n) | O(n) | 原数组有序 |
在实际应用中,当n较大时,双指针法的优势非常明显。
通过这三个题目,我们可以总结出处理数组问题的一些通用方法:
在实际面试中,理解这些模式并能灵活运用非常重要。我建议初学者可以按照以下步骤练习:
在教授算法和指导学员的过程中,我发现以下几个问题是初学者经常遇到的:
边界条件处理不当
left <= right vs left < right时间复杂度分析错误
空间复杂度优化不足
代码冗余
经过多年的算法教学和实战,我总结出以下几点经验:
对于数组问题,我特别推荐使用"纸上编码"法:先在纸上写出算法步骤和伪代码,确认逻辑正确后再开始实际编码。这种方法能显著减少调试时间。
在实际工程中,数组操作往往伴随着更多业务约束。例如,可能需要处理超大数组(内存不足)、并发访问(线程安全)或特殊数据类型(自定义对象)。这些情况下,基础算法的变通应用就显得尤为重要。