二分查找的局部有序情况

​ 我们已经知道二分查找是一种在有序数组中查找某一特定元素的查找算法。

​ 那如果一个数组不是整体有序,而是局部有序呢?这时我们就可以通过分治策略,我们在局部有序的区间内进行二分查找,然后合并各个局部结果组成整体结果。

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

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

输入一个元素互不相同的数组,输出一个整型值表示数组的最小值。

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

解析:

​ 一个不包含重复元素的升序数组在经过旋转之后,我们可以直接将其划分为两个部分,例如数组[4,5,6,7,0,1,2]可以划分为[4,5,6,7][0,1,2]两个部分。可以看出数组的最小值就是旋转数组两个局部有序区间的分界点。

​ 那我们怎么找到这个分界点呢?因为数组在旋转之前是整体有序的,所以旋转之后nums[0]是进行旋转的中心,那么旋转之后数组局部有序的两个部分中一部分中的元素全都大于等于nums[0],另一部分中的元素全都小于nums[0]

利用这一特性,我们使用二分查找的方法找到旋转数组这两部分的分界点:

  • 从中间开始找,如果当前查找的值大于等于nums[0],那么分界点在 mid 的右侧 left = mid+1
  • 如果当前查找的值小于nums[0],那么分界点在 mid 的左侧 right = mid
  • 循环上述步骤最终找到分界点 left == right

​ 当然我们要考虑到一个特殊情况:长度问 n 的数组旋转了 n 恢复到原来的状态。这种情况下我们不能用上述寻找分界点的方式去找最小值,因为该情况不能将旋转数组划分为大于等于nums[0]局部有序区间 + 全都小于nums[0]局部有序区间的两部分组合。但是这种情况的最小值也很好找就是nums[0],且很容易判断这种情况就是数组首元素小于尾元素,说明该数组整体有序。

class Solution {public:int findMin(vector<int>& nums) {int left = 0, right = nums.size();// nums[left] < nums[right-1] 判断数组是否是整体有序// nums[left] == nums[right-1] 判断数组是否只有一个元素if(nums[left]<=nums[right-1]){return nums[left];}// 二分查找寻找分界即最小值while(left<right){int mid = left + (right-left)/2;if(nums[mid] < nums[0]){right = mid;}else{left = mid+1;}}return nums[left];}
};

154 寻找旋转排序数组中的最小值 II

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

输入一个可能存在重复元素的数组,输出一个整型值表示数组的最小值。

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

解析:

​ 本题和153 寻找旋转排序数组中的最小值解决方法一样,唯一的区别是我们需要考虑元素重复的情况。

​ 旋转情况我们还是通过寻找分界点来找数组中的最小值,还是根据旋转数组的特性以nums[0]作为参考值将数组划分为大于等于nums[0]和小于nums[0]的两部分

​ 但是如果存在重复的元素,这种二分类的划分将无法实现。例如[2,3,0,1,2,2,2,2,2],我们将其划分为[2,3][0,1,2,2,2,2,2],显然[0,1,2,2,2,2,2]并不满足全都小于nums[0]==2的条件。这种情况下我们无法正确地使用二分查找寻找分界点,例如上例中第一次循环中 nums[mid] == nums[4] == 2,这将导致向其右侧寻找分界点,显然那无法找到。

​ 那么我们怎么让存在重复元素的旋转数组恢复可二分类的特性呢?

​ 可以观察到只有当以重复元素为旋转中心时才会破坏旋转数组的可二分类特性。恢复方法也很简单,只需要删除小于nums[0]局部区间中的等于nums[0]的元素即可,即将旋转数组尾部与nums[0]相等的元素删除即可。这种操作并不会影响对数组整体最小值的判断,也恢复了旋转数组的可二分类特性。

class Solution {public:int findMin(vector<int>& nums) {// 删除旋转数组尾部与nums[0]相等的元素,tail>0 如果数组元素全都一样要保留nums[0]int tail = nums.size()-1;while(tail>0){if(nums[tail]==nums[0]){--tail;}else{break;}}// nums[left] < nums[right-1] 判断数组是否是整体有序// nums[left] == nums[right-1] 判断数组是否只有一个元素  int left = 0, right = tail+1;if(nums[left]<=nums[right-1]){return nums[left];}// 二分查找寻找分界即最小值while(left<right){int mid = left + (right-left)/2;if(nums[mid] < nums[0]){right = mid;}else{left = mid+1;}}return nums[left];}
};

33 搜索旋转排序数组

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

输入是一个数组和一个值,输出是一个整型值,表示目标值在旋转数组中的位置,若不存在则返回 -1。

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

解析:

​ 我们观察这个所谓的旋转数组可以看到数组[4,5,6,7,0,1,2]可以划分为有序的两个部分,分别为[4,5,6,7][0,1,2]。所以我们可以在这两个局部有序的部分中使用二分查找搜索目标值。

​ 基于上述思路我们需要完成两个任务:

