高中男女做那个视频网站win7优化
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] >= targetright = mid - 1; // 继续在左区间 [left, mid-1] 查找}}// 循环结束后,left 指向第一个大于等于 target 的元素下标// 或者,如果 target 大于所有元素,left 指向 nums.lengthreturn 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 位置的前一个节点 prevListNode prev = dummy;for (int i = 1; i < left; i++) {prev = prev.next;}// prev.next 就是反转区间的第一个节点,记为 curListNode cur = prev.next;// 2. 执行头插法反转 left 到 right 区间的节点// 总共需要执行 right - left 次头插操作for (int i = left; i < right; i++) {// 获取 cur 的下一个节点,它将是下一个要移动到头部的节点ListNode nodeToMove = cur.next;// cur 跳过 nodeToMovecur.next = nodeToMove.next;// 将 nodeToMove 插入到 prev 的后面nodeToMove.next = prev.next;prev.next = nodeToMove;}return dummy.next;}
}