[typescript]二分查找算法进阶总结(包括旋转数组)

字节踏雪使
• 阅读 1357

关于二分查找的题型

普通的二分

  • LC704 二分查找 简单
  • LC34 在排序数组中查找元素的第一个和最后一个位置 中等

变体:旋转数组

  • LC153 寻找旋转排序数组的最小值 中等
  • LC33 搜索旋转排序数组 中等

二分通用技巧

最常用最基础的二分查找,接收一个数组,和一个target目标值,要寻找到这个目标,返回该目标的下标。找不到就返回-1。直接对应LC704的答案

function search(nums: number[], target: number): number {
    let left: number = 0, right: number = nums.length - 1
    while (left <= right) {
        let mid: number = Math.floor(left + (right - left) / 2)
        if (nums[mid] < target){
            left = mid + 1
        } else if (nums[mid] > target){
            right = mid - 1
        } else if (nums[mid] === target){
            return mid
        }
    }
    return -1
};

注意点:

  1. 不用 mid = Math.floor((left + right)/2) 的主要原因是怕超出最大值
  2. while里的left<=right而不是单纯的小于,是因为我们需要搜索的区域是一个闭区间,也就是[left,right],这样才能确定不漏掉一个数

但是,有些时候,目标值不止一个,也就是说可能有多个目标值,我们需要找到其左边界和右边界,所以上面的算法靠不住了,但是依然可以在上面的算法中做一定的改动。这里主要学习到的是labuladong的算法秘籍中提到的思路。

假设在一个新的数组中,[1,2,3,3,3,4],我们要找target = 3的左右边界,怎么办呢?

这里直接贴LC34里我的解法,超过了98%的同学

//main function
function searchRange(nums: number[], target: number): number[] {
    return [leftBound(nums,target), rightBound(nums,target)]
};

//find left bound
function leftBound(nums: number[], target: number): number {
    let left: number = 0, right: number = nums.length - 1
    while (left <= right){
        let mid: number = Math.floor(left + (right - left)/2)
        if (nums[mid] < target){
            left = mid + 1
        } else if (nums[mid] > target){
            right = mid - 1
        } else {
            right = mid - 1
        }
    }
    //重点部分
    if (left >= nums.length || nums[left] !== target) {
        return -1
    } else {
        return left
    }
}


//find right bound
function rightBound(nums: number[], target: number): number {
    let left: number = 0, right: number = nums.length - 1
    while (left <= right){
        let mid: number = Math.floor(left + (right - left)/2)
        if (nums[mid] < target){
            left = mid + 1
        } else if (nums[mid] > target){
            right = mid - 1
        } else {
            left = mid + 1
        }
    }
    //重点部分
    if (right < 0 || nums[right] !== target) {
        return -1
    } else {
        return right
    }
}

这个题目的意思很简单,就是让我们找到左右边界而已,那我们分别去找左边界和右边界就行。仔细观察的话,会发现和基础的差别不大,但是多出来两行判断结果的部分。然后在nums[mid] === target 的时候,我们没有去输出,而是缩小边界继续查找。

重点的部分,在于理解,什么时候会跳出循环,我们给定的条件是left <= right, 因此,当left > right 的时候,就可以跳出循环。

先考虑leftBound这个函数,当nums[mid]正好等于左边界的时候,我们的right再次缩小范围到左边界-1的位置,此之后的left会不停的增大直到和right相等,最后left = mid + 1 便刚刚好大于right,且等于左边界的位置。

跳出循环之后,再次进行判断,因为我们没有考虑两种极端情况,一种是left超过边界,一种是找到的左边界并不等于target,这样算出来的结果就是正确的。

旋转数组:二分查找的变体

在leetcode中,有不少旋转数组的题,这里挑两个聊聊。一个是LC153的最基础的旋转数组。像是[3,4,5,0,1,2]就是典型的旋转数组,我们可以发现规律,就是除了中间一个点是无序的,其余的都是有序的,那我们是不是通过二分查找可以去找到这个旋转点呢?(有的同学选择直接遍历,也不是不行,就是在数据量巨大的时候,时间复杂度很高)在这个题目中,我们只要输出0就可以,也就是所说的旋转点。直接贴答案

function findMin(nums: number[]): number {
    let left: number = 0, right: number = nums.length - 1
    while (left < right){
        let mid: number = Math.floor(left + (right - left) /2)
        if (nums[mid] < nums[right]){
            right = mid
        } else if (nums[mid] > nums[right]){
            left = mid + 1
        }
    }
    return nums[left]
};

这里又出现了一个比较坑的点,为什么是left < right呢,一个等号区别这么大吗? 我试过改为小于等于,再改点其他地方七七八八,发现都得不到正确的结果。咱来仔细分析一下原因。

之前说过一个问题,那就是判断循环的出口条件,这里的话就是left === right,循环就结束了,我们想找到的旋转点是最小值,所以当我们在mid刚好是旋转点时,肯定是进入的第一个判断。

为什么呢?为什么我能确定mid一定比此时的right小呢,第一个原因是Math.floor,mid跟right相等的唯一条件是left === right,就退出循环了。所以我们下一步得到的是mid = 旋转点 - 1,此时对应的数值是最大值,所以肯定比刚才的right大,然后left和right相等,退出循环,返回left和right皆可。

