携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 19 天,点击查看活动详情
起因
在日常的学习和面试中,二分查找是非常常见的算法,虽然在日常的开发中我们不一定会直接用到二分查找,但是我们使用的一些第三方的组件库中就会用到,并且面试过程中如果面试官出了一道二分查找的题目给我们,我们却做不出来,那么就可能与这个工作机会失之交臂了,因此我们还是需要来学习一下二分查找,并使用js来实现二分查找。
分析
二分查找的前提必须是一个可排序的数组,如果这个数组中的值是乱序的,那么就无法使用二分查找,因此必须要是有序的。简单举例:如果一个数组中有从1到10000总共1W个数字,我们需要找到1000,那么我们使用二分查找就会将1W从中间拆开,然后判断我们要找的1000是在拆分的左边还是右边,如果在左边,则将左边的数组再从中间拆分,以此往复,这样就能在最快的时间内找到我们需要的值,这就是二分查找的基本逻辑。
思路
上述的分析中,我们可以通过递归来实现二分查找,这样实现会使代码的逻辑更加的清晰。当然我们也可以使用非递归的模式来实现,使用非递归的模式来使用会使性能更好,下面我们就一起来学习一下如何用js实现二分查找,并且对比一下使用递归和非递归实现的区别。
实现
首先我们使用非递归的方法来实现,具体的代码如下:
/**
* 二分查找(循环)
* @param arr number[]
* @param target number
* @return number
*/
const binarySearch1 = (arr: number[], target: number):number => {
// 获取到数组的长度
const len = arr.length;
// 如果数组长度为0,则直接返回-1,表示没有找到
if (len === 0) return -1;
// 开始位置
let startIndex = 0;
// 结束位置
let endIndex = len - 1;
while (startIndex <= endIndex) {
// 获取到中间位置
const midIndex = Math.floor((startIndex + endIndex) / 2);
// 获取中间的值
const midValue = arr[midIndex];
// 如果目标值小于中间值,说明目标值在左边,则需要继续在左侧查找
if (target < midValue) {
// 将介绍位置向当前中间值左侧移动一个位置,这样查找的时候就是从开始位置到当前的介绍位置
endIndex = midIndex - 1;
} else if (target > midValue) {
// 如果目标值大于中间值,说明目标值在右侧,则需要继续在右侧查找
startIndex = midIndex + 1;
} else {
// 说明目标值与当前中间值相等,直接返回中间位置
return midIndex;
}
}
// 当循环结束都还没有找到目标值,说明数组中不存在这个值,直接返回-1
return -1;
};
// 功能测试
const arr = [10, 30, 50, 80, 100, 120, 150, 180];
const target = 100;
console.log(binarySearch(arr, target)); // 4
上述的代码中,我们通过循环来完成二分查找,最终的执行效果可以狠戳这里。
下面我们再一起来看一下递归的实现方式,具体代码如下:
/**
* 二分查找(递归)
* @param arr number[]
* @param target number
* @param startIndex 可选 number
* @param endIndex 可选 number
* @return number
*/
const binarySearch2 = (arr: number[], target: number, startIndex?: number, endIndex?: number):number => {
// 获取到数组的长度
const len = arr.length;
// 如果数组长度为0,则直接返回-1,表示没有找到
if (len === 0) return -1;
// 开始和结束的范围
if (startIndex == null) startIndex = 0;
if (endIndex == null) endIndex = len - 1;
// 如果开始位置和结束位置重合了,则直接结束
if (startIndex > endIndex) return -1;
// 获取中间位置
const midIndex = Math.floor((startIndex + endIndex) / 2);
// 获取中间值
const midValue = arr[midIndex];
// 如果目标值小于中间值,说明目标值在左边,则需要继续在左侧查找
if (target < midValue) {
// 只需要将结束位置替换为中间位置的左侧第一个位置即可
return binarySearch(arr, target, startIndex, midIndex - 1);
} else if (target > midValue) {
// 如果目标值大于中间值,说明目标值在右侧,则需要继续在右侧查找
return binarySearch(arr, target, midIndex + 1, endIndex);
} else {
// 说明目标值与当前中间值相等,直接返回中间位置
return midIndex;
}
}
// 功能测试
const arr = [10, 30, 50, 80, 100, 120, 150, 180];
const target = 80;
console.log(binarySearch(arr, target)); // 3
上述的代码中,我们通过循环来完成二分查找,最终的执行效果可以狠戳这里。
性能分析
在上面我们通过循环和递归实现了二分查找,那么这两种实现方式,哪种会更快一些呢?我们可以一起做一个性能的测试,代码如下:
const arr = [10, 30, 50, 80, 100, 120, 150, 180];
const target = 120;
// 我们执行100W次来进行对比
console.time('binarySearch1');
for (let i = 0; i < 100 * 10000; i++) {
binarySearch1(arr, target);
}
console.timeEnd('binarySearch1');
console.time('binarySearch2');
for (let i = 0; i < 100 * 10000; i++) {
binarySearch2(arr, target);
}
console.timeEnd('binarySearch2');
通过执行,我们可以在控制台中看到这两个方法实现的效率,递归执行的时间大概是循环的两倍,具体如下图所示:
其实通过上述的图我们可以发现,递归相对来说只是比循环慢一点,但它们本身的时间复杂度是一样的,都是O(logn),当然递归的实现思路相对循环来说会简单一些,因此这两种实现的方式我们可以根据自己的喜欢来进行学习和理解。
最后
我们通过用js实现二分查找说明了一个问题,凡是有序的数据,我们都必须用二分来进行查找;凡是二分查找,时间复制度必包含O(logn);并且我们还通过递归和非递归两种模式来实现了相同的功能,并做了相关的对比,总结来说,递归更好理解,非递归性能"更好",具体如何取舍就根据个人的开发喜好来即可。
最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家