vlambda博客
学习文章列表

一点就透的二分查找算法

69 求开方

题目描述

给定一个非负整数,求它的开方,向下取整。

例子1

输入: 8
输出: 2
解释: 8 的开方结果是 2.82842...,向下取整即是 2

思考 1

这个思路很简单,因为是求n的开方,可以查找1n/2的数字,这里就可以使用二分查找,如果发现等于n,则返回,如果大于n,则可以在比较小的一半中查找,如果发现小于n,则可以在比较大的一半中查找

当然这个题目还有其他的数学解法,这里我们只是了解二分查找的思想,其他的数学解法,可以自己去了解。

实现1

/**

 * @param{number}x

 * @return{number}

 */

exportdefault (x) => {

  consthalfX = Math.ceil(x / 2);

  letlow = 1;

  lethigh = halfX;

  while(low <= high) {

    letmid = Math.floor(low + (high -low) / 2);

    if(mid * mid === x) {

      returnmid;

    } elseif (mid * mid < x) {

      low =mid + 1;

    } else{

      high= mid - 1;

    }

  }

  returnlow * low > x ? low - 1: low + 1;

};

 

算法时间复杂度 O(lgn/2), 空间复杂度 O(1)

一点就透的二分查找算法


34 查找区间

题目描述

给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置。如果不存在该值,则两个返回值都设为-1

进阶使用Olgn)时间复杂度解决。

例子1

输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
解释: 数字 8 在第 3 位第一次出现,在第 4 位最后一次出现。

思考 1

这个思路很简单,因为数组是升序的,而且指明使用Olgn)方法解决,很明显使用二分查找解决。

这个也比较简单,就是常用的二分查找,如果最后没有发现,返回[-1,-1]就可以了

这里需要注意的就是你要找到target在数组中第一出现的位置和target最后一次出现的位置。

实现1

/**

 * @param{number[]}nums

 * @param{number}target

 * @return{number[]}

 */

exportdefault (nums, target) => {

  if(nums.length === 0|| (nums.length === 1&& nums[0]!== target)) {

    return[-1, -1];

  }

  if(nums.length === 1&& nums[0]=== target) {

    return[0, 0];

  }

 

  constlen = nums.length;

  letlow = 0;

  lethigh = len - 1;

 

  constres = [];

  while(low <= high) {

    // 为了防止数据溢出

    letmid = Math.floor(low + (high -low) / 2);

 

    if(nums[mid] === target) {

      letminFlag = false;

      letmaxTemp = mid;

      letminTemp = mid;

      while(minTemp >= 0 &&nums[minTemp] === target) {

       minTemp--;

      }

 

      if(minTemp + 1 !== mid) {

       res.push(minTemp + 1);

      } else{

       res.push(mid);

      }

      while(maxTemp < len && nums[maxTemp] === target) {

       maxTemp++;

      }

 

      if(maxTemp === mid + 1){

       res.push(mid);

      } else{

       res.push(maxTemp - 1);

      }

      returnres;

    }

    if(nums[mid] < target) {

      low =mid + 1;

    }

    if(nums[mid] > target) {

      high= mid - 1;

    }

  }

  if(res.length === 2){

    returnres.sort((a, b) => a - b);

  } else{

    return[-1, -1];

  }

};

 

算法时间复杂度 O(lgn), 空间复杂度 O(1)

一点就透的二分查找算法


81 旋转数组查找数字

题目描述

一个原本增序的数组被首尾相连后按某个位置断开( [1,2,2,3,4,5] → [2,3,4,5,1,2],在第一位和第二位断开),我们称其为旋转数组。给定一个值,判断这个值是否存在于这个旋转数组中。如果存在返回true,如果不存在返回false

例子1

输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true

思考 1

最简单的肯定直接遍历数组,不过这里显然不使用这种

可以看到数组是一部分升序,另外一部分也是升序,问题是如何找到在哪里分割开?

当然这里可以遍历数组,找到分割开的两个升序数组,但是这样那不如直接遍历,不用二分查找了。

那么是否可以换个角度,假设我们如果直接使用二分查找,会怎么样呢?

可以想一下

刚开始我是想根据midmid+1的关系来决定移动lowhigh或者根据midmid-1的关系来决定移动lowhigh,可是这样感觉逻辑很复杂

后来看了题解,才明白可以把midlowhigh的关系来移动指针,如果nums[mid] 小于nums[high],那肯定是升序,因为问的数组是升序的,如果target在这个升序数组中,那么就可以排除另一半了

nums[mid] 大于nums[low]的时候,说明这也是一个派系数组,如果同时target在这个排序数组呢,同时也能排除另外一半数组了

这里还有另外一种情况,因为数组中存在重复的数字,如果发现nums[mid]等于num[low],此时我们可以把low++,重新计算mid,计算target存在的范围

nums[mid]等于num[high],此时我们可以把high,重新计算mid,计算target存在的范围

但是运行之后,可以发现这里的二分查找和遍历数组使用时间基本差不多。

实现1

/**

 * @param{number[]}nums

 * @param{number}target

 * @return{boolean}

 */

exportdefault (nums, target) => {

  if(nums.length === 0|| !nums) {

    returnfalse;

  }

  if(nums.length === 1)return nums[0]=== target;

 

  letlow = 0;

  lethigh = nums.length - 1;

  while(low <= high) {

    letmid = Math.floor(low + (high -low) / 2);

    if(nums[mid] === target) {

      returntrue;

    }

    if(nums[mid] < nums[high]) {

      // 判断target是否在这个范围内

      if(target > nums[mid] && target <= nums[high]) {

        low= mid + 1;

      } else{

       high = mid - 1;

      }

    } elseif (nums[mid] > nums[low]) {

      if(target >= nums[low] && target < nums[mid]) {

       high = mid - 1;

      } else{

        low= mid + 1;

      }

    } elseif (nums[low] === nums[mid]) {

     low++;

    } else{

     high--;

    }

  }

  returnfalse;

};

 

 

算法时间复杂度 O(n),因为最坏的情况下,二分查找就变成了遍历数组了。空间复杂度 O(1)

一点就透的二分查找算法


其他的待续。。。。。。


高品质全面课程升级,2020最新SVIP豪华版