  • 找到划分局部有序的分界点
  • 使用二分查找在局部有序的数组中搜索目标值

​ 第一个任务我们可以利用旋转数组的特性快速找到分界点。旋转数组中两个局部有序的部分可以用一个简单的性质划分:因为数组在旋转之前是整体有序的,所以旋转之后nums[0]是进行旋转的中心,那么旋转之后数组局部有序的两个部分中一部分中的元素全都大于等于nums[0],另一部分中的元素全都小于nums[0]

​ 利用这一特性,我们使用二分查找的方法找到旋转数组这两部分的分界点:

  • 从中间开始找,如果当前查找的值大于等于nums[0],那么分界点在 mid 的右侧 left = mid+1
  • 如果当前查找的值小于nums[0],那么分界点在 mid 的左侧 right = mid
  • 循环上述步骤最终找到分界点 left == right

​ 第二个任务我们仍然可以基于旋转数组的特性,直接将目标值 target 与 nums[0] 比较选择对应的局部有序区间中进行二分查找。

​ 当然也可以使用分治策略,不进行比较而是直接根据分界点划分的两个区间,同时在两个区间中使用二分查找搜索目标值 target,返回由多个局部结果组成的最终结果。

class Solution {public:int binarySerach(vector<int> nums, int target, int leftBound, int rightBound){int left = leftBound, right = rightBound;while(left<right){int mid = left + (right-left)/2;if(nums[mid] == target){return mid;}else if(nums[mid] > target){right = mid;}else{left = mid + 1;}}return -1;}int search(vector<int>& nums, int target) {// 使用二分查找找到分界点,将数组分为大于等于nums[0]的和小于nums[0]的两部分int left = 0, right = nums.size();while(left<right){int mid = left + (right-left)/2;if(nums[mid] < nums[0]){right = mid;}else{left = mid + 1;}}// 如果目标值小于nums[0],则在分界点右侧的局部有序区间中用二分查找搜索目标值if(nums[0] > target){return binarySerach(nums,target,left,nums.size());}else{// 如果目标值大于等于nums[0],则在分界点左侧的局部有序区间中用二分查找搜索目标值return binarySerach(nums,target,0,left);}}
};

81 搜索旋转排序数组 II

一个存在重复元素且原本增序的数组被首尾相连后按某个位置断开(如 [0,0,1,2,2,5,6] → [2,5,6,0,0,1,2],在第4位断开),我们称其为旋转数组。给定一个值,判断这个值是否存在于这个为旋转数组中。

输入是一个数组和一个值,输出是一个布尔值,表示数组中是否存在该值。

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

解析:

​ 本题和33 搜索旋转排序数组解决方法一样,唯一的区别是我们需要考虑元素重复的情况。

​ 同样,我们可以通过找分界点将旋转数组划分为两个局部有序的部分,并在局部有序的区间中使用二分查找搜索目标值。我们仍然需要完成两个任务:

