百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

一点就透的二分查找算法(二分查找计算公式)

itomcoil 2025-06-15 16:59 9 浏览

无处不在的二分查找?

1 二分查找在实际中应用的很多,但是思想确实很简单,就是类似于分治的思想,比如你想从1000甚至更多的数字中寻找特定的数,如果你挨个去查找,当然可以,但是如果可以每次查找就可以确定想要查找的数不在另外一半中,是不是要快很多。

二分查找就是这么简单,只要记住,找到方法可以把范围缩小一半,就可以使用二分查找。

69 求开方

题目描述

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

例子1

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

例子2

输入: 4
输出: 2
解释: 4 的开方结果是 2。

说明
0 <= x <= 2^31 - 1

思考 1

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

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

实现1

/**
 * @param {number} x
 * @return {number}
 */
export default (x) => {
  const halfX = Math.ceil(x / 2);
  let low = 1;
  let high = halfX;
  while (low <= high) {
    let mid = Math.floor(low + (high - low) / 2);
    if (mid * mid === x) {
      return mid;
    } else if (mid * mid < x) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  return low * low > x ? low - 1 : low + 1;
};

复制代码

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

34 查找区间

题目描述

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

进阶
使用O(lgn)时间复杂度解决。

例子1

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

例子2

输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
解释: 6 在数组中没有出现

例子3

输入: nums = [3,3,3], target = 3
输出: [0,2]
解释: 3 在数组中出现第一次位置是0,最后一次的位置2

说明
1 0 <= nums.length <= 10^5
2 -10^9 <= nums[i] <= 10^9
3 -10^9 <= target <= 10^9

思考 1

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

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

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

实现1

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
export default (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];
  }

  const len = nums.length;
  let low = 0;
  let high = len - 1;

  const res = [];
  while (low <= high) {
    // 为了防止数据溢出
    let mid = Math.floor(low + (high - low) / 2);

    if (nums[mid] === target) {
      let minFlag = false;
      let maxTemp = mid;
      let minTemp = 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);
      }
      return res;
    }
    if (nums[mid] < target) {
      low = mid + 1;
    }
    if (nums[mid] > target) {
      high = mid - 1;
    }
  }
  if (res.length === 2) {
    return res.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

例子2

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

思考 1

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

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

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

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

可以想一下

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

后来看了题解,才明白可以把mid和low和high的关系来移动指针,如果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}
 */
export default (nums, target) => {
  if (nums.length === 0 || !nums) {
    return false;
  }
  if (nums.length === 1) return nums[0] === target;

  let low = 0;
  let high = nums.length - 1;
  while (low <= high) {
    let mid = Math.floor(low + (high - low) / 2);
    if (nums[mid] === target) {
      return true;
    }
    if (nums[mid] < nums[high]) {
      // 判断target是否在这个范围内
      if (target > nums[mid] && target <= nums[high]) {
        low = mid + 1;
      } else {
        high = mid - 1;
      }
    } else if (nums[mid] > nums[low]) {
      if (target >= nums[low] && target < nums[mid]) {
        high = mid - 1;
      } else {
        low = mid + 1;
      }
    } else if (nums[low] === nums[mid]) {
      low++;
    } else {
      high--;
    }
  }
  return false;
};


复制代码

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

154 旋转数组查中寻找最小的元素

题目描述

假设一个排好序的数组在某处执行了一次“旋转”,找出最小的元素(数组元素可能重复),数组中包含重复数字

例子1

输入: [1,3,5]
输出: 1

例子2

输入: [2,2,2,0,1]
输出: 0

例子3

输入: [3,3,1,3]
输出: 1

例子4

输入: [10,1,10,10,10]
输出: 1

思考 1

这里和上面的81题目有点类似,应该可以采用同样的策略,只不过是把81的题目的target变成了最小的数字,思路基本类似。

另外说一下,这题在leetcode上标记为hard,但是81题标记为medium,但是两者的难度差不多,所以有时候,没有必要对题目的难度过于太在意

