二分法基础
技巧1: 找到二分时mid的对比值
技巧2: 闭区间和开区间转换(部分题目开区间更好写)
low bound
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出第一个不小于target的元素位置。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
解法分析:
寻找目标值在数组中的开始位置,即找出target对应的lower bound。
假设每次分析基于闭区间[left, right],在每次循环迭代时:
-
nums[mid] < target,此时从mid - 1往前,肯定是小于target的,left = mid + 1
-
nums[mid] > target,此时从mid + 1往后,肯定是大于target的,right = mid - 1
-
nums[mid] = target, 此时需要收缩right,而不是left,如[8, 8, 8], target=8时,收缩left的话,会找不到下界。收缩right的话,
- nums[mid - 1] = target,迭代的下一个循环继续处理
- nums[mid - 1] < target,在此之后,left会不断收缩,直到left > right
-
循环结束条件:由于是闭区间,left <= right
循环结束时,会有三种情况:
- 数组中存在正常下界,此时left的位置为目标值的下界(right + 1也为目标值下界)
- 数组元素均小于target,此时left为nums.length
- 数组元素均大于target,此时left为0
// 闭区间循环
export function low_bound(nums, target) {
if (nums.length === 0) return -1
let left = 0, right = nums.length - 1
while (left <= right) { // [left, right]
const mid = Math.floor(left + (right - left) / 2)
if (nums[mid] < target) {
left = mid + 1 // [mid + 1, right]
} else {
// num[mid] = target时,需要收缩右端点
// 收缩左端点的话,会找不到下界,如[8, 8, 8, 8], target=8
right = mid - 1 // [left, mid - 1]
}
}
// 1. 数组中存在正常下界,此时left的位置为目标值的下界
// 2. 数组元素均小于target,此时left为nums.length
// 3. 数组元素均大于target,此时left为0
return left
}
解法分析:
假设每次分析基于左闭右开[left, right)
此时在循环时需要注意:
- 循环条件: left < right
- 右端点收缩时,right = mid
// 左闭右开循环
export function low_bound2(nums, target) {
if (nums.length === 0) return -1
let left = 0, right = nums.length
while (left < right) { // [left, right)
const mid = Math.floor(left + (right - left) / 2)
if (nums[mid] < target) {
left = mid + 1 // [mid + 1, right)
} else {
right = mid // [left, mid)
}
}
// 1. 数组中存在正常下界,此时left的位置为目标值的下界
// 2. 数组元素均小于target,此时left为nums.length
// 3. 数组元素均大于target,此时left为0
return left
}
解法分析:
假设每次分析基于左开右开(left, right)
此时在循环时需要注意:
- 循环条件: left + 1 < right
- 左端点收缩时,left = mid
- 右端点收缩时,right = mid
// 左开右开循环
export function low_bound3(nums, target) {
if (nums.length === 0) return -1
let left = -1, right = nums.length
while (left + 1 < right) { // (left, right)
const mid = Math.floor(left + (right - left) / 2)
if (nums[mid] < target) {
left = mid // (mid, right)
} else {
right = mid // (left, mid)
}
}
// 1. 数组中存在正常下界,此时left的位置为目标值的下界
// 2. 数组元素均小于target,此时left为nums.length
// 3. 数组元素均大于target,此时left为0
return right
}
其他情况
整数数组时,以下四种情况,均可以转化成low bound形式
- , 对应low bound
- ,转化成
- , 转化成 的前一个元素
- , 转化成 的前一个元素
在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题
function low_bound(nums, target) {
if (nums.length === 0) return -1
let left = 0, right = nums.length - 1
while (left <= right) { // [left, right]
const mid = Math.floor(left + (right - left) / 2)
if (nums[mid] < target) {
left = mid + 1 // [mid + 1, right]
} else {
// num[mid] = target时,需要收缩右端点
// 收缩左端点的话,会找不到下界,如[8, 8, 8, 8], target=8
right = mid - 1 // [left, mid - 1]
}
}
// 1. 数组中存在正常下界,此时left的位置为目标值的下界
// 2. 数组元素均小于target,此时left为nums.length
// 3. 数组元素均大于target,此时left为0
return left
}
var searchRange = function(nums, target) {
let start = low_bound(nums, target)
if (start === - 1 || start === nums.length || nums[start] > target) return [-1, -1]
return [start, low_bound(nums, target + 1) - 1]
};
寻找峰值
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) **的算法来解决此问题
解法分析:
复杂度为 O(log n),已经在提示要使用二分法。另外需要注意,题目只要求找到其中一个峰值即可。
在每次循环时,比较mid和mid+1的关系
-
nums[mid] < nums[mid+1], 此时从mid向前,都属于上坡,峰值肯定在mid + 1后,left = mid + 1
-
nums[mid] > nums[mid+1],此时从mid + 1向后,都属于下坡,峰值肯定在mid之前。但是需要额外的判断right的下一个位置
- right不能直接赋值为mid-1,因为mid可能是峰顶
- 如果mid是峰顶, right = mid
- 如果mid不是封顶,right = mid - 1
-
循环结束条件:闭区间判断,left <= right
返回结果:
- 数组元素递增时,left=nums.length
- 其他情况,left为封顶位置
var findPeakElement = function(nums) {
if (nums.length < 2) return 0
let left = 0, right = nums.length - 2
while (left <= right) {
const mid = Math.floor((left + right) / 2)
if (nums[mid] < nums[mid + 1]) {
left = mid + 1
} else {
if (nums[mid] > nums[right]) {
right = mid
} else {
right = mid - 1
}
}
}
return left === nums.length ? left - 1 : left
};
搜索旋转排序数组中的最小值
已知一个长度为 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) 的算法解决此问题
解法分析:
对比条件:nums[nums.length - 1]
比较区间:左开右开
var findMin = function(nums) {
if (nums.length < 2) return nums[0]
let left = -1, right = nums.length - 1
while (left + 1 < right) {
const mid = Math.floor((left + right) / 2)
if (nums[mid] > nums[nums.length - 1]) {
left = mid
} else {
right = mid
}
}
return nums[right]
};