  • 找到划分局部有序的分界点
  • 使用二分查找在局部有序的数组中搜索目标值

​ 寻找分界点我们还是根据旋转数组的特性以nums[0]作为参考值将数组划分为大于等于nums[0]和小于nums[0]的两部分

​ 但是如果存在重复的元素,这种二分类的划分将无法实现。例如[2,5,6,0,0,1,2],我们将其划分为[2,5,6][0,0,1,2],显然[0,0,1,2]并不满足全都小于nums[0]==2的条件。

​ 那么我们怎么让存在重复元素的旋转数组恢复可二分类的特性呢?

​ 可以观察到只有当以重复元素为旋转中心时才会破坏旋转数组的可二分类特性。恢复方法也很简单,只需要删除小于nums[0]局部区间中的等于nums[0]的元素即可,即将旋转数组尾部与nums[0]相等的元素删除即可。这种操作并不会影响对元素存在性的判断,也恢复了旋转数组的可二分类特性。

​ 解决了重复元素的问题,其他的步骤就和33 搜索旋转排序数组解决方法一致了。

class Solution {public:bool  binarySearch(vector<int> nums, int target, int leftBound, int rightBound){int left = leftBound, right = rightBound;while(left<right){int mid = left + (right-left)/2;if(nums[mid] == target){return true;}else if(nums[mid] > target){right = mid;}else{left = mid + 1;}}return false;}bool search(vector<int>& nums, int target) {// 删除旋转数组尾部与nums[0]相等的元素,tail>0 如果数组元素全都一样要保留nums[0]int tail = nums.size()-1;while(tail>0){if(nums[tail]==nums[0]){--tail;}else{break;}}// 使用二分查找找到分界点,将数组分为大于等于nums[0]的和小于nums[0]的两部分int left = 0, right = tail+1;while(left<right){int mid = left + (right-left)/2;if(nums[mid] < nums[0]){right = mid;}else{left = mid+1;}}// 如果目标值小于nums[0],则在分界点右侧的局部有序区间中用二分查找搜索目标值if(nums[0] > target){return binarySearch(nums,target,left,nums.size());}else{// 如果目标值大于等于nums[0],则在分界点左侧的局部有序区间中用二分查找搜索目标值return binarySearch(nums,target,0,left);}}
};

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

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

输入两个有序数组,输出一个浮点型值表示两个数组的中位数,要求算法的时间复杂度应该为 O(log (m+n))

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

解析:

本题没有完全理解,有待更新

class Solution {public:double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {int tot = nums1.size() + nums2.size();if (tot % 2 == 0) {int left = find(nums1, 0, nums2, 0, tot / 2);int right = find(nums1, 0, nums2, 0, tot / 2 + 1);return (left + right) / 2.0;} else {return find(nums1, 0, nums2, 0, tot / 2 + 1);}}int find(vector<int>& nums1, int i, vector<int>& nums2, int j, int k) {if (nums1.size() - i > nums2.size() - j) return find(nums2, j, nums1, i, k);if (k == 1) {if (nums1.size() == i) return nums2[j];else return min(nums1[i], nums2[j]);}if (nums1.size() == i) return nums2[j + k - 1];int si = min((int)nums1.size(), i + k / 2), sj = j + k - k / 2;if (nums1[si - 1] > nums2[sj - 1])return find(nums1, i, nums2, sj, k - (sj - j));elsereturn find(nums1, si, nums2, j, k - (si - i));}
};

参考资料

LeetCode 101:和你一起轻松刷题(C++) 第 4 章 居合斩!二分查找

一文带你刷遍二分查找(视频+绘图)

LeetCode刷题笔记 二分查找 局部有序相关推荐

  1. leetcode刷题笔记——二分查找

    leetcode刷题笔记--二分查找 目前完成的贪心相关的leetcode算法题序号: 中等:80,81 困难:4 来源:力扣(LeetCode) 链接:https://leetcode-cn.com ...

  2. 卷进大厂系列之LeetCode刷题笔记:二分查找(简单)

