从三道经典的leetcode题掌握二分法

简介: 前言二分法是典型的搜索算法,其主要运用于有序序列中寻找目标值。其思路非常简单,就是不断比较搜索区间的中间值与目标值,当中间值等于目标值则结束搜索,如果中间值大于目标值,则继续搜索左半区间,反之搜索右半区间。总结下,二分法就是在搜索过程中不断地将搜索区间减半,直到搜索到目标值或者搜索区间为空集。

前言


二分法是典型的搜索算法,其主要运用于有序序列中寻找目标值。其思路非常简单,就是不断比较搜索区间的中间值与目标值,当中间值等于目标值则结束搜索,如果中间值大于目标值,则继续搜索左半区间,反之搜索右半区间。


总结下,二分法就是在搜索过程中不断地将搜索区间减半,直到搜索到目标值或者搜索区间为空集。


实践


二分法的思路很简单,只要按照前面描述即可实现。那为何我们还要去学习掌握呢?

那是因为二分法看起来非常简单,但是却非常考验细节,我们称其为细节魔鬼也不为过。下面我们通过leetcode真题来学习。


leetcode-704 二分查找


我们先来个最简单的二分查找


leetcode-704


给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。(nums无重复)


const search = function(nums, target) {
  // 初始区间
  let left = 0
  let right = nums.length - 1
  // 结束条件 [right, left]搜索区间为空
  while(right >= left) {
    // 中间值
    // 注意区分 Math.floor((right + left) / 2) 的写法
    // 这种写法可以保证不会出现整数溢出
    const mid = left + Math.floor((right - left) / 2)
    // 等于目标值返回
    if (nums[mid] === target) return mid
    // 大于目标值收缩右区间
    // 注意mid已经被搜索过了
    // 所以是mid-1
    if (nums[mid] > target) {
      right = mid - 1
    }
    // 小于目标值收缩左区间
    // 同理mid+1
    if (nums[mid] < target) {
      left = mid + 1
    }
  }
  // 没有目标值返回-1
  // 前提是我们搜索条件是 right >= left
  // 可以保证已经搜索完所有元素
  return -1
};
复制代码


看我写完感觉不难,但是如果自己动手写呢?


我们来改变下搜索的结束条件,将 right >= left 改为 right > left

const search = function(nums, target) {
  let left = 0
  let right = nums.length - 1
  // 结束条件更改为 [left, left] 此时还剩下唯一值未经搜索
  while(right > left) {
    const mid = left + Math.floor((right - left) / 2)
    if (nums[mid] === target) return mid
    if (nums[mid] > target) {
      right = mid - 1
    }
    if (nums[mid] < target) {
      left = mid + 1
    }
  }
  // 相应需要对比剩余值和目标值完成搜索
  return nums[left] === target ? left : -1
};
复制代码


leetcode-35 搜索插入位置


leetcode-35


给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。(nums无重复)

const searchInsert = function(nums, target) {
  let left = 0
  let right = nums.length - 1
  let ans = null
  // 我们保持结束条件不变
  while(left <= right) {
    const mid = left + Math.floor((right - left) / 2)
    // 返回目标值索引
    if (nums[mid] === target) return mid
    if (nums[mid] > target) {
      // 我们在这引入新变量来存储位置
      // 实际将更新为第一个大于目标值的元素索引
      ans = mid
      right = mid - 1
    }
    if (nums[mid] < target) {
      left = mid + 1
    }
  }
  // 返回插入位置
  return ans
};
复制代码


leetcode-34 在排序数组中查找元素的第一个和最后一个位置


我们再来看看如何搜索边界值吧

leetcode-34


给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。


我们先来看看如何搜索左边界吧

const searchLeft = function(nums, target) {
  let left = 0
  let right = nums.length - 1
  // 同样我们引入变量来存储位置
  let first = -1
  while(left <= right) {
    const mid = left + Math.floor((right - left) / 2)
    if (nums[mid] === target) {
      // 当元素值和目标值相等的时候
      // 我们存储当前索引并缩减右区间 
      // 这样可以让我们有机会往左搜索左边界
      first = mid
      right = mid - 1
    }
    if (nums[mid] > target) {
      right = mid - 1
    }
    // 这部分保持不变
    // 只要搜索值小于目标值才会更新left值
    // 可以保证left始终小于左边界或者等于左边界
    // 如果等于左边界也不用担心
    // 因为此索引会在我们所减右区间时被搜索
    if (nums[mid] < target) {
      left = mid + 1
    }
  }
}
// 此时first变是左边界或者保持为-1
return first
复制代码