实现1

/**
 * @param {number[]} nums
 * @return {number}
 */
// 2, 2, 2, 0, 1;
export default (nums) => {
  if (nums.length === 0 || !nums) {
    return false;
  }
  if (nums.length === 1) return nums[0];
  let low = 0;
  let high = nums.length - 1;
  let min = Number.MAX_VALUE;
  while (low <= high) {
    let mid = Math.floor(low + (high - low) / 2);
    if (nums[mid] < nums[high]) {
      high = mid - 1;
      min = Math.min(nums[mid], min);
    } else if (nums[mid] > nums[low]) {
      min = Math.min(nums[low], min);
      low = mid + 1;
    } else if (nums[low] === nums[mid]) {
      min = Math.min(nums[low], min);
      low++;
    } else {
      min = Math.min(nums[high], min);
      high--;
    }
  }
  return min;
};



复制代码

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

540. 有序数组中的单一元素

题目描述

给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。

例子1

输入: [1,1,2,3,3,4,4,8,8]
输出: 2

例子2

输入: [3,3,7,7,10,11,11]
输出: 10

注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。

思考 1

很简单,用二分查找就可以,要点就是因为数组中除了一个数字,其它的数字都是出现两次,所以可以根据mid的下标是奇数或者偶数来判断数字是否出现在那一侧

如果mid是偶数,那说明low到mid有奇数个数字,所以就可以判断nums[mid] 和nums[mid-1]是否相等,来判断只出现一次的数字是否出现在这一侧。

思路比较简单

实现1

/**
 * @param {number[]} nums
 * @return {number}
 */

export default (nums) => {
  if (nums.length === 1) return nums[0];
  let low = 0;
  let high = nums.length - 1;
  while (low <= high) {
    let mid = Math.floor(low + (high - low) / 2);
    console.log(low, high, mid);
    if (
      (mid + 1 < nums.length && nums[mid] !== nums[mid + 1] && mid >= 1 && nums[mid] !== nums[mid - 1]) ||
      (mid === nums.length - 1 && nums[mid] !== nums[mid - 1]) ||
      (mid === 0 && nums[mid] !== nums[mid + 1])
    ) {
      return nums[mid];
    }
    if (mid % 2 !== 0) {
      if (mid >= 1 && nums[mid - 1] === nums[mid]) {
        low = mid + 1;
      } else {
        high = mid - 1;
      }
    } else {
      if (mid >= 1 && nums[mid] === nums[mid - 1]) {
        high = mid - 1;
      } else {
        low = mid + 1;
      }
    }
  }
  return nums[low];
};

};



复制代码

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

4. 寻找两个正序数组的中位数

题目描述

给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。

进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?

例子1

输入: nums1 = [1,3], nums2 = [2]
输出: 2.00000
解释:合并数组 = [1,2,3] ,中位数 2

例子2

输入: nums1 = [1,2], nums2 = [3,4]
输出: 2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

例子3

输入: nums1 = [0,0], nums2 = [0,0]
输出: 0.00000

例子4

输入: nums1 = [], nums2 = [1]
输出: 1.00000

例子5

输入: nums1 = [2], nums2 = []
输出: 2.00000

提示:
1 nums1.length == m
2 nums2.length == n
3 0 <= m <= 1000
4 0 <= n <= 1000
5 1 <= m + n <= 2000
6 -10^6 <= nums1[i], nums2[i] <= 10^6

思考 1

题目如果第一次见到,很难想出如何使用二分查找的,不过也可以思考试试

首先判断nums1和nums2的数组长度,让nums1的数组长度小于等于nums2的数组长度

假设k是nums1和nums2合并之后,我们要寻找的中位数下标,这里如果nums1和nums2合并后的长度是奇数,我们可以寻找 k = left的数字

const left = Math.floor((nums1Len + nums2Len + 1) / 2)
复制代码

