1. 算法训练营Day1:二分查找与双指针核心精讲
作为算法面试中的两大基础利器,二分查找和双指针技术几乎出现在所有大厂笔试的必考清单中。今天我将结合LeetCode高频题库(特别是Hot 100)中的经典案例,拆解这两种算法的实现细节与实战技巧。无论你是正在准备秋招的应届生,还是想巩固算法基础的在职工程师,这些经过实战检验的代码模板都能让你少走弯路。
提示:本文所有代码示例均基于Java语言实现,但算法思想适用于任何编程语言。建议配合LeetCode原题同步练习。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二分查找的两种范式解析
2.1 算法前提与基本思想
二分查找的高效性建立在两个基础条件上:
- 有序性:输入数组必须是有序排列(升序或降序)
- 唯一性:理想情况下数组元素不应重复(虽然存在处理重复元素的变种)
其核心思想是通过不断缩小搜索范围来降低时间复杂度,从O(n)优化到O(log n)。就像查字典时我们不会从头翻到尾,而是根据字母顺序不断折半定位。
2.2 闭区间写法[left, right]
这是最符合直觉的实现方式,我们以LeetCode 704题为例:
java复制public int search(int[] nums, int target) {
int left = 0, right = nums.length - 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;
}
关键细节说明:
right初始值为nums.length - 1,表示包含右边界- 循环条件
left <= right确保区间有效时继续搜索 - 边界更新时
mid ± 1避免重复检查已知不符合条件的元素
避坑指南:计算mid时使用
left + (right - left)/2而非(left+right)/2,可防止整数溢出。这是面试常考的细节问题。
2.3 开区间写法[left, right)
这种写法在部分场景下更为简洁,但理解难度稍高:
java复制public int search(int[] nums, int target) {
int left = 0, right = nums.length; // 右边界不包含
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;
}
两种写法的对比分析:
| 特性 | 闭区间写法 | 开区间写法 |
|---|---|---|
| 初始right值 | length - 1 | length |
| 循环条件 | left <= right | left < right |
| right更新规则 | mid - 1 | mid |
| 直观性 | 更易理解 | 需要适应 |
| 适用场景 | 标准二分查找 | 边界查找类问题 |
3. 二分查找的进阶应用
3.1 搜索插入位置(LeetCode 35)
当目标不存在时需要返回应插入的位置,这实际上是寻找第一个大于等于target的元素位置:
java复制public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 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 left; // 关键点:循环结束时left指向插入位置
}
为什么返回left:当循环终止时,left总是指向第一个大于target的元素位置,这正是需要插入的位置。例如在数组[1,3,5,6]中查找2,最终left=1指向3,这正是2应该插入的位置。
3.2 边界查找问题(LeetCode 34)
处理含重复元素的数组时,需要分别找到左右边界:
java复制public int[] searchRange(int[] nums, int target) {
int leftIdx = findBound(nums, target, true);
if (leftIdx == -1) return new int[]{-1, -1};
int rightIdx = findBound(nums, target, false);
return new int[]{leftIdx, rightIdx};
}
private int findBound(int[] nums, int target, boolean isLeft) {
int left = 0, right = nums.length - 1;
int index = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
index = mid;
if (isLeft) right = mid - 1; // 继续向左找左边界
else left = mid + 1; // 继续向右找右边界
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return index;
}
算法核心:在找到target时不立即返回,而是继续向指定方向搜索。找左边界时收缩右边界,找右边界时收缩左边界。
4. 双指针技术深度剖析
4.1 快慢指针基础(LeetCode 27)
移除指定元素的经典实现:
java复制public int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow++] = nums[fast]; // 先赋值后自增
}
}
return slow;
}
执行过程可视化:
以nums = [3,2,2,3], val = 3为例:
| 步骤 | fast | nums[fast] | slow | 数组状态 |
|---|---|---|---|---|
| 1 | 0 | 3 | 0 | [3,2,2,3] |
| 2 | 1 | 2 | 0 | [2,2,2,3] |
| 3 | 2 | 2 | 1 | [2,2,2,3] |
| 4 | 3 | 3 | 2 | [2,2,2,3] |
最终返回slow=2,前两个元素[2,2]即为有效结果。
4.2 左右指针应用(LeetCode 977)
处理有序数组平方的巧妙解法:
java复制public int[] sortedSquares(int[] nums) {
int n = nums.length;
int[] res = new int[n];
int left = 0, right = n - 1;
int k = n - 1; // 从后往前填充
while (left <= right) {
int leftSq = nums[left] * nums[left];
int rightSq = nums[right] * nums[right];
if (leftSq > rightSq) {
res[k--] = leftSq;
left++;
} else {
res[k--] = rightSq;
right--;
}
}
return res;
}
算法优势:时间复杂度O(n),相比先平方后排序的O(nlogn)方案更优。关键在于利用原数组的有序特性,最大元素只可能出现在两端。
5. 双指针的进阶挑战
5.1 去重问题(LeetCode 26)
删除排序数组中的重复项:
java复制public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int slow = 0;
for (int fast = 1; fast < nums.length; fast++) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast]; // 注意前置自增
}
}
return slow + 1;
}
易错点:
- 初始条件处理空数组
- slow从0开始,fast从1开始
- 返回的是长度(slow+1)而非索引
5.2 移动零问题(LeetCode 283)
保持非零元素相对顺序的解法:
java复制public void moveZeroes(int[] nums) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != 0) {
nums[slow++] = nums[fast];
}
}
// 尾部补零
while (slow < nums.length) {
nums[slow++] = 0;
}
}
优化技巧:如果数组中没有零或零很少时,可以增加提前终止的判断,减少不必要的赋值操作。
6. 高频面试题精解
6.1 含退格的字符串比较(LeetCode 844)
双指针从后向前处理的经典案例:
java复制public boolean backspaceCompare(String s, String t) {
int i = s.length() - 1, j = t.length() - 1;
int skipS = 0, skipT = 0;
while (i >= 0 || j >= 0) {
// 处理s的退格
while (i >= 0) {
if (s.charAt(i) == '#') {
skipS++;
i--;
} else if (skipS > 0) {
skipS--;
i--;
} else {
break;
}
}
// 处理t的退格
while (j >= 0) {
if (t.charAt(j) == '#') {
skipT++;
j--;
} else if (skipT > 0) {
skipT--;
j--;
} else {
break;
}
}
// 比较有效字符
if (i >= 0 && j >= 0) {
if (s.charAt(i) != t.charAt(j)) {
return false;
}
} else if (i >= 0 || j >= 0) {
return false;
}
i--;
j--;
}
return true;
}
算法难点:
- 内层循环处理连续退格字符
- 比较时机需要等待所有退格处理完成
- 边界条件处理(一个字符串已遍历完另一个还有剩余)
6.2 完全平方数判断(LeetCode 367)
二分查找的数学应用:
java复制public boolean isPerfectSquare(int num) {
if (num < 1) return false;
long left = 1, right = num; // 使用long防溢出
while (left <= right) {
long mid = left + (right - left) / 2;
long square = mid * mid;
if (square == num) {
return true;
} else if (square < num) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
优化方案:可以使用牛顿迭代法获得更优的时间复杂度,但二分查找的实现更符合本文主题且易于理解。
7. 算法模板与实战建议
7.1 二分查找通用模板
java复制int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 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;
}
适用场景:
- 有序数组查找
- 寻找边界值
- 数学计算问题(如平方根)
7.2 双指针使用场景总结
| 问题类型 | 指针类型 | 经典例题 |
|---|---|---|
| 数组元素操作 | 快慢指针 | 移除元素(27) |
| 有序数组处理 | 左右指针 | 有序数组平方(977) |
| 字符串比较 | 反向双指针 | 退格字符串(844) |
| 滑动窗口 | 同向变距指针 | 无重复子串(3) |
7.3 调试与验证技巧
- 边界测试:空数组、单元素数组、全相同元素数组
- 过程打印:在循环中打印指针位置和关键变量值
- 可视化辅助:在纸上画出指针移动过程
- 极端值验证:最大/最小整数输入测试
在实际面试中,建议先明确说明算法思路,再着手编码,最后用测试案例验证。对于二分查找问题,要特别注意终止条件和边界更新,这是面试官重点考察的部分。
