持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
先看力扣题目
力扣题链接
给定一个n个元素有序的(升序)整形数组nums和一个目标值target,写一个函数搜索nums中的target,如果目标值存在返回下标,否则返回-1。
示例1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 之间。
- nums 的每个元素都将在 之间。
暴力破解法
看到题目,首先想到的是数组的findIndex方法,直接暴力破解,findIndex方法接受一个函数参数,返回在数组中查找的符合条件的第一个元素的索引值
const search = (nums: number[], target: number): number => {
return nums.findIndex((n: number) => n === target)
}
使用findIndex确实可以通过测试,甚至很多数组方法都可以去实现,但这就脱离了我们的初衷了,暴力破解毕竟不是王道,肯定还有更优的解决方案,这就是我们今天要学的二分查找。
题目中数组为有序数组,同时数组值不重复,这就是使用二分查找法的前提条件,以后看到这样的描述,第一时间都要想想是不是可以用二分查找。
什么是二分查找
二分查找也称为折半查找,顾名思义,用一个中间值把一个数组分成两段(左区间,右区间 ),判断目标值出现在那个区间,循环(递归)二分(折半),直到找到目标元素或不满足条件退出,想想看,这样效率是不是高很多了。
实现二分查找
首先根据上面的思想来定义边界条件,进行 二分(折半) 操作;
1. left: 左边界值(默认等于0,也就是第一个元素的下标)
2. min: 中间值,计算方式为 Math.floor((left + right) / 2)
3. right: 一般有两种定义,等于 nums.length - 1(左闭右闭),等于 nums.length (左闭右开)
其实难点就在于边界值的定义上面
图解 “左闭右闭” 和 “左闭右开” 的区别
根据上面的图,我们用代码分别来实现,看看具体有什么不同
// 左闭右闭法 [left, right]
function search1(nums: number[], target: number) {
let left = 0, right = nums.length - 1;
// 这里注意:要用 <=
// 因为在[left, right]区间里面,left === right 是有意义的
while(left <= right) {
let min = Math.floor((left + right) / 2)
if (nums[min] === target) {
return min
} else if (nums[min] > target) { // 如果中间值大于目标值,目标值在左区间,移动右边界值
right = min - 1 // 这里为什么要减一,可以看图
} else if (nums[min] < target) { // 如果中间值小于目标值,目标值在右区间,移动左边界值
left = min + 1
}
}
return -1
}
// 左闭右开法 [left, right)
function search2(nums: number[], target: number) {
let left = 0, right = nums.length;
// 这里注意:要用 <
// 因为在[left, right)区间里面,left === right 是没有意义的
while(left < right) {
const min = Math.floor((left + right) / 2)
if (nums[min] === target) {
return min
} else if (nums[min] > target) { // 如果中间值大于目标值, 目标值在左区间,移动右边界值
right = min
} else if (nums[min] < target) { // 如果中间值小于目标值, 目标值在右区间,移动左边界值
left = min + 1
}
}
return -1
}
这里能明显的看出来,取得边界条件不一样,相应得判断条件也不一样;这里要注意的点就是,如果是闭区间,那么left === right是有意义的
搞明白了边界值的定义,那么根据边界值去做二分(折半) 操作,根据判断移动边界条件,找出目标值就好了。
递归实现
最后我们用递归来实现一下
// 左闭右闭法 [left, right]
function search1(nums: number[], target: number): number {
const handle = (nums: number[], target: number, left: number, right: number): number => {
if (left > right) return -1
let min = Math.floor((left + right) / 2)
if (nums[min] > target) {
return handle(nums, target, left, min - 1)
} else if (nums[min] < target) {
return handle(nums, target, min + 1, right)
} else {
return min
}
}
return handle(nums, target, 0, nums.length - 1)
};
// 左闭右开法 [left, right)
function search1(nums: number[], target: number): number {
const handle = (nums: number[], target: number, left: number, right: number): number => {
if (left >= right) return -1
let min = Math.floor((left + right) / 2)
if (nums[min] > target) {
return handle(nums, target, left, min)
} else if (nums[min] < target) {
return handle(nums, target, min + 1, right)
} else {
return min
}
}
return handle(nums, target, 0, nums.length)
};