如果nums1和nums2合并后的长度是偶数,我们可以分别寻找合并后的数组中下标分别是下面这两个位置的,也就是寻找k = left和k=right两个位置的数字

const left = Math.floor((nums1Len + nums2Len + 1) / 2);
  const right = Math.floor((nums1Len + nums2Len + 2) / 2);
复制代码

原理很简单,我们先从nums1中拿出k/2个数字和从nums2中拿出k/2个数字,如果nums1[k/2] 大于nums2[k/2],那么我们要寻找的k位置的数字,肯定在nums1和nums2除去0到k/2的数组中。

因为nums1[k/2]大于nums2[k/2],所以就可以排除nums[k/2]之前的数字,但是我们不知道nums1的数字是否都大于nums2[k/2],所以可以在剩下的数组中寻找排在k/2位置的数字。

实现1

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number}
 */
const getKth = (nums1, start1, end1, nums2, start2, end2, k) => {
  const len1 = end1 - start1 + 1;
  const len2 = end2 - start2 + 1;
  // 如果nums1的长度大于nums2的长度,改变两个数组的位置, 让数组长度最小的在前面,防止
  // 这里的nums2[start2 + k - 1]报错
  if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
  if (len1 === 0) return nums2[start2 + k - 1];
  if (k === 1) return Math.min(nums1[start1], nums2[start2]);

  const i = start1 + Math.min(len1, Math.floor(k / 2)) - 1;
  const j = start2 + Math.min(len2, Math.floor(k / 2)) - 1;

  const nums1End = Math.min(end1, i + 1);
  const nums2End = Math.min(end2, j + 1);
  if (nums1[i] > nums2[j]) {
    return getKth(nums1, start1, nums1End, nums2, j + 1, end2, k - (j - start2 + 1));
  } else {
    return getKth(nums1, i + 1, end1, nums2, start2, nums2End, k - (i - start1 + 1));
  }
};
export default (nums1, nums2) => {
  if (nums1.length === 0 && nums2.length === 1) {
    return nums2[0];
  }
  if (nums1.length === 1 && nums2.length === 0) {
    return nums1[0];
  }
  const nums1Len = nums1.length;
  const nums2Len = nums2.length;
  // 分别找到奇数和偶数的中位数的左边和右边
  const left = Math.floor((nums1Len + nums2Len + 1) / 2);
  const right = Math.floor((nums1Len + nums2Len + 2) / 2);
  // 如果是奇数,只寻找一次就可以了
  if ((nums1Len + nums2Len) % 2 !== 0) {
    return getKth(nums1, 0, nums1Len - 1, nums2, 0, nums2Len - 1, left);
  }
  return (
    (getKth(nums1, 0, nums1Len - 1, nums2, 0, nums2Len - 1, left) +
      getKth(nums1, 0, nums1Len - 1, nums2, 0, nums2Len - 1, right)) *
    0.5
  );
};

复制代码

算法时间复杂度也很容易理解
m 是nums1.length,n 是nums2.length 因为每次查找的范围都从(m+n)/2 缩小一半范围。所以时间复杂度是O(lg(m+n))
空间复杂度 O(lg(m+n))

二分查找算法总结

二分查找其实就是一个分值思想,如果你可以根据一些条件,每次缩小一半的查找范围,基本就可以确定使用二分查找。

不过有些可能不是很明显,可能需要经过转化处理,不过核心就是可以不断的缩小查找范围,让我们离答案更近一下。

相关推荐

Java 如何从一个 List 中随机获得元素

概述从一个List中随机获得一个元素是有关List的一个基本操作,但是这个操作又没有非常明显的实现。本页面主要向你展示如何有效的从List中获得一个随机的元素和可以使用的一些方法。选择一个...

想月薪过万吗?计算机安卓开发之&quot;集合&quot;