同理,搜索右边界的代码也是一样的

const searchRight = function(nums, target) {
  let left = 0
  let right = nums.length - 1
  let last = -1
  while(left <= right) {
    const mid = left + Math.floor((right - left) / 2)
    if (nums[mid] === target) {
      // 唯一的改变点就是此时缩减左区间
      // 以此达到往右到右边界的目的
      last = mid
      left = mid - 1
    }
    if (nums[mid] > target) {
      right = mid - 1
    }
    if (nums[mid] < target) {
      left = mid + 1
    }
  }
}
return last
复制代码


以搜索左边界为例,我们来试试改变搜索结束条件后的代码

const searchLeft = function(nums, target) {
  let left = 0
  let right = nums.length - 1
  // 我们改变结束条件为left<right
  // 也就是结束后剩下[left,left]未搜索
  while(left < right) {
    const mid = left + Math.floor((right - left) / 2)
    // 当搜索值大于目标值时我们缩减右区间
    // 此时的区别在于更新右区间为mid而非mid-1
    // 这边实际和搜索结束条件是有联系的
    // 如果搜索条件为left<=right有可能导致搜索区间缩减为[right,right]后陷入死循环
    if (nums[mid] >= target) {
      right = mid
    }
    if (nums[mid] < target) {
      left = mid + 1
    }
  }
}
// 同样的我们需要完成最后一个值的搜索
return nums[left] === target ? left : -1
复制代码


算法公式


通过前面的题我们可以总结出一套常用的二分法公式


const search = (nums, target) => {
  let left = 0
  let right = nums.length - 1
  // 有时候也可以是left<right
  // 如果可以的话
  // 我们尽量设置为left<=right完成全区间搜索
  // 也方便统一
  while(left <= right) {
    const mid = left + Math.floor((right - left) / 2)
    if (nums[mid] === target) {
      // ...
    }
    if (nums[mid] > target) {
      // ...
    }
    if (nums[mid] < target) {
      // ...
    }
  }
  return // ...
}
复制代码


总结


今天我们通过三道经典题来掌握二分法,实际二分法还有很多其他的题型。虽然公式是大致相同的,但是其中的细节,包括初始边界,结束条件,以及循环体内的区间缩减都是很考验大家的。


相关文章
|
6月前
|
算法
六六力扣刷题数组之再刷二分法
六六力扣刷题数组之再刷二分法
43 0
Leetcode-每日一题886. 可能的二分法(种类并查集)
时间复杂度:O(2 * n + m),其中n表示点的个数,m表示dislikes数组的长度,维护一个2 * n的种类并查集,需要O(2 * n)的时间,find和union种类并查集需要O(m)的时间。
126 0
Leetcode-每日一题886. 可能的二分法(种类并查集)
|
算法 索引
【力扣】搜索插入位置 学习大神的二分法查找
【力扣】搜索插入位置 学习大神的二分法查找
【力扣】搜索插入位置 学习大神的二分法查找
|
算法
LeetCode 04寻找两个正序数组的中位数(困难)二分法
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。 请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。 你可以假设 nums1 和 nums2 不会同时为空。
119 0
LeetCode 04寻找两个正序数组的中位数(困难)二分法
|
人工智能 BI
LeetCode每日一题——886. 可能的二分法
给定一组 n 人(编号为 1, 2, …, n), 我们想把每个人分进任意大小的两组。每个人都可能不喜欢其他人,那么他们不应该属于同一组。
114 0
|
2月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
3月前
|
Python
【Leetcode刷题Python】剑指 Offer 32 - III. 从上到下打印二叉树 III
本文介绍了两种Python实现方法,用于按照之字形顺序打印二叉树的层次遍历结果,实现了在奇数层正序、偶数层反序打印节点的功能。
55 6
|
3月前
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
110 2
|
15天前
|
机器学习/深度学习 人工智能 自然语言处理
280页PDF,全方位评估OpenAI o1,Leetcode刷题准确率竟这么高
【10月更文挑战第24天】近年来,OpenAI的o1模型在大型语言模型(LLMs)中脱颖而出,展现出卓越的推理能力和知识整合能力。基于Transformer架构,o1模型采用了链式思维和强化学习等先进技术,显著提升了其在编程竞赛、医学影像报告生成、数学问题解决、自然语言推理和芯片设计等领域的表现。本文将全面评估o1模型的性能及其对AI研究和应用的潜在影响。
14 1
|
2月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口