《算法笔记》总结No.7——二分(多例题详解版)

news2024/9/23 15:32:36

一.二分查找

        目前有一个有序数列,举个例子,假设是1~1000,让我们去查找931这个数字,浅显且暴力的做法就是直接从头到尾遍历一遍,直到找到931为止。当n非常大,比如达到100w时,这是一个非常大的量级,考虑到效率的优劣这是不能接收的。

        二分查找是基于有序序列的一种查找算法,所谓的有序是序列严格递增:每次根据当前查找区间的中位数来判断是否与目标相同,如果不同就根据当前大小以上个区间的中点作为区间的某一端,继续执行这个二分查找过程~

        高效的点在于,二分的每一步都可以去除区间中一半的元素,其时间复杂度是O(logn),学过数学的各位理科生都知道,对数的增加速度是非常慢的,甚至小于一次的幂函数,当N非常大的时候,越能体会到logN复杂度的含金量!

话不多说直接上代码,依旧是博主自研的vector函数版:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;


void BinarySearch(vector<int> V,int x){
	int left=0;
	int right=V.size()-1;      //数组的末位 
	int i=(left+right)/2;     //初始中点 
	int flag=0;// flag为0意味着未查询到 
	
	while(flag==0&&left<=right)
	{
		cout<<"当前查询的元素下标为:"<<i<<endl;
		if(V[i]==x)
		{
			cout<<i<<"位即为x的值~";
			flag=1;
		}
		else if(x>V[i])  //目标大于当前中点,中点的右一位+1作为新区间的左端点 
		{
			left=i+1;
			i=(left+right)/2;
		}
		else if(x<V[i])//目标小于当前中点,中点的左一位-1作为新区间的左端点
		{
			right=i-1;
			i=(left+right)/2;
		}					 
	}
	if(flag==0)
		cout<<"很遗憾,未找到~"<<endl;
} 


int main() {
	int n=0;
	vector<int> V;
	cout<<"请输入数列规模:"<<endl; 
	cin>>n;
	for(int i=1;i<=n;i++) //读入数据 
	{
		int temp=0;
		cin>>temp;
		V.push_back(temp); 
	}
	sort(V.begin(),V.end());//排序一下,保证V本身有序!
	//其实这种题目直接就是有序~
	int x=0;
	cout<<"请输入待查询的值:"<<endl;
	cin>>x;
	BinarySearch(V,x);
	

	return 0;
}

以书上的测试用例作为验证组:

10

3 7 8 11 15 21 33 52 66 88 

11

首先给大家图解画一下过程,其实不难想象:

测试结果如下,没什么bug:


        tips:如果各位是考试或者写什么数构的伪码,其实用普通的数组就行,博主在实现的时候偏爱使用vector——这种随时可以扩展的数组(向量)属实给人极大的安全感,各位在阅读的时候不要见怪~ 


升级问题,假设序列中有多个相同的元素,请用二分法找到这个区间?(格式为左开右闭~)

其实和之前差不多,区别在于,我们要找到该元素第一次和最后一次出现的位置,因此之前的大小比较应该做出改善,如下:

  • 找左端点时,还是用mid来与目标值对比,如果mid值大于等于目标值,则说明目标值第一次出现的位置有可能还要往左,因此还要往左查询,因此要令right=mid(向左缩小范围~);如果mid比目标值小,则说明目标值第一次出现的位置还要再往右,应该令left=mid+1
  • 找右端点时,如果mid>x,说明最后的目标数x在mid处或者mid的左侧,因此应该在left~mid里面继续找,即right=mid;如果mid小于等于x,说明最后一个x一定在mid的右侧,因此令left=mid+1
  • 其实两者改变区间的方式一致,区别就在于改变的条件~

先是找左端点的函数:

int FindLeft(vector<int> V,int x){
	int left=0;
	int right=V.size()-1;      //数组的末位 
	int i=(left+right)/2;     //初始中点  
	
	while(left<right)
	{
		//cout<<"当前查询的元素下标为:"<<i<<endl;
		if(V[i]>=x)   //如果当前值大于等于目标值,说明最小的有可能是mid或者mid的左边
		{
			right=i;
			i=(left+right)/2;
		}
		else if(V[i]<x)
		{
			left=i+1;//如果当前值小于目标值,说明最小的还要再往右
			i=(left+right)/2;	
		}
	}
	return left; 
}

然后是找右端点的函数:

