当前位置: 首页 > news >正文

二分查找思路详解,包含二分算法的变种,针对不同题的做法

在这里插入图片描述

👨‍💻程序员三明治:个人主页

🔥 个人专栏: 《设计模式精解》 《重学数据结构》

🤞先做到 再看见!

传统的解题思路

题目

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

思路

左闭右闭

class Solution {public int search(int[] nums, int target) {// 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算if (target < nums[0] || target > nums[nums.length - 1]) {return -1;}int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target) {return mid;}else if (nums[mid] < target) {left = mid + 1;}else { // nums[mid] > targetright = mid - 1;}}// 未找到目标值return -1;}
}

左闭右开

class Solution {public int search(int[] nums, int target) {int left = 0, right = nums.length;while (left < right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target) {return mid;}else if (nums[mid] < target) {left = mid + 1;}else { // nums[mid] > targetright = mid;}}// 未找到目标值return -1;}
}

153寻找排序数组中的最小值

题目

已知一个长度为 n 的数组,预先按照升序排列,经由 1n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:

输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

示例 3:

输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

思路

理解题意

「旋转」的定义是:把一个数组最开始的若干个元素「搬」到数组的末尾,也可以「不搬」元素。

分析旋转数组的特点

  • 多次旋转等价于旋转一次;
  • 只会有一次「转折」,一分为二看,一定有一段是有序的;
  • 重点理解 1:最大值和最小值相邻,即:最大值的右边,如果有的话,一定是最小值;
  • 重点理解 2:如果两点是上升的,那么两点之间一定是上升的。

下面说明如果两点是上升的,那么两点之间一定是上升的。如图:

左边 < 中间,从左边到中间就一定是上升的,否则就不能称为是旋转有序数组。

在旋转有序数组上,有 3 个位置比较重要,它们分别是最左边元素、中间元素和最右边元素。

「比较最左边和中间」还是「比较中间和最右边」?

  1. 比较左边和中间会发现,最小值可能在前面,也可能在后面

下图都满足最左边 < 中间,但是左图最小值在后面,右图最小值在前面。

最极端就是上图右边这种情况,最小值在数组的第 1 位。

  1. 比较中间和最右边可以确定最小值的位置

下图都满足中间 < 最右边,并且最小值都在前面。

最极端的情况下,当中间 < 最右边时,最小值在中间。

所以我们可以通过比较中间和最右边,知道旋转数组的最小值在哪里。如果要比较中间和最左边,需要做一些分类讨论,使得解决问题变得复杂。

左闭右闭

public class Solution {public int findMin(int[] nums) {int n = nums.length;int left = 0;int right = n - 1;while (left < right) {int mid = (left + right) / 2;if (nums[mid] < nums[right]) {// 下一轮搜索区间 [left..mid]right = mid;} else {// 因为题目中说:数组中不存在重复元素// 此时一定是 nums[mid] > nums[right]// 下一轮搜索区间 [mid + 1..right]left = mid + 1;}}// 一定存在最小元素,因此无需再做判断return nums[left];}
}
为什么这道题用 while (left < right)?

如果使用 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">while (left <= right)</font> 配合 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">right = mid</font>

  • <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">left == right</font> 时,<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">mid = left = right</font>
  • 如果进入 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">right = mid</font> 分支,状态不变 → 死循环

所以这道题采用 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">while (left < right)</font> 是为了:

  1. 避免死循环:在区间长度为1时自动退出
  2. 利用答案的唯一性:最终剩下的那个元素就是最小值
那为什么不能也把right = mid - 1呢?

问题所在:当 right = mid - 1跳过最小值!

154.寻找排序数组中的最小值②

题目

已知一个长度为 n 的数组,预先按照升序排列,经由 1n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
  • 若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

你必须尽可能减少整个过程的操作步骤。

示例 1:

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

示例 2:

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

思路

比上一题多出的一个条件是:数组中可能存在重复元素

nums[mid] = nums[right] 时,只能把 right 排除掉

因为

  • 如果去掉的数是最小值,那么 nums[mid] 也是最小值,这说明最小值仍然在数组中。
  • 如果去掉的数不是最小值,那么我们排除了一个错误答案。
public class Solution {public int findMin(int[] nums) {int n = nums.length;int left = 0;int right = n - 1;while (left < right) {int mid = (left + right) / 2;if (nums[mid] == nums[right]) {right--;} else if (nums[mid] < nums[right]) {// 下一轮搜索区间 [left..mid]right = mid;} else {// 因为题目中说:数组中不存在重复元素// 此时一定是 nums[mid] > nums[right]// 下一轮搜索区间 [mid + 1..right]left = mid + 1;}}// 一定存在最小元素,因此无需再做判断return nums[left];}
}

33.搜索旋转排序数组

题目

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 向左旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 下标 3 上向左旋转后可能变为 [4,5,6,7,0,1,2]

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

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

示例 2:

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

示例 3:

输入:nums = [1], target = 0
输出:-1

思路

最简单的做法, 先找到最值将旋转数组分成两段有序数组,接下来在有序数组中找目标值就轻车熟路了。

