每日算法-250405
34. 在排序数组中查找元素的第一个和最后一个位置
题目
思路
本题的核心思路是二分查找。
解题过程
- 问题分析:在一个升序排列的数组中查找一个目标值
target
的起始和结束位置。这是一个典型的二分查找应用场景。- 核心转换:题目要求找到
target
的第一个位置和最后一个位置。这可以转换为两个子问题:
- 找到第一个 大于等于
target
的元素的下标(记为left_bound
)。- 找到第一个 大于
target
的元素的下标,然后将这个下标减 1,就得到target
的最后一个位置(记为right_bound
)。这等价于找到第一个 大于等于target + 1
的元素的下标,然后减 1。- 实现
lower_bound
函数:我们可以实现一个通用的二分查找函数lower_bound(nums, k)
,用于查找数组nums
中第一个大于等于k
的元素的下标。
- 初始化指针
left = 0
,right = nums.length - 1
。- 循环条件
while (left <= right)
。- 计算中间位置
mid = left + (right - left) / 2
。- 如果
nums[mid] < k
,说明目标值k
(或第一个大于等于k
的元素)一定在mid
的右侧,更新left = mid + 1
。- 如果
nums[mid] >= k
,说明mid
可能是第一个大于等于k
的元素,或者目标在mid
的左侧。因此,我们需要继续在左半部分(包括mid
本身)查找,更新right = mid - 1
。- 循环结束后,
left
指针指向的位置就是第一个大于等于k
的元素的下标。如果数组中所有元素都小于k
,left
将会等于nums.length
。- 求解:
- 调用
lower_bound(nums, target)
得到left_index
。- 检查
left_index
:如果left_index
等于数组长度nums.length
或者nums[left_index]
不等于target
,说明数组中不存在target
,直接返回[-1, -1]
。- 调用
lower_bound(nums, target + 1)
得到right_index_plus_one
。target
的最后一个位置是right_index_plus_one - 1
。- 返回
[left_index, right_index_plus_one - 1]
。
复杂度
- 时间复杂度: O(log n) - 两次二分查找。
- 空间复杂度: O(1) - 只使用了常数级别的额外空间。
Code
class Solution {
public int[] searchRange(int[] nums, int target) {
// 查找第一个大于等于 target 的位置
int leftIdx = lower_bound(nums, target);
// 检查 leftIdx 是否越界 或 nums[leftIdx] != target
// 如果是,说明 target 不存在
if (leftIdx == nums.length || nums[leftIdx] != target) {
return new int[] {-1, -1};
}
// 查找第一个大于等于 target + 1 的位置
// 这个位置的前一个位置就是 target 的最后一个位置
int rightIdx = lower_bound(nums, target + 1) - 1;
return new int[] {leftIdx, rightIdx};
}
// 查找数组中第一个大于等于 k 的元素的下标
private int lower_bound(int[] nums, int k) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < k) {
// [mid+1, right]
left = mid + 1;
} else { // nums[mid] >= k
// [left, mid-1]
right = mid - 1;
}
}
// 循环结束后,left 就是第一个 >= k 的元素的下标
return left;
}
}
35. 搜索插入位置
题目
思路
同样使用二分查找。
解题过程
- 问题分析:在一个 无重复元素 的有序数组中,查找目标值
target
。如果找到,返回其下标;如果找不到,返回它应该插入的位置的下标,以保持数组有序。- 核心思想:这个问题本质上就是查找数组中第一个 大于等于
target
的元素的下标。
- 如果数组中存在
target
,那么第一个大于等于target
的元素就是target
本身,其下标即为所求。- 如果数组中不存在
target
,那么第一个大于等于target
的元素的位置,就是target
应该插入的位置。- 实现:可以直接复用上一题中的
lower_bound
查找逻辑。
- 初始化
left = 0
,right = nums.length - 1
。- 循环
while (left <= right)
。- 计算
mid
。- 如果
nums[mid] < target
,目标在右侧,left = mid + 1
。- 如果
nums[mid] >= target
,目标在mid
或其左侧,right = mid - 1
。- 循环结束后,
left
就是第一个大于等于target
的元素的下标。- 边界情况处理:
- 如果数组中所有元素都小于
target
,循环过程中left
会一直右移,最终left
变为nums.length
,这正好是target
应该插入的位置。- 如果数组中所有元素都大于
target
,循环过程中right
会一直左移,最终left
保持为0
,这也是target
应该插入的位置。- 因此,该二分查找的返回值
left
直接就是答案。
复杂度
- 时间复杂度: O(log n) - 一次二分查找。
- 空间复杂度: O(1) - 只使用了常数级别的额外空间。
Code
class Solution {
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) {
left = mid + 1; // 继续在右区间 [mid+1, right] 查找
} else { // nums[mid] >= target
right = mid - 1; // 继续在左区间 [left, mid-1] 查找
}
}
// 循环结束后,left 指向第一个大于等于 target 的元素下标
// 或者,如果 target 大于所有元素,left 指向 nums.length
return left;
}
}
92. 反转链表 II (复习)
题目
复习心得
今天是第二次做这道题。核心思路仍然是找到
left
位置的前一个节点prev
,然后使用头插法,在left
到right
区间内,依次将cur
后面的节点curNext
移动到prev
的后面(也就是反转区间的头部)。在
while
循环里,关键在于理解节点连接的变化:
- 保存
cur
的下一个节点:ListNode curNext = cur.next;
- 让
cur
跳过curNext
,指向curNext
的下一个节点:cur.next = curNext.next;
- 将
curNext
插入到反转区间的头部,也就是prev
的后面:curNext.next = prev.next;
- 让
prev
指向新的头部curNext
:prev.next = curNext;
今天在写的时候,容易混淆的是步骤 3 和 4。一开始容易错误地写成
curNext.next = cur
,这是不对的,因为cur
是在移动的,只有在循环开始前prev.next
才指向反转区间的第一个节点。正确的做法是始终将curNext
插入到prev.next
所指向的位置。详细题解可以参考之前的笔记:每日算法-250328
Code
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
// 使用虚拟头节点简化边界处理(left=1 的情况)
ListNode dummy = new ListNode(0);
dummy.next = head;
// 1. 找到 left 位置的前一个节点 prev
ListNode prev = dummy;
for (int i = 1; i < left; i++) {
prev = prev.next;
}
// prev.next 就是反转区间的第一个节点,记为 cur
ListNode cur = prev.next;
// 2. 执行头插法反转 left 到 right 区间的节点
// 总共需要执行 right - left 次头插操作
for (int i = left; i < right; i++) {
// 获取 cur 的下一个节点,它将是下一个要移动到头部的节点
ListNode nodeToMove = cur.next;
// cur 跳过 nodeToMove
cur.next = nodeToMove.next;
// 将 nodeToMove 插入到 prev 的后面
nodeToMove.next = prev.next;
prev.next = nodeToMove;
}
return dummy.next;
}
}