int FindRight(vector<int> V,int x){
 	int left=0;
	int right=V.size()-1;      //数组的末位 
	int i=(left+right)/2;     //初始中点 
	
	while(left<right)
	{
		//cout<<"当前查询的元素下标为:"<<i<<endl;
		if(V[i]>x)   //如果当前值大于等于目标值,说明最大的有可能是mid或者mid的左边
		{
			right=i;
			i=(left+right)/2;
		}
		else if(V[i]<=x)
		{
			left=i+1;//如果当前值小于目标值,说明最大的可能还要再往右
			i=(left+right)/2;	
		}
	}
	return right; 
}

最后main函数调用:

int main() {
	int n=0;
	vector<int> V;
	cout<<"请输入数列规模:"<<endl; 
	cin>>n;
	for(int i=1;i<=n;i++) //读入数据 
	{
		int temp=0;
		cin>>temp;
		V.push_back(temp); 
	}
	sort(V.begin(),V.end());//排序一下,保证V本身有序!
	//其实这种题目直接就是有序~
	int x=0;
	cout<<"请输入待查询的值:"<<endl;
	cin>>x;
	cout<<x<<"的存在区间是:"<<endl;
	cout<<"["<<FindLeft(V,x)<<","<<FindRight(V,x)<<")"<<endl;
	
	return 0;
}

两组测试用例:

  • 【1,2,2,2,3,3,3,3,3,4】——3
  • 【0,0,0,1,2,2,2,2,3,3】——2

没什么bug~

tips:其实写成双闭区间也可以,一个道理~修改一下if条件即可


一些注意事项:

  • 在程序设计时,二分更多用的是非递归的设计方法
  • 当上界超越int范围的一半时,计算中点的left+right这一算式可能会导致溢出,这里修改的策略是mid=left+(right-left)/2
  • 上面找左右端点的时候,需要将条件改为left<mid,不然会造成死循环

进一步思考上一步中寻找区间端点的过程:本质上,所有需要用到二分法的题目都是在解决:寻找有序序列中第一个满足某条件元素的位置

        如上,核心点就在于修改判断的条件,这个条件,在序列中一定是从左到右先不满足,然后满足的过程

二.应用

1.方程根的近似值

先来看一个简单的例子:求出根号n的近似值,也就是sqrt(n)。

与其用mid和sqrt(n)比,不如直接用mid平方和n比,再对mid开方,不难发现,这也是一个二分查找~

直接上代码:

#include <iostream>
#include <cmath> 
using namespace std;


void Calsqrt(int x)
{
	double n1=sqrt(x);
	cout<<x<<"的真实开根号值为:"<<n1<<endl;
	double left=trunc(n1),right=trunc(n1)+1;
	cout<<x<<"的取值范围是:["<<left<<","<<right<<"]"<<endl;
	double i=0; 
	while(right-left>0.0001)  //设置精度 
	{
		i=(left+right)/2;//初始中点 
		if((i*i)>x)  //目标大于待查找值,所以将上一个值作为右端点,查询区间左移 
			right=i;	
		else //目标小于待查找值,所以将上一个值作为左端点,查询区间右移
			left=i;
	}
	
	cout<<i<<endl;
}


int main() {
	int n=0;
	cout<<"请输入n的值:";
	cin>>n;
	
	Calsqrt(n);
	
	return 0;
}

注意点:

  • 与真正的二分查找相比,本次的二分其实是针对一个连续的函数因此所谓的left和right一定要为double型的,不然无效且死循环
  • 同样的道理,在改变区间时,直接用left或者right与mid相等就行——考过考研数学一的道友肯定知道,对于连续性的函数,端点处划分到哪个区间都不影响! 

2.装水问题

如上,其实还是一个同样的问题,关键要找到一个切入点:随着水高度h的增加,面积S是不断增加的——这符合二分要求序列递增的前提!

因此我们可以对h进行二分,并计算当前h下的面积与目标面积的差距,然后还是左右移动二分区间的套路,即可解决~

#include <iostream>
#include <cmath> 
using namespace std;

#define Pi 3.14 

void CalH(double R,double r)
{
	double sq1=R*R*Pi;//原始面积
	double sq2=sq1*r;//目标面积
	double answer=sq2/Pi;//设置一个中间值为目标h的平方倍 

	double i=0; //二分中点
	double left=0,right=R;// 二分区间 
	while(right-left>0.0001&&r<=1)  //设置精度 ,比例不能超过1 
	{
		i=(left+right)/2;//初始中点 
		if((i*i)>answer)  //目标大于待查找值,所以将上一个值作为右端点,查询区间左移 
			right=i;	
		else //目标小于待查找值,所以将上一个值作为左端点,查询区间右移
			left=i;
	}
	
	cout<<"目标h的值为:"<<i<<endl;
}


