在做题过程中参考了代码随想录的解题思路,以及刷题顺序,下面是代码随想录的网站
2026大厂笔试真题整理|腾讯阿里字节美团华为京东小红书算法笔试题(含题解+OJ判题) | 代码随想录-全网最全算法数据结构刷题学习路线|图文+视频教程|免费开源
目录索引
基础入门:二分查找 (LeetCode 704) —— 拿捏区间定义的艺术
双指针初探:移除元素 (LeetCode 27) —— 快慢指针的原地重塑
双指针进阶:有序数组的平方 (LeetCode 977) —— 两端向中间的夹击相遇
终极压轴:长度最小的子数组 (LeetCode 209) —— 滑动窗口的不回头哲学
1. 二分查找 (LeetCode 704)
思考:到底写left <= right还是left < right?
很多同学背二分查找的模板,往往死在循环条件和边界更新上。其实,根本原因取决于你对“查找区间”的定义。
这里我们采用最符合直觉的左闭右闭[left, right]区间:
随着区间不断砍掉一半,
left和right离得越来越近,最后它们指到了同一个位置(即left == right)。此时,区间变成了
[left, left]。在数学上,这个区间依然包含最后一个元素!如果写
while (left <= right):程序会进入循环,去检查这最后一个数。如果写
while (left < right):循环直接终止,这最后一个元素就被漏掉了!如果它恰好是target,就会直接返回-1造成 Bug。
代码实现
Java
class Solution { public int search(int[] nums, int target) { // 剪枝优化:如果 target 超出数组边界,直接返回 -1 if (target < nums[0] || target > nums[nums.length - 1]) { return -1; } int left = 0, right = nums.length - 1; while (left <= right) { // 左闭右闭区间,当 left == right 时依旧有效 // 安全写法,防止 (left + right) 直接相加导致 int 溢出 int mid = left + (right - left) / 2; if (nums[mid] > target) { right = mid - 1; // target 在左区间,所以更新右边界 } else if (nums[mid] < target) { left = mid + 1; // target 在右区间,所以更新左边界 } else { return mid; // 找到了,直接返回下标 } } return -1; } }时间复杂度:O ( log n ) O(\log n)O(logn)
空间复杂度:O ( 1 ) O(1)O(1)
2. 移除元素 (LeetCode 27)
思考:如何在O ( 1 ) O(1)O(1)空间原地重塑数组?
由于数组在内存中是连续存储的,我们不能真正地“删除”某个元素,只能用后面的元素去“覆盖”。
这里引入快慢指针(Fast & Slow Pointers):
fast(快指针):负责冲锋陷阵,遍历原数组去寻找新数组所需要的元素(即不等于val的元素)。slow(慢指针):负责镇守大后方,维护新数组的更新位置。
代码实现
Java
class Solution { public int removeElement(int[] nums, int val) { int slow = 0; // slow 指针用来维持不含 val 的新数组 for (int fast = 0; fast < nums.length; fast++) { // 只要快指针发现不是 val 的元素,就把它“搬”到慢指针的位置 if (nums[fast] != val) { nums[slow] = nums[fast]; slow++; // 慢指针向前移动一位,等待下一个新元素 } } return slow; // slow 的值正好就是新数组的长度 } }时间复杂度:O ( n ) O(n)O(n),快指针一步到头。
空间复杂度:O ( 1 ) O(1)O(1),无任何额外空间开销。
3. 有序数组的平方 (LeetCode 977)
思考:最大值藏在何处?
既然原数组是升序的,并且含有负数,那么平方后的最大值只可能出现在哪里?
答案:要么在最左边(绝对值很大的负数),要么在最右边(绝对值很大的正数)。
基于这个单调性,我们可以使用左右双指针从两端向中间夹击:
比较
left和right平方的大小,谁的大,谁就是当前剩余元素里的最大值。将最大值逆序放入新数组
result的尾部(从后往前填)。
代码实现
Java
class Solution { public int[] sortedSquares(int[] nums) { int left = 0; int right = nums.length - 1; int k = nums.length - 1; // 新数组的指针,从后往前填充 int[] result = new int[nums.length]; while (left <= right) { // 比较左右两端平方大小 if (nums[left] * nums[left] >= nums[right] * nums[right]) { result[k--] = nums[left] * nums[left]; left++; // 左指针向右移 } else { result[k--] = nums[right] * nums[right]; right--; // 右指针向左移 } } return result; } }时间复杂度:O ( n ) O(n)O(n),相比于暴力平方后再排序的O ( n log n ) O(n \log n)O(nlogn),这是质的飞跃。
空间复杂度:O ( n ) O(n)O(n),用于存放结果数组。
4. 长度最小的子数组 (LeetCode 209)
思考:滑动窗口到底比暴力好在哪里?
很多人觉得带break的暴力解法和滑动窗口差不多,其实差远了。
假设nums = [1, 2, 3, 4, 5],target = 10。
暴力解法:
i=0时,累加1+2+3+4=10满足条件,记录并break。但当i=1时,暴力解法不得不回滚去重新连加2+3+4...。这些中间数字被大量重复计算,最坏复杂度依然是O ( n 2 ) O(n^2)O(n2)。滑动窗口:它利用了“连续子数组”的单调性。当
j走到 4 满足1+2+3+4=10后,我们不需要回滚重加!直接用减法(sum -= nums[left++])把最左边的1吐出来,变成[2,3,4]。接着只需要再加一个5就能继续判断。
总结:
暴力解法是每搬完一车砖,就要把车倒回起点重新装;而滑动窗口是车子一直往前开,前面装一块砖(
right++),后面扔一块砖(left++),绝不倒车。
代码实现
Java
class Solution { public int minSubArrayLen(int target, int[] nums) { int left = 0; // 窗口起始位置 int result = Integer.MAX_VALUE; // 初始化为极大值 int sum = 0; // 维持当前滑动窗口内的数值之和 // right 代表窗口的终止位置 for (int right = 0; right < nums.length; right++) { sum += nums[right]; // 扩大窗口 // 注意:这里必须用 while!因为左边界可能需要连续右移缩小窗口 // 如果用 if,会导致缩减不彻底,漏掉更短的符合条件的子串 while (sum >= target) { int lens = right - left + 1; // 动态计算当前窗口长度 result = result < lens ? result : lens; // 更新极小值 sum -= nums[left++]; // 精髓:缩小窗口,并向右移动左指针 } } // 如果 result 没被改动过,说明没有符合条件的子数组,返回 0 return result == Integer.MAX_VALUE ? 0 : result; } }为什么看似嵌套循环,时间复杂度确是O ( n ) O(n)O(n)?
不要被for循环里嵌套while循环的外表给骗了!
我们看每一个元素的生命周期:它最多被right指针扫描推入窗口1次,最多被left指针弹出窗口1次。
每个元素总共被操作了2次。因此,总总移动次数不超过2 n 2n2n,时间复杂度是不折不扣的O ( n ) O(n)O(n)。
🛠️ 总结与心法
从二分查找的固定区间,到快慢指针/左右指针的联动,再到滑动窗口的动态伸缩,你会发现双指针算法的本质就是:利用题目中的单调性或边界规律,剪掉不必要的盲目遍历。
双指针:适合用于“原地修改”或“两端有序”的场景。
滑动窗口:专门治各种“满足条件的连续子数组/子串”的疑难杂症。
希望这篇博文能帮你彻底理清数组这一关的思路!
如果对你有帮助,希望点赞支持,感谢!如果有任何疑问,欢迎在评论区留言交流,我们下期见!👋