  • 先找到 「153. 寻找旋转排序数组中的最小值」的索引,由此可以将数组分为升序的两段。
  • 根据 nums[0] 与 target 的关系判断 target 在左段还是右段,再对升序数组进行二分查找即可。
  • 根据nums[n] 与 target的关系判断target在左段还是右段,再对升序数组进行二分查找即可

同样的思路可以解决「1095. 山脉数组中查找目标值」,即先找到山脉数组的峰顶「852. 山脉数组的峰顶索引」, 通过峰顶将山脉数组分为两段有序的数组,接下来就可以在有序数组中查找目标值了。

class Solution {public int search(int[] nums, int target) {int minValueIndex = findMin(nums);int n = nums.length - 1;if (target > nums[n]) {// target 在第一段(而且一定是旋转数组而非有序数组)return binarySearch(nums, 0, minValueIndex - 1, target);} else {// target 在第二段return binarySearch(nums, minValueIndex, n, target);}}// 153. 寻找旋转排序数组中的最小值(返回的是下标)public int findMin(int[] nums) {// 左闭右闭int left = 0;int right = nums.length - 1;while (left < right) {int mid = (left + right) / 2;if (nums[mid] < nums[right]) {right = mid;} else {left = mid + 1;}}return left;}public int binarySearch(int[] nums, int left, int right, int target) {while (left <= right) {int mid = (left + right) / 2;if (nums[mid] > target) {right = mid - 1;} else if (nums[mid] < target) {left = mid + 1;} else {return mid;}}return -1;}
}

81.搜索旋转排序数组②

题目

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 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

思路

思维误区就是以为在上一题的基础上继续使用两次二分去做,但是忽略了问题

33题(无重复元素)的情况:

  • 旋转点是唯一的
  • 返回准确的最小值位置
  • 数组被清晰地分为两个有序段
  • ✅ 两次二分法有效

81题(有重复元素)的情况:

  • 旋转点可能不唯一
  • 返回的"最小值位置"可能不是真正的旋转点
  • 划分的"有序段"实际上可能不是有序的

所以两次二分会退化为On,这题只能用一次二分!!!!

class Solution {public boolean search(int[] nums, int target) {int left = 0;int right = nums.length - 1;while (left <= right) {int mid = (left + right) / 2;// 情况1:直接找到目标值if (nums[mid] == target) {return true;}// 情况2:无法判断哪边有序(重复元素导致)if (nums[mid] == nums[left] && nums[mid] == nums[right]) {left++;right--;}// 情况3:左段有序 (nums[mid] >= nums[left])else if (nums[mid] >= nums[left]) {if (target >= nums[left] && target < nums[mid]) {right = mid - 1;} else {left = mid + 1;}} else {    // 情况4:右段有序if (target <= nums[right] && target > nums[mid]) {left = mid + 1;} else {right = mid - 1;}}}return false;}
}

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

题目

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

思路

定义函数,二分法找到>=这个元素的第一个位置,用左闭右闭

最后一个位置的话,我可以查找大于target+1的第一个位置,然后-1就得到了target的最后一个位置

class Solution {public int[] searchRange(int[] nums, int target) {int start = binarySearch(nums, target);if (start == nums.length || nums[start] != target) {return new int[]{-1, -1};}int end = binarySearch(nums, target + 1) - 1;return new int[]{start, end};}public int binarySearch(int[] nums, int target) {int left = 0;int right = nums.length - 1;while (left <= right) {int mid = (left + right) / 2;if (nums[mid] < target) {left = mid + 1; // nums[left-1] < target} else {right = mid - 1; // nums[right+1] >= target}}// 循环结束后 left = right+1,所以 left 就是第一个 >= target 的元素下标return left;}
}

如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!
在这里插入图片描述

http://www.dtcms.com/a/414064.html

相关文章:

  • 58同城枣庄网站建设wordpress 会员分值
  • C# .NetCore WebApi 性能改进 响应压缩
  • PyTorch CNN 改进:全局平均池化与 CIFAR10 测试分析
  • 精读C++20设计模式——创造型设计模式:单例模式
  • 网络实践——基于epoll_ET工作、Reactor设计模式的HTTP服务
  • 设计模式-行为型设计模式(针对对象之间的交互)
  • 选手机网站彩票网站开发制作模版
  • qq钓鱼网站在线生成器北京网站设计公司地址
  • SQL流程控制函数完全指南
  • 做电商网站前端的技术选型是移动商城积分和积分区别
  • 弄一个关于作文的网站怎么做微信分销网站建设官网
  • 怎么做站旅游网站上泡到妞平面设计师服务平台
  • 温室大棚建设 网站及排名转卖类似淘宝网站建设有哪些模板
  • 广西网站建设-好发信息网阿里邮箱 wordpress
  • 便捷网站建设费用搜关键词网站
  • 网站添加百度地图导航wordpress安装 centos
  • 如何自己建一个网站企业简介宣传片视频
  • 成都美誉网站设计建设优惠券网站
  • 整形网站源码一个网站如何做盈利
  • 机械设备东莞网站建设石家庄开发区网站建设
  • 代制作网站公司网站建设包括
  • 怎么手动安装网站程序搭建微信小程序
  • 郑州建网站371怎么把东西发布到网上卖
  • wordpress 点图片链接拼多多seo怎么优化
  • 石家庄做网站wordpress 文章摘要
  • 网站建设服务类型现状做兼职上哪个网站
  • 重庆网站seo排名用dw制作一个网站
  • 太原模板建站定制深圳网站建设及推广
  • vps 网站 需要绑定域名吗建设部网站拆除资质
  • 六安网站自然排名优化价格遵义网站建设网帮你