int main() {
	double R=0,r=0;
	cout<<"请输入半径的值:"<<endl;
	cin>>R;
	cout<<"请输入比例的值:"<<endl;
	cin>>r;
	CalH(R,r);
	
	return 0;
}

这里我们选一个测试用例检测一下:

没什么问题~

 

3.木棒切割问题

还是老套路:找到递增和目标值,两个条件直接选用二分!

不难发现,当每一根木棒的长度越大时,分出来的个数肯定越小,因此应该用当前单体长度来进行二分,从小到大——当对应木棒的个数不符合题意中要求的个数时,则结束二分。

直接上代码:

#include <iostream>
#include <vector>
#include <cmath> 
using namespace std;

int Cut(vector<int>V,int k) //当前长度最多在数组中切多少~ 
{
 	int count=0;
	for(int i=0;i<=V.size()-1;i++)
	{
		int temp=0;
		temp=V[i]/k;
		count+=temp;	
	}	
	return count;
} 

void CalN(vector<int> V,int k)
{
	int min=V[0];
	for(int i=1;i<=V.size()-1;i++)
		if(V[i]<min)
			min=V[i];
	//找出最小值作为二分区间的右端点 
	int left=1,right=min;//最短也要长度为1,最长也超不过单体的最短~
	int i=0; //二分中点
	while(right-left>1)  //当差距是一位时,说明已经找到了,由于向下取整的原因会造成死循环,因此此时可以直接输出中点的值 
	{
		i=(left+right)/2;//初始化中点
		if(Cut(V,i)>=k) //如果当前长度下的段数比目标的多,说明每一段的长度还可以再长,因此区间右移 	
			left=i;
		else          //如果当前的比目标的还少,应该缩短每一段的长度来增加个数,使得满足要求~ 
			right=i;		 	 
	}
	cout<<"最大长度为:"<<i<<endl; 
	
}


int main() {
	int num=0;
	vector<int> V;
	cout<<"请输入木棒的个数:"<<endl;
	cin>>num;
	cout<<"请输入每段木棒的长度:"<<endl;
	for(int i=1;i<=num;i++)
	{
		int temp=0;
		cin>>temp;
		V.push_back(temp);
	}
	int K=0;
	cout<<"请输要求长度相等的木棒个数:"<<endl;
	cin>>K;

	CalN(V,K); 
	return 0;

}

测试就用书上的测试两次:

和手算的结果一致,没什么问题:

三.快速幂

        快速幂,顾名思义,一种快速计算幂函数的方法。博主的个人理解是,这玩意既像二分的思想,又像递归的思想,因此也可以称之为二分幂~

现有数2的10000000次方,我们当然不能把2累乘10000000次——这是相当失败的程序。其实中间有很多步骤可以省略:

  • 当指数是偶数时,我们可以让指数除以2,底数乘以底数;
  • 当指数是奇数时,我们可以将指数变为偶数;

举个例子,2的10次方,使用快速幂的思想,计算过程如下:

  • 2的10次方,10是偶数,此时待算式为:4的5次
  • 5为奇数,因此待算式为:4*4的4次
  • 4为偶数,因此待算式为:4*16的2次
  • 2为偶数,因此待算式为:4*16*256的一次
  • 1为奇数,因此待算式为:4*16*256*256的0次,直接计算出来~

不难发现,上述过程只需要5步,而累乘需要10步,当指数变得非常大时,这一优势会愈加明显~

代码如下:

1.非递归形态

#include<iostream>
using namespace std;
long long fpow(long long a,long long b){//a是底数,b是指数 
	long long ans=1;//初始化答案为1
	while(b){//当指数不为0时执行
		if(b%2==0){//指数为偶数时,指数除以2,底数乘以底数
			b/=2;
			a*=a; 
		}else{//指数为奇数时,分离指数,ans乘以底数
			ans*=a; 
			b--;
		}
	} 
	return ans; 
}
int main(){
	long long n,m;
	cin>>n>>m;
	cout<<fpow(n,m)<<endl;
}

2.递归形态

#include<iostream>
using namespace std;
typedef long long LL;