所以说二分查找,如果要讨论的非常细致的话,边界条件是考虑的非常复杂的。

最后看一个题,是LC33,搜索旋转排序数组,其实就是把旋转数组和二分查找的经典模版结合而已。

function search(nums: number[], target: number): number {
    const spin = findSpinIndex(nums)
    if (target < nums[spin] || target > nums[spin - 1]) {
        return -1
    } else if (target > nums[nums.length - 1]){
        return binarySearch(nums, target, 0, spin - 1)
    } else if (target <= nums[nums.length - 1]){
        return binarySearch(nums, target, spin, nums.length - 1)
    }
};

function findSpinIndex(nums:number[]): number {
    let left: number = 0, right: number = nums.length - 1
    while (left < right){
        let mid: number = Math.floor(left + (right - left) / 2)
        if (nums[mid] < nums[right]){
            right = mid
        } else if (nums[mid] > nums[right]){
            left = mid + 1
        }
    }
    return left
}

function binarySearch(nums: number[], target: number, leftBound: number, rightBound: number): number {
    let left: number = leftBound, right: number = rightBound
    while (left <= right){
        let mid: number = Math.floor(left + (right - left) / 2)
        if (nums[mid] > target){
            right = mid - 1
        } else if (nums[mid] < target){
            left = mid + 1
        } else if (nums[mid] === target) {
            return mid
        }
    } 
    return -1 
}

这个题没什么特别需要注意的地方,注意一下主函数的边界判断就好

点赞
收藏
评论区
推荐文章
林末 林末
4年前
折半查找-Python版(二分查找)
介绍二分查找也称折半查找(BinarySearch),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。前提必须待查找的序列有序时间复杂度O(log2n)原理1)确定该期间的中间位置K2)将查找的值t与arrayk比较,若相等,查找成功返回此位置;否则确定新的查找区域,继续二分
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
好买-葡萄 好买-葡萄
3年前
【数据结构与算法】—— 二分查找
1.二分查找的概念二分查找指的是在排好序的数组中,找到目标元素。如果元素存在则返回元素的下标,不存在则返回1.下面以升序为例进行简单描述2.查找过程:取数组中间元素与查找元素target比较。如果target等于中间元素则直接返回中间元素的下标,如果target小于数组中间元素则在数组左边查找,如果target大于数组中间元素则在右边查找。重复以上步骤。
Wesley13 Wesley13
3年前
java.util.Arrays,java.lang.Math,java.lang.System 类的常用方法汇总
java.util.Arrays类是数组的工具类,一般数组常用的方法包括二分查找:publicstaticint binarySearch(array\\,intkey),返回key的下标index扩容缩容:publicstaticint\\ copyOf(array\\,newLength),返回新数组取部分:publ
Wesley13 Wesley13
3年前
java泛型的二分查找
随便测试了一下不知道是否完全正确/CreatedbyVoidYoungon7:57PM6/25/2016.IDEATest.泛型二分查找此处返回的负数为在list里1开始计算的次序位置,不是数组下标/publicclassGe
九路 九路
4年前
7 二分搜索树的原理与Java源码实现
1折半查找法了解二叉查找树之前,先来看看折半查找法,也叫二分查找法在一个有序的整数数组中(假如是从小到大排序的),如果查找某个元素,返回元素的索引。如下:intarrnewint{1,3,4,6,8,9};在arr数组中查找6这个元素,查到返回对应的索引,没有找到就返回1思想很简单:1先找到数组中间元素ta
九路 九路
4年前
二分查找法的递归和非递归的实现
//二分查找法非递归实现,在一个有序的数组中查找e元素的位置,找不到返回1publicstaticintbinarySearch(intdata,inte){intl0;intrdata.length1;while(l<r){
Wesley13 Wesley13
3年前
JAVA并发工具类
一、分而治之fork/join   二叉树  二分查找  快速排序  归并排序  mapreduce  动态规划1、fork/join(工作密取)  RecursiveTask要有返回值  RecursiveAction没有返回值  invoke(同步)  submit(有返回结果异步)  execute(没有返
Stella981 Stella981
3年前
Kafka中改进的二分查找算法
最近有学习些Kafak的源码,想给大家分享下Kafak中改进的二分查找算法。二分查找,是每个程序员都应掌握的基础算法,而Kafka是如何改进二分查找来应用于自己的场景中,这很值得我们了解学习。由于Kafak把二分查找应用于索引查找的场景中,所以本文会先对Kafka的日志结构和索引进行简单的介绍。在Kafak中,消息以日志的形式保存,每个日志其实就是一个文
Wesley13 Wesley13
3年前
C#二分查找算法设计实现
C二分查找算法设计实现1.介绍二分查找也称折半查找(BinarySearch),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。(记住了前提要求是顺序存储结构,而且要有序排序,所以说对于一个无序的是没法用二分查找的)2.查找算法过程
菜园前端 菜园前端
2年前
什么是二分搜索?
原文链接:什么是二分搜索?二分搜索是一种比较高效的搜索算法,但前提必须是有序数组。主要步骤如下:1.从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束2.如果目标值大于或者小于中间元素,则在大于或者小于中间元素的那一半数组中继续二分搜索基础案例时间