    LeetCode刷题笔记:二分查找(简单) 学算法,刷力扣,加油卷,进大厂! 题目描述 涉及算法 题目解答 学算法,刷力扣,加油卷,进大厂! 题目描述 力扣题目链接 给定一个 n 个元素有序的(升序) ...

  3. LeetCode刷题笔记汇总

    LeetCode刷题笔记汇总 第一次刷LeetCode写的一些笔记. 1.两数之和 3.无重复字符的最长子串 15.三数之和 18.四数之和 19.删除链表的倒数第 N 个结点 20.有效的括号 21 ...

  4. LeetCode刷题笔记2——数组2

    LeetCode刷题笔记2--数组2 重塑数组 题目 在 MATLAB 中,有一个非常有用的函数 reshape ,它可以将一个 m x n 矩阵重塑为另一个大小不同(r x c)的新矩阵,但保留其原 ...

  5. 小何同学的leetcode刷题笔记 基础篇(01)整数反转

    小何同学的leetcode刷题笔记 基础篇(01)整数反转[07] *** [01]数学取余法*** 对数字进行数位操作时,常见的方法便是用取余的方法提取出各位数字,再进行操作 操作(1):对10取余 ...

  6. 【leetcode刷题笔记】动态规划

    #[leetcode刷题笔记]动态规划 石子游戏 public boolean stoneGame(int[] piles) {int N = piles.length;// dp[i][j] is ...

  7. LeetCode刷题笔记-动态规划-day4

    文章目录 LeetCode刷题笔记-动态规划-day4 55. 跳跃游戏 1.题目 2.解题思路 3.代码 45. 跳跃游戏 II 1.题目 2.解题思路 3.代码 LeetCode刷题笔记-动态规划 ...

  8. LeetCode刷题笔记- 15.三数之和

    LeetCode刷题笔记- 15.三数之和 C语言 题目 注意点 C语言 /*** Return an array of arrays of size *returnSize.* The sizes ...

  9. LeetCode刷题笔记第6题:Z字形变换

    LeetCode刷题笔记第6题:Z字形变换 想法: 要完成字符串根据给定的行数从上往下,从左到右完成Z字形排列.当只有一行时直接返回原字符串,当行数大于1时,先以行数构建一个行数数值个空字符串的列表, ...

最新文章

  1. ABP源码分析三十四:ABP.Web.Mvc
  2. SAP Fiori应用Footerbar区域按钮的高亮显示逻辑
  3. bash 别名_Linux的10个方便的Bash别名
  4. centos 如何测udp端口是否开放_centos测试udp端口是否打开
  5. python歌词统计单词词频_python词频统计
  6. 【函数】- 如何在C++中求平方根?
  7. java 字符串排列组合_字符串排列组合问题
  8. ffmpeg新手成长之路——使用av_seek_frame做seek定位
  9. TRY HACK ME | INTERNAL「渗透测试挑战02」
  10. 编写一个截取字符串的函数
  11. maven 创建java项目_手把手教你创建Java Maven依赖项目
  12. Dubbo系列之框架概括(一)
  13. java自动化测试语言高级之Applet 基础
  14. J-Link V9 复活
  15. 游戏引擎开发技术栈总结(自用)
  16. java练习题——手动输入成绩, 每次加分百分之二十。
  17. 小程序video隐藏控件
  18. Android-通过SlidingPaneLayout高仿微信6.2最新版手势滑动返回(一)
  19. c语言中的指数位,c语言的指数符号
  20. Java多线程分批处理数据

热门文章

  1. 工厂生产线设备数据采集方法
  2. 记录制作音响的青葱岁月
  3. c语言对于随机数的产生于应用,c语言之产生随机数详解
  4. rsync 同步本地文件到远端
  5. SEO写作,小白如何快速写一篇高质量SEO文章
  6. Raw Socket和Socket编程
  7. ArcGIS 地图切图系列之(三)MapReduce 实现
  8. 事物的特点:(一句话让你知道什么是事物)?
  9. MySQL——建表建库基本命令
  10. UE4中的GameMode、GameState、GameInstance