来源0x3f:https://space.bilibili.com/206214
二分查找
闭区间、开区间、半闭半开区间的区别
闭区间:l = mid + 1, r = mid - 1
半闭半开区间:l = mid + 1, r = mid
或者 l = mid, r = mid - 1
问题:返回有序数组中第一个 >= 8
的数的位置,如果所有数都 < 8
,返回数组长度
一、闭区间写法 + 原理:
暴力做法:遍历每个数,询问它是否 > 8 ?
高效做法:L和 R分别指向询问的左右边界,即闭区间[L, R]
指向当前正在询问的数
关键:循环不变量(红色小于8,蓝色大于等于8)
L-1
始终是红色,R+1
始终是蓝色,根据循环不变量R+1
就是所要找的答案,由于闭区间,R+1=L
// lowerBound 返回最小的满足 nums[i] >= target 的 i
// 如果数组为空,或者所有数都 < target,则返回 nums.length
// 要求 nums 是非递减的,即 nums[i] <= nums[i + 1]
// 闭区间写法 , 返回第一个满足nums[i] >= target 的 i
private int lowerBound(int[] nums, int target) {
int left = 0, right = nums.length - 1; // 闭区间 [left, right]
while (left <= right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right+1] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid + 1; // 范围缩小到 [mid+1, right]
else
right = mid - 1; // 范围缩小到 [left, mid-1]
}
return left; // 或者 right+1
}
二、左闭右开区间
// 左闭右开区间写法 , 返回第一个满足nums[i] >= target 的 i
private int lowerBound2(int[] nums, int target) {
int left = 0, right = nums.length; // 左闭右开区间 [left, right)
while (left < right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid + 1; // 范围缩小到 [mid+1, right)
else
right = mid; // 范围缩小到 [left, mid)
}
return left; // 或者 right
}
三、左开右开区间
- 注意while条件是
l+1 = R
,表示(l,R)
区间内没有任何数了,根据循环不变量,R就是第一个满足nums[i] >= target
的i
// 开区间写法 , 返回第一个满足nums[i] >= target 的 i
private int lowerBound3(int[] nums, int target) {
int left = -1, right = nums.length; // 开区间 (left, right)
while (left + 1 < right) { // 区间不为空
// 循环不变量:
// nums[left] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid; // 范围缩小到 (mid, right)
else
right = mid; // 范围缩小到 (left, mid)
}
return right; // 或者 left+1
}
【重要】大于、大于等于、小于、小于等于四种情况
这四种情况是可以相互转换的
-
大于等于 的第一个:
>=x
-
大于 的第一个 :
>X
等价于>= (x+1)
-
小于 的最后一个:
<x
等价于(>=x) - 1
(大于等于x 的 左边的那个数) -
小于等于 的最后一个:
<=x
等价于(>x) - 1 =【(>=(x+1)) - 1】
(大于x 的 左边那个数)
34. 在排序数组中查找元素的第一个和最后一个位置
难度中等2147
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums
是一个非递减数组-109 <= target <= 109
class Solution {
public int[] searchRange(int[] nums, int target) {
// 根据题目,要求找 >= target 和 <= target
// 1. 求 >= target
int start = lowerBound(nums, target);
if(start == nums.length || nums[start] != target){
return new int[]{-1, -1};
}
// 2. 求 <= target = (>x)-1 = (>=x+1) - 1
int end = lowerBound(nums, target+1) - 1;
return new int[]{start, end};
}
// lowerBound 返回最小的满足 nums[i] >= target 的 i
// 如果数组为空,或者所有数都 < target,则返回 nums.length
// 要求 nums 是非递减的,即 nums[i] <= nums[i + 1]
// 闭区间写法,返回第一个满足nums[i] >= target 的 i
private int lowerBound(int[] nums, int target) {
int left = 0, right = nums.length - 1; // 闭区间 [left, right]
while (left <= right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right+1] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid + 1; // 范围缩小到 [mid+1, right]
else
right = mid - 1; // 范围缩小到 [left, mid-1]
}
return left; // 或者 right+1
}
}
练习
162. 寻找峰值
难度中等982
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums
,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞
。
你必须实现时间复杂度为 O(log n)
的算法来解决此问题。
示例 1:
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
提示:
1 <= nums.length <= 1000
-231 <= nums[i] <= 231 - 1
- 对于所有有效的
i
都有nums[i] != nums[i + 1]
class Solution {
public int findPeakElement(int[] nums) {
int res = 0;
int n = nums.length;
int left = 0, right = n;
while(left < right){
int mid = (left + right) >> 1;
if(mid < n-1 && nums[mid] < nums[mid+1]) left = mid+1;
else right = mid;
}
return right;
}
}
153. 寻找旋转排序数组中的最小值
难度中等908
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums
中的所有整数 互不相同nums
原来是一个升序排序的数组,并进行了1
至n
次旋转
class Solution {
public int findMin(int[] nums) {
int n = nums.length;
int left = 0, right = n;
int res = 0;
while(left < right){
int mid = (left + right) >> 1;
if(mid < n-1 && nums[mid] < nums[mid+1]) right = mid;
else left = mid+1;
}
return nums[left];
}
}
33. 搜索旋转排序数组
难度中等2489
整数数组 nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标 k
(0 <= k < nums.length
)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
nums
中的每个值都 独一无二- 题目数据保证
nums
在预先未知的某个下标上进行了旋转 -104 <= target <= 104
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
int left = 0, right = n;
while(left < right){
int mid = (left + right) >> 1;
if(nums[mid] == target) return mid;
// 左边有序
if(nums[left] < nums[mid]){
// 判断target是否在区间内,是则缩小右边界
if(nums[left] <= target && target <= nums[mid]){
right = mid;
}else{
left = mid+1;
}
}
// 左边有序
else{
// 判断target是否在区间内,是则扩大左边界
if(nums[mid] <= target && target <= nums[right-1]){
left = mid+1;
}else{
right = mid;
}
}
}
return -1;
}
}
其他
左闭右闭、左闭右开模板
①左闭右闭
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length-1;
while(left <= right){
int mid = (left + right) >> 1;
if(nums[mid] == target) return mid;
else if(nums[mid] < target) left = mid+1;
else right = mid-1;
}
return left;
}
}
②左闭右开
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length;
while(left < right){
int mid = (left + right) >> 1;
if(nums[mid] == target) return mid;
else if(nums[mid] < target) left = mid+1;
else right = mid;
}
return left;
}
}