【算法初探】前端学算法之用JS实现二分查找

284 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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');

通过执行,我们可以在控制台中看到这两个方法实现的效率,递归执行的时间大概是循环的两倍,具体如下图所示:

image.png

其实通过上述的图我们可以发现,递归相对来说只是比循环慢一点,但它们本身的时间复杂度是一样的,都是O(logn),当然递归的实现思路相对循环来说会简单一些,因此这两种实现的方式我们可以根据自己的喜欢来进行学习和理解。

最后

我们通过用js实现二分查找说明了一个问题,凡是有序的数据,我们都必须用二分来进行查找;凡是二分查找,时间复制度必包含O(logn);并且我们还通过递归非递归两种模式来实现了相同的功能,并做了相关的对比,总结来说,递归更好理解,非递归性能"更好",具体如何取舍就根据个人的开发喜好来即可。

最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

往期回顾

【算法初探】前端学算法之旋转数组(1)

【算法初探】前端学算法之旋转数组(2)

【算法初探】前端学算法之有效的括号

【算法初探】前端学算法之用栈实现队列

【算法初探】前端学算法之反转单向链表

【算法初探】前端学算法之用链表实现队列