集合的总结:/***Collection*List(存取有序,有索引,可以重复)*ArrayList*底层是数组实现的,线程不安全,查找和修改快,增和删比较慢*LinkedList*底层是...

China Narrows AI Talent Gap With U.S. as Research Enters Engineering Phase: Report

ImagegeneratedbyAITMTPOST--ChinaisclosinginontheU.S.intheAIindustry-academia-research...

大促系统优化之应用启动速度优化实践

作者:京东零售宋维飞一、前言本文记录了在大促前针对SpringBoot应用启动速度过慢而采取的优化方案,主要介绍了如何定位启动速度慢的阻塞点,以及如何解决这些问题。希望可以帮助大家了解如何定位该类问...

MyEMS开源能源管理系统核心代码解读004

本期解读:计量表能耗数据规范化算法:myems/myems-normalization/meter.py代码见底部这段代码是一个用于计算和存储能源计量数据(如电表读数)的小时值的Python脚本。它主...

Java接口与抽象类:核心区别、使用场景与最佳实践

Java接口与抽象类:核心区别、使用场景与最佳实践一、核心特性对比1.语法定义接口:interface关键字定义,支持extends多继承接口javapublicinterfaceDrawabl...

超好看 vue2.x 音频播放器组件Vue-APlayer

上篇文章给大家分享了视频播放器组件vue-aliplayer,这次给大家推荐一款音频插件VueAplayer。vue-aplayer一个好看又好用的轻量级vue.js音乐播放器组件。清爽漂亮的U...

Linq 下的扩展方法太少了,MoreLinq 来啦

一:背景1.讲故事前几天看同事在用linq给内存中的两个model做左连接,用过的朋友都知道,你一定少不了一个叫做DefaultIfEmpty函数,这玩意吧,本来很流畅的from......

MapReduce过程详解及其性能优化(详细)

从JVM的角度看Map和ReduceMap阶段包括:第一读数据:从HDFS读取数据1、问题:读取数据产生多少个Mapper??Mapper数据过大的话,会产生大量的小文件,由于Mapper是基于虚拟...

手把手教你使用scrapy框架来爬取北京新发地价格行情(实战篇)

来源:Python爬虫与数据挖掘作者:霖hero前言关于Scrapy理论的知识,可以参考我的上一篇文章,这里不再赘述,直接上干货。实战演练爬取分析首先我们进入北京新发地价格行情网页并打开开发者工具,如...

屏蔽疯狂蜘蛛,防止CPU占用100%(mumu模拟器和雷电模拟器哪个更占用cpu)

站点总是某个时间段莫名的cpu100%,资源占用也不高,这就有必要怀疑爬虫问题。1.使用"robots.txt"规范在网站根目录新建空白文件,命名为"robots.txt&#...

Web黑客近年神作Gospider:一款基于Go语言开发的Web爬虫,要收藏

小白看黑客技术文章,一定要点首小歌放松心情哈,我最爱盆栽!开始装逼!Gospider是一款运行速度非常快的Web爬虫程序,对于爱好白帽黑客的小白来说,可谓是佳作!Gospider采用厉害的Go语言开发...

用宝塔面板免费防火墙屏蔽织梦扫描网站

今天教大家在免费的基础上屏蔽织梦扫描,首先您要安装宝塔面板,然后再安装免费的防火墙插件,我用的是Nginx免费防火墙,然后打开这个插件。设置GET-URL过滤设置一条简单的宝塔面板的正则规则就可以屏蔽...

蜘蛛人再捞4千万美元 连续三周蝉联北美票房冠军

7月15日讯老马追踪票房数据的北美院线联盟今天表示,“蜘蛛人:离家日”(Spider-Man:FarFromHome)击退两部新片的挑战,连续第2周勇夺北美票房冠军,海捞4530万美元。法新...

夏天到了,需要提防扁虱,真是又小又恐怖的动物

夏天马上要到了,你知道吗,扁虱是这个夏天最危险的动物之一,很少有动物能比它还凶猛。Whenitcomestosummer'slittledangers,fewarenastiert...