LL binaryPow(LL a,LL b)
{
	if(b==0) 	//递归边界——即0次方 
		return 1;    
	if(b%2==1)   //奇偶各一次递归式 
		return a*binaryPow(a,b-1);
	else{
		LL mul =binaryPow(a,b/2);
		return mul*mul;
	}
}

int main(){
	long long n,m;
	cin>>n>>m;
	cout<<binaryPow(n,m)<<endl;
}

tips:写在最后

  • 对于二分法的使用时机,主要要看两个点:需要找到某个数值第一次或者最后一次出现的位置且在寻找这个值的过程中,满足该值单调递增(递减)
  • 二分的循环条件,基本上都是right>left,或者作差大于某个值~
  • 文中基本上全是博主根据伪码自创的实现方式,博主酷爱使用vector,可能有些朋友看起来比较吃力;此外所有的mid都写成了i,大家看的时候尽量理解~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1929869.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

获取欧洲时报中国板块前新闻数据(多线程版)

这里写目录标题 一.数据获取流程二.获取主页面数据并提取出文章url三.获取文章详情页的数据并提取整体代码展示 一.数据获取流程 我们首先通过抓包就能够找到我们所需数据的api 这里一共有五个参数其中只有第一个和第五个参数是变化的第一个参数就是第几页第五个是一个由时…

论文翻译 | Successive Prompting for Decomposing Complex Questions 分解复杂问题的连续提示

摘要 回答需要做出潜在决策的复杂问题是一项具有挑战性的任务&#xff0c;尤其是在监督有限的情况下。 最近的研究利用大型语言模型&#xff08;LMs&#xff09;的能力&#xff0c;在少量样本设置中通过展示如何在单次处理复杂问题的同时输出中间推理过程&#xff0c;来执行复杂…

【自学安全防御】二、防火墙NAT智能选路综合实验

任务要求&#xff1a; &#xff08;衔接上一个实验所以从第七点开始&#xff0c;但与上一个实验关系不大&#xff09; 7&#xff0c;办公区设备可以通过电信链路和移动链路上网(多对多的NAT&#xff0c;并且需要保留一个公网IP不能用来转换) 8&#xff0c;分公司设备可以通过总…

Jdk8 Idea Maven Received fatal alert: protocol_version

问题描述 使用idea开发工具&#xff0c;maven加载项目依赖时&#xff0c;出现错误&#xff1a; Could not transfer artfact xxxxxxx from/to maven-dep-repos https://XXXXXXX: Received fatal alert: protocol_version初步思路 用关键字protocol_version 去检索&#xff0…

Schematics,一个牛逼的python库用于数据验证和转换的库

目录 什么是Schematics&#xff1f; 为什么使用Schematics&#xff1f; 安装Schematics 定义模式 验证数据 自定义验证 转换数据 结语 什么是Schematics&#xff1f; 在Python的世界中&#xff0c;Schematics是一个用于数据验证和转换的库。它通过定义数据结构的模式(…

30秒学会UML-功能类图

目录 1、类图本体 三部分 修饰符 2、类与类直接关系 泛化关系 实现关系 简单关联关系 依赖关系 组合关系 聚合关系 1、类图本体 三部分 第一层&#xff1a;类名第二层&#xff1a;成员变量&#xff08;类的属性&#xff09;第三层&#xff1a;函数方法&#xff08;类…

PX4 运行 make px4_sitl_default gazebo 报错

报错原因&#xff1a;最开始我把依赖一直都是在base环境下安装的&#xff0c;没有conda deactivate&#xff0c;而pip install的东西应该装在系统环境&#xff0c;不能装在base环境下&#xff0c;sudo apt 是装在系统环境的 1.检查ros 用鱼香ros安装 wget http://fishros.…

SSL证书续费

讲解下域名证书如何续费&#xff08;以阿里云为例&#xff09; ‍ 提醒 一般云服务器厂商&#xff0c;都会提前和你一个月左右通知&#xff08;邮件、短信等&#xff09;&#xff0c;例如&#xff1a; 尊敬的 xxx&#xff1a;您域名 www.peterjxl.com 使用的 SSL 证书 xxxxx…

Linux编程(通信协议---udp)

UDP&#xff08;用户数据报协议&#xff09;是一种无连接的网络协议&#xff0c;主要用于快速传输数据。以下是UDP协议的一些主要特点&#xff1a; 1. **无连接**&#xff1a;UDP是无连接的协议&#xff0c;这意味着在数据传输之前不需要建立连接。每个UDP数据包都是独立的&am…

