【二分查找延伸--实际算法应用】数组类题目
声明:博主是基于
labuladong
微信公众号文章模板驱动刷题,进行的自我刷题感悟和记录在此。
模板详情见labuladong微信公众号文章文末
;原创于自己在此基础上的笔记、感悟、整合其它文献和自己的code。
二分查找到底能运用在哪里?
【基础】使用
基本/必备应用
一、求开方——二分查找求解二元一次方程问题
题目描述: x的平方根——使用二分查找算法
- 实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4 输出: 2 示例 2:
输入: 8 输出: 2 说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
分析:我们可以把这道题想象成,给定一个非负整数 a,求 f(x) = x2− a = 0 的解。因为我们只考
虑 x ≥ 0,所以 f(x) 在定义域上是单调递增的。考虑到 f(0) = −a ≤ 0,f(a) = a2− a ≥ 0,我们
可以对 [0,a] 区间使用二分法找到 f(x) = 0 的解。
注意,在以下的代码里,为了防止除以 0,我们把 a = 0 的情况单独考虑,然后对区间 [1,a]
进行二分查找。我们使用了左闭右闭的写法。
#include <cmath>
#include <iostream>
#include <map>
#include <unordered_map>
#include <vector>
#include <windows.h>using namespace std;class Solution
{public:// 最简单可以完成int mySqrt1(int x){return sqrt(x);//二分查找 中间值}// 通过二分查找,找到方大于x 直至刚好小于x的下确界int mySqrt2(int x){// if (x == 0)// return x;// int l = 1, r = x, mid, sqrt;// while (l <= r)// {// mid = l + (r - l) / 2;// sqrt = x / mid;// if (sqrt == mid)// {// return mid;// }// else if (mid > sqrt)// {// r = mid - 1;// }// else// {// l = mid + 1;// }// }// return r;if (x == 0) return 0;int left = 1; //gaiint right = x; // 左闭右闭// 查找所以等于while (left <= right){// int mid = (left + right) / 2; // 会溢出!所以最好用下面一行的!int mid = left + (right - left) / 2;int sqr2 = x/mid; // 重点if ( sqr2 == mid){return mid;}else if (sqr2 < mid) //【重点】 类似于nums[mid] > nums[target]{right = mid - 1;}else if (sqr2 > mid){left = mid + 1;}}return right; // 返回一个 刚好的下确界 【重点!】}
};int main()
{string s = "abcabcbb";// string s = "bbbb";// // string s = "pwwkew";// // string s = "";// // string s = " ";// Solution1 so;// int num = so.lengthOfLongestSubstring(s);Solution so;int x = 2147483647;cout << "x" << x << "的向下根号为:" << so.mySqrt2(x) << endl;system("pause");return 0;
}
注意: 上述过程 即为求解方程问题,且 注意最后mySqrt2()
返回使用 right
这也是由停止条件决定,此时right
处于下确界!
另外,这道题还有一种更快的算法——牛顿迭代法,其公式为 xn+1= xn− f(xn)/f′(xn)。给
定 f(x) = x2− a = 0,这里的迭代公式为 xn+1= (xn+ a/xn)/2,其代码如下。
// 牛顿迭代法求解方程.int mySqrt3(int a){long x = a;while (x * x > a){x = (x + a / x) / 2;}return x;}
注意 这里为了防止 int 超上界,我们使用 long 来存储乘法结果。
二、查找区间
Find First and Last Position of Element in Sorted Array (Medium)
在排序数组中查找元素的第一个和最后一个位置
- 给定一个按照升序排列的整数数组 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]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109 nums 是一个非递减数组
-109 <= target <= 109
这个就是直接左右边界的二分搜索。 没啥好说的,前文看懂了就行。
三、搜索排序好的旋转数组
搜索旋转排序数组II * 假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,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 进阶:
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。 这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
雾曹!最后这句话!!!—— 绝对增加复杂度了!见下方解题思路!!!
解题思路
1、即使数组被旋转过,我们仍然可以利用这个数组的递增性,使用二分查找。对于当前的中点,
如果它指向的值小于等于右端,那么说明右区间是排好序的;反之(直接else),那么说明左区间是排好序的。
2、如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找(修改搜寻l&r);反之(else),我们对于另一半区间继续二分查找。
3、【注意】,因为数组存在重复数字,如果中点和右端/(或左端)的数字相同,我们并不能确定是左区间全部
相同,还是右区间完全相同。在这种情况下,我们可以简单地将右端左移一位/(或左端点右移一位),然后继续进行二分查找。
4、其次:①code过程中,出现的边界问题提取出来的判断;②以及等于号的细节运用 & else的算法边界限制【要好好的思考或 用一些特殊的实例进行排除!】
完整代码
// 思路:本题虽然有循环,但是内部也还是有递增的序列;
// 仍然可以找到一边的 递增序列;进行log(n)的二分查找。
class Solution {public:bool search(vector<int>& nums, int target) {int n = nums.size();if(n==0) return false;// int split;int left=0, right = n;//外部寻找 有序数组while(left < right){int mid = left + (right-left)/2;if(nums[mid] == target) return true; // 错了拿出来// 少边界!!! 相等时候,if( nums[mid] == nums[right-1] ) right -= 1;// 内部在有序内 真正的二分查找else if( nums[mid] < nums[right-1] ){// 说明右边是排好序的// if(target < nums[mid]){// right = mid;// }else{// right-=1;// }if(target >= nums[mid] && target <= nums[right-1]){// 做靠谱的事情, 剩下未知的交给 else// right = mid;left = mid + 1;}else{// 不在右边// right-=1;right = mid;}//--- 下面的太慢了// // auto a = this->binarySearch(nums.begin(), target);// int low = mid+1, high = right;// while( low < high){// int midRight = low+ (high-low)/2;// if(nums[midRight] == target){// return true;// }else if(nums[midRight] < target){// low = midRight+1;// }else high = midRight;// }// // 跳出来就是未找到.// right = mid;}else{ // (nums[mid] > nums[left]) 错误!// 左边排好序 左边二分查找if(target>=nums[left] && target <= nums[mid-1]){ // 错了 —— 23 4|512 找1就不行!// left = mid + 1;right = mid;}else{left= mid + 1;}//-----下面的太慢了!// int low = left, high = mid;// while( low < high){// int midLeft = low+ (high-low)/2;// if(nums[midLeft] == target){// return true;// }else if(nums[midLeft] < target){// low = midLeft+1;// }else high = midLeft;// }// // 说明左边未找到// left = mid + 1;}}// 循环结束,二分查找 还是没能找到,返回falsereturn false;}// int binarySearch(vector<int> &nums, int target){// int n = nums.size();// if (n == 0) return -1;// int left = 0;// int right = n ;// // 查找所以等于// while (left < right)// {// int mid = left + (right - left)/2;// if (nums[mid] == target)// {// return mid;// }// else if (nums[mid] > target)// {// right = mid;// }// else if (nums[mid] < target)// {// left = mid + 1;// }// }// return -1; // 表示未查找到。// }
};int main(){// vector<int> arr = {2,5,6,0,0,1,2};// vector<int> arr = {1,1,1,3,1};// vector<int> arr = {1};// vector<int> arr = {1,1,3};// vector<int> arr = {3,5,1};vector<int> arr = {2,2,2,0,1};int target = 0;// int target = 1;Solution so;bool answer = so.search(arr,target);cout<<"answer is : "<< answer << endl;system("pause");return 0;
}
【提升】实际应用
最常见的就是教科书上的例子,在有序数组中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。
PS:以上提到的三种二分查找算法形式在前文 二分查找算法详解 有代码详解,如果没看过强烈建议看看。
以及一些基础必备的应用问题求解见前文。
抛开有序数组这个枯燥的数据结构,二分查找如何运用到实际的算法问题中呢?当搜索空间有序的时候,就可以通过二分搜索「剪枝」,大幅提升效率。
说起来玄乎得很,本文用「Koko 吃香蕉」和「货物运输」的问题来举个例子。
一、Koko 吃香蕉
也就是说,Koko 每小时最多吃一堆香蕉,如果吃不下的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。在这个条件下,让我们确定 Koko 吃香蕉的最小速度(根/小时)。
如果直接给你这个情景,你能想到哪里能用到二分查找算法吗?如果没有见过类似的问题,恐怕是很难把这个问题和二分查找联系起来的。
那么我们先抛开二分查找技巧,想想如何暴力解决这个问题呢?
首先,算法要求的是「H小时内吃完香蕉的最小速度」,我们不妨称为speed,请问speed最大可能为多少,最少可能为多少呢?
显然最少为 1,最大为max(piles),因为一小时最多只能吃一堆香蕉。那么暴力解法就很简单了,只要从 1 开始穷举到max(piles),一旦发现发现某个值可以在H小时内吃完所有香蕉,这个值就是最小速度:
int minEatingSpeed(int[] piles, int H) {// piles 数组的最大值int max = getMax(piles);for (int speed = 1; speed < max; speed++) {// 以 speed 是否能在 H 小时内吃完香蕉if (canFinish(piles, speed, H))return speed;}return max;
}
注意这个 for 循环,就是在连续的空间线性搜索,这就是二分查找可以发挥作用的标志。
由于我们要求的是最小速度,所以可以用一个搜索左侧边界的二分查找来代替线性搜索,提升效率:
int minEatingSpeed(int[] piles, int H) {// 套用搜索左侧边界的算法框架int left = 1, right = getMax(piles) + 1;while (left < right) {// 防止溢出int mid = left + (right - left) / 2;if (canFinish(piles, mid, H)) {right = mid;} else {left = mid + 1;}}return left;
}
PS:如果对于这个二分查找算法的细节问题有疑问,建议看下前文 二分查找算法详解 搜索左侧边界的算法模板,这里不展开了。
剩下的辅助函数也很简单,可以一步步拆解实现:
// 时间复杂度 O(N)
boolean canFinish(int[] piles, int speed, int H) {int time = 0;for (int n : piles) {time += timeOf(n, speed);}return time <= H;
}int timeOf(int n, int speed) {return (n / speed) + ((n % speed > 0) ? 1 : 0);
}int getMax(int[] piles) {int max = 0;for (int n : piles)max = Math.max(n, max);return max;
}
至此,借助二分查找技巧,算法的时间复杂度为 O(NlogN)。
我的做法类似:分析:1、首先暴力搜索方法思考 2、其次,用二分搜索,替代暴力搜索 3、直接二分搜索可以的堆树的 左侧界,吃的天数和H相比的左侧界
完整代码:
#include <iostream>
#include <windows.h>
#include <algorithm>
#include <vector>
#include <string>
#include <cmath>using namespace std;class Solution {public:int minEatingSpeed(vector<int>& piles, int H) {int n = findMaxNum(piles);// cout << "n:" << n << endl;if(n==0) return 1;int left=1, right = n; //至多至少 吃的根数while(left <= right){ //采用左闭右闭int mid = left + (right - left) /2;int days = canEatInV(piles, mid);// cout << "mid v:" << mid << " ;eat days:" << days << endl;if(days <= H){right = mid - 1;}else{left = mid + 1;}}if( left >= n+1 || canEatInV(piles, left) > H) return -1;return left;}int canEatInV(vector<int> piles, int v){int days=0;for(auto p:piles){days+=ceil((float)p/v);// cout << "caneat:" << days ;}// cout << "Caneat End" << endl;return days;}int findMaxNum(vector<int> piles){// vector<int>::iterator p = piles.begin();int max = 0;for(auto p : piles){if(p>max){max = p;}}return max;}
};int main(){// vector<int> arr = {3,6,7,11};vector<int> arr = {30,11,23,4,20};// int H = 8;int H = 5;// int H = 6;Solution so;int ans = so.minEatingSpeed(arr, H);cout << "koko最少的吃香蕉速度为:" << ans << endl;system("pause");return 0;
}
【重点:】学会使用:days+=ceil((float)p/v);
二、包裹运输问题
类似的,再看一道运输问题:
要在D天内运输完所有货物,货物不可分割,如何确定运输的最小载重呢(下文称为cap)?
其实本质上和 Koko 吃香蕉的问题一样的,首先确定cap的最小值和最大值分别为max(weights)和sum(weights)。
类似刚才的问题,我们要求最小载重,可以用 for 循环从小到大遍历,那么就可以用搜索左侧边界的二分查找算法优化线性搜索:
// 寻找左侧边界的二分查找
int shipWithinDays(int[] weights, int D) {// 载重可能的最小值int left = getMax(weights);// 载重可能的最大值 + 1int right = getSum(weights) + 1;while (left < right) {int mid = left + (right - left) / 2;if (canFinish(weights, D, mid)) {right = mid;} else {left = mid + 1;}}return left;
}// 如果载重为 cap,是否能在 D 天内运完货物?
boolean canFinish(int[] w, int D, int cap) {int i = 0;for (int day = 0; day < D; day++) {int maxCap = cap;while ((maxCap -= w[i]) >= 0) {i++;if (i == w.length)return true;}}return false;
}
通过这两个例子,你是否明白了二分查找在实际问题中的应用呢?
首先思考使用 for 循环暴力解决问题,观察代码是否如下形式:
for (int i = 0; i < n; i++)if (isOK(i))return answer;
如果是,那么就可以使用二分搜索优化搜索空间:如果要求最小值就是搜索左侧边界的二分,如果要求最大值就用搜索右侧边界的二分。
和上个一样,知道了也没啥了。重点学到了 返回值有多个时的处理方法。
我的完整代码:
#include <iostream>
#include <windows.h>
#include <algorithm>
#include <vector>
#include <string>
#include <cmath>using namespace std;class Solution {public:/*** @description: 1、首先寻找所有货物重量 2、查找当前mid载重的天数 3、二分查找,找最低载重——即左侧界* @param {*}* @return {*}* @notes: 注意:最低包裹限制要 大于等于最小的包裹。*/int shipWithinDays(vector<int>& weights, int D) {auto pair = findMaxMinBoatWeight(weights);int n = pair.first, max = pair.second;if(n==0) return 1;int left = max,right = n;// cout << "max:" << max << ",n:" << n << endl;while(left<=right){int mid = left + (right - left)/2;int days = canDaysInboat(weights, mid);if(days <= D){right = mid - 1;}else{left = mid + 1;}}if(left >= n+1 || canDaysInboat(weights,left)>D) return -1;return left;}int canDaysInboat(vector<int> weights, int boat){int sum = 0, days=1;for(auto w : weights){sum += w;if(sum>boat){days+=1;sum = w;}}return days;}pair<int,int> findMaxMinBoatWeight(vector<int> weights){int sum = 0,max = 0;for(auto w : weights){sum+=w;max = w > max ? w : max ;}pair<int,int> res = {sum, max};return res;}
};int main(){vector<int> arr = {1,2,3,4,5,6,7,8,9,10};// vector<int> arr = {3,2,2,4,1,4};// vector<int> arr = {1,2,3,1,1};int D = 5;// int D = 3; Solution so;int ans = so.shipWithinDays(arr, D);cout << "船只的最低载重量为:" << ans << endl;system("pause");return 0;
}
参考
- labuladong的算法小抄.
- 具体篇章——二分搜索.
【二分查找延伸--实际算法应用】数组类题目相关推荐
- [算法]LeetCode 专题 -- 二分查找专题 34. 在排序数组中查找元素的第一个和最后一个位置
LeetCode 专题 – 二分查找专题 34. 在排序数组中查找元素的第一个和最后一个位置 难度:中等 题目描述 给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值 ...
- python 二分查找_LeetCode基础算法题第120篇:二分查找算法
技术提高是一个循序渐进的过程,所以我讲的leetcode算法题从最简单的level开始写的,然后> 到中级难度,最后到hard难度全部完.目前我选择C语言,Python和Java作为实现语言,因 ...
- 算法总结——堆栈、字符串、数组类题目
先说stack的题目 stack的实现:链表,数组 题目: (1)简单的:min stack,一个数组实现三个stack (2)经典的stack问题:经典汉诺塔问题,逆波兰式计算或者产生逆波兰式,简化 ...
- java二分查找法_java算法之二分查找法的实例详解
java算法之二分查找法的实例详解 原理 假定查找范围为一个有序数组(如升序排列),要从中查找某一元素,如果该元素在此数组中,则返回其索引,否则返回-1.通过数组长度可取出中间位置元素的索引,将其值与 ...
- 1.1.10 从二分查找BinarySearch开启算法学习之路---《java算法第四版》
文章目录 0.前言 1.功能 2.示例 有两个名单tinyW.txt和tinyT.txt,将tinyT.txt名单中不在tinyW.txt的数据打印出来 ① 实现原理 ② 实现代码 ③ 性能分析 0. ...
- codeforces 贪心+优先队列_算法基础04-深度优先搜索、广度优先搜索、二分查找、贪心算法...
深度优先搜索DFS.广度优先搜索BFS 比较 拿谚语打比方的话,深度优先搜索可以比作打破砂锅问到底.不撞南墙不回头:广度优先搜索则对应广撒网,多敛鱼 两者没有绝对的优劣之分,只是适用场景不同 当解决方 ...
- 深度搜索和广度搜索领接表实现_算法基础04-深度优先搜索、广度优先搜索、二分查找、贪心算法...
深度优先搜索DFS.广度优先搜索BFS 比较 拿谚语打比方的话,深度优先搜索可以比作打破砂锅问到底.不撞南墙不回头:广度优先搜索则对应广撒网,多敛鱼 两者没有绝对的优劣之分,只是适用场景不同 当解决方 ...
- 七十六、Python | Leetcode二分查找和分治算法系列
@Author:Runsen @Date:2020/7/4 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...
- C语言二分查找法(指针和数组实现)
/** 编写一个函数,对一个已排序的整数表执行二分查找.* 函数的输入包括各异指向表头的指针,表中的元素个数,以及待查找的数值.* 函数的输出时一个指向满足查找要求的元素的指针,当未查找到要求的数值时 ...
最新文章
- CIR:2020年全球数据中心应用AOC市场达$42亿
- 微信小程序开发中如何实现侧边栏的滑动效果?
- 谷歌研究院最新发现:训练结果不准确,超大数据规模要背锅!
- 提高CSDN阅读量。需要写出好文章。新技术的笔记!
- 【BZOJ】3301: [USACO2011 Feb] Cow Line(康托展开)
- 内存错误 处理 [CAlayer release]
- 89.算数移位 -逻辑移位-循环移位
- kubernetes常用对象
- php 猴子选大王,php猴子选大王
- 千亿级的数据难题,优酷工程师怎么解决?
- 数据结构与算法系列——排序(3)_折半插入排序
- 请不要把数据分析和机器学习混为一谈
- DirectX Audio和DirectShow入门
- Unity内置Shader解读1——Bumped Diffuse
- 【VMware】下安装OSX10.10-Yosemite【Mac】系统
- 常见的中间件有哪些?
- 狂神Javascript笔记
- Linux系统下 修改服务器用户密码
- python代码一行过长怎么办_Python – 我写代码时如果有一行过长该怎么处理?
- 数据库 高性能服务器,高性能数据库服务器配置
热门文章
- 微信小程序http连接访问解决方案
- java如何调取别人接口_调别人的接口会,现在别人调我的接口。不知道怎么操作了...
- 分布式技术一周技术动态 2016-09-18
- [2008-NBA-视频直播]NBA常规赛
- 随机梯度下降,mini-batch梯度下降,在线学习机制,mapreduce
- *--p *p-- --*p
- 解决“您可以尝试添加 --skip-broken 选项来解决改问题“错误
- CSS3旋转实例学习
- AC-baidu-重定向脚本造成百度首页皮肤失效的解决方法
- 聊一聊ChatGPT