数据库操作太复杂?Python Shelve模块让你轻松存储,一键搞定!

目录 1、基本操作入门 &#x1f4da; 1.1 安装Shelve模块 1.2 创建与打开Shelve文件 2、存储与读取数据 &#x1f510; 2.1 写入键值对 2.2 读取存储的数据 3、高级功能探索 &#x1f9ed; 3.1 使用Shelve迭代键和值 3.2 键的管理&#xff1a;添加、删除与更新 4、异…

minishell

今天完成了minishell的制作 项目需求&#xff1a; 1. 获取终端用户输入的命令&#xff0c;并输出相应的执行结果。 touch cp mv ls ls -a ls -l mkdir rmdir pwd cd ln ln -s exit ---------…

鸿蒙开发HarmonyOS NEXT (四) 熟悉ArkTs (下)

一、动画 1、属性动画 animation,可以通过配置动画时间duration等参数&#xff0c;实现移动时的平滑过度 写了个小鱼游动的小案例 Entry Component struct ActionPage {State fish: Resource $r(app.media.fish_right) //小鱼图片State fishX: number 200 //初始化小鱼横坐…

Day07-员工管理-上传下载

1.员工管理-导出excel 导出员工接口返回的是二进制axios配置responseType为blob接收二进制流文件为Blob格式按装file-saver包&#xff0c;实现下载Blob文件npm install add file-saver导出员工excel的接口 (src/api/employee.js) export function exportEmployee(){return req…

【区块链 + 智慧政务】涉税行政事业性收费“e 链通”项目 | FISCO BCOS应用案例

国内很多城市目前划转至税务部门征收的非税收入项目已达 17 项&#xff0c;其征管方式为行政主管部门核定后交由税务 部门征收。涉税行政事业性收费受限于传统的管理模式&#xff0c;缴费人、业务主管部门、税务部门、财政部门四方处于 相对孤立的状态&#xff0c;信息的传递靠…

【Diffusion学习】【生成式AI】Diffusion Model 原理剖析 (2/4) (optional)【公式推导】

文章目录 影像生成模型本质上的共同目标【拟合分布】Maximum Likelihood Estimation VAE 影像生成模型本质上的共同目标【拟合分布】 Maximum Likelihood Estimation VAE

图片服务器是什么?常见的图片服务器是哪几种?图片服务器的要求是什么?

什么是图片服务器 图片服务器&#xff0c;顾名思义就是专门用于处理图片的服务器&#xff0c;向外提供图片的上传&#xff0c;下载&#xff0c;图片展示等服务 为什么我们要使用专门的服务器处理图片 图片的数据量比文字展示高得多&#xff0c;图片的上传下载展示一系列操作…

Linux进程——进程优先级与僵尸进程孤儿进程

文章目录 僵尸进程变成僵尸状态的过程 孤儿进程进程优先级如何修改进程优先级为什么优先级有范围 僵尸进程 僵尸状态进程本质上就是死亡状态 在进程死亡之后&#xff0c;不会直接对进程进行释放&#xff0c;而是先会处理一些后事 进程在结束退出的时候&#xff0c;也会有一些…

介绍 Elasticsearch 中的 Learning to Tank - 学习排名

作者&#xff1a;来自 Elastic Aurlien Foucret 从 Elasticsearch 8.13 开始&#xff0c;我们提供了原生集成到 Elasticsearch 中的学习排名 (learning to rank - LTR) 实现。LTR 使用经过训练的机器学习 (ML) 模型为你的搜索引擎构建排名功能。通常&#xff0c;该模型用作第二…

CSA笔记1-基础知识和目录管理命令

[litonglocalhost ~]$ 是终端提示符&#xff0c;类似于Windows下的cmd的命令行 litong 当前系统登录的用户名 分隔符 localhost 当前机器名称&#xff0c;本地主机 ~ 当前用户的家目录 $ 表示当前用户为普通用户若为#则表示当前用户为超级管理员 su root 切换root权限…

我利用ChatGPT开发了一个网盘资源搜索神器APP

首先声明,本文不是买东西,仅分享个人利用ChatGPT开发项目的个人经验分享。 之前已经开发完Web端网盘资源搜索引擎,而在安卓平台使用浏览器访问总是有点不方便,于是考虑开发一个安卓端APP。 可是,自己并没有开发APP经验,那怎么办? 都说AI可以帮你搞定一切,那就用一用…