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

一.二分查找

        目前有一个有序数列,举个例子,假设是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,大家看的时候尽量理解~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

经纬恒润底盘控制产品R-EPS成功量产

近日&#xff0c;经纬恒润开发的齿条式电动助力转向系统R-EPS&#xff08;Rack-Electronic Power Steering&#xff09;搭载某新能源车企中高端MPV车型&#xff0c;成功量产落地。 该产品采用恒润Double Pinion/Rack平台级的软硬件方案&#xff0c;模块复用程度更高&#xff0c…

5.4 软件工程-系统设计

系统设计 - 概述 设计软件系统总体结构 数据结构及数据库设计 编写概要设计文档、评审 详细设计的基本任务 真题

DHCP服务、FTP服务

一、DHCP 1.1 DHCP是什么 DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09;是一种网络协议&#xff0c;用于自动分配 IP 地址和其他网络配置信息给网络中的设备 1.2 DHCP的好处 自动化: 减少了手动配置 IP 地址和网络参数的工…

pc端注册页面 密码校验规则

1.密码校验规则 格应包含大小写字母、数字和特殊符号,长度为8-20 var validateRetrievePassword (rule, value, callback) > {let reg /^(?.*[A-Za-z])(?.*\d)(?.*[~!#$%^&*()_<>?:"{},.\/\\;[\]])[A-Za-z\d~!#$%^&*()_<>?:"{},.\/\\;…

Linux系统下weblogic10.3.6版本打补丁步骤

linux系统 weblogic补丁压缩包&#xff1a;p35586779_1036_Generic.zip 链接&#xff1a;https://pan.baidu.com/s/1EEz_zPX-VHp5EU5LLxfxjQ 提取码&#xff1a;XXXX &#xff08;补丁压缩包中包含以下东西&#xff09; 打补丁步骤&#xff1a; 1.备份原weblogic(需要先确保服…

【Linux杂货铺】期末总结篇3:用户账户管理命令 | 组账户管理命令

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux杂货铺、Linux实践室 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 第五章5.1 ⛳️Linux 账户5.2 ⛳️用户配置文件和目录&#xff08;未完待续&#xff09;5.2.1 …

【机器学习实战】Datawhale夏令营2:音视频攻防(deepfake)Baseline句解

# Datawhale # AI夏令营 # 夏令营 文章目录 1. 赛题简要介绍2. 赛题数据集3. 评价指标4. Baseline整体4.1 计算样本数4.2 创建video对象4.3 下载需要的库&&补充知识4.4 设置pytorch随机种子&&CUDNN配置4.5 音视频预处理4.6 创建训练数据文件夹4.7 生成梅尔频谱…

habase集群安装

解压到/opt/softs目录 tar -zxvf hbase-2.4.11-bin.tar.gz -C /opt/softs/ 改名 mv hbase-2.4.11/ hbase2.4.11 配置环境变量 修改/etc/profile vim /etc/profile 添加 #HBASE_HOME export HBASE_HOME/opt/softs/hbase2.4.11 export PATH$PATH:$HBASE_HOME/bin 修改其中的…

Linux部署禅道(无脑复制版)

目录 环境部署1、下载&#xff0c;解压2、启动3、设置开机自启 登录禅道登录数据库1、设置账号2、网页登录数据库 环境 Linux系统 Centos7 《Linux一键安装包安装禅道》视频链接&#xff1a; https://www.zentao.net/zentao-install/zentao-linux-install-80523.html 部署 …

matine组件库踩坑日记 --- react

Mantine实践 一 禁忌核心css样式二 添加轮播图扩展组件 一 禁忌核心css样式 import React from react import ReactDOM from react-dom/client import { BrowserRouter } from react-router-dom; import App from ./App.jsx import ./index.css import mantine/core/styles.cs…

如何PR到别人仓库(指定分支,无废话)

如何PR到别人仓库&#xff08;指定分支&#xff09; 记录一下&#xff0c;之前都是直接master分支&#xff0c;现在记录如何pr到别人仓库的其他分支 首先进入别人仓库然后点击fork到自己仓库 步骤&#xff08;以博主自己一个例子为例&#xff09; &#xff08;1&#xff09;…

Andriod Stdio新建Kotlin的Jetpack Compose简单项目

1.选择 No Activity 2.选择kotlin 4.右键选择 在目录MyApplication下 New->Compose->Empty Project 出现下面的画面 Finish 完成

MySql 数据库 - 下载安装

MySQL数据库 简单介绍 数据库 数据存储的仓库数据库管理系统 操作和管理数据库的大型软件SQL 操作关系型数据库的变成语言&#xff0c;是一套标准 版本 MySQL官方提供了两种不同的版本&#xff1a; 社区版 免费&#xff0c;MySQL不提供任何的技术支持商业版 收费&#xff0c…

[crypt]-密码学心声

通过音乐来传递情报&#xff0c;乐谱如下&#xff1a; 乐谱中有请转成艾塞克、十进制等等&#xff0c;可以将数字转为assic试试&#xff0c;1234567&#xff0c;猜测是8进制&#xff0c;三位一组&#xff0c;破解如下&#xff1a; oct8 [111, 114, 157, 166, 145, 123, 145, …

【vue教程】二. Vue特性原理详解

目录 回顾本章涵盖知识点Vue 实例和选项创建 Vue 实例Vue 实例的选项 Vue 模板语法插值表达式指令v-bindv-modelv-on 自定义指令创建自定义指令在模板中使用自定义指令自定义指令的钩子函数自定义指令的实例演示 指令注册局部注册指令过滤器 数据绑定和响应式原理响应式数据绑定…

Prometheus监控主机进程

前言 客户端安装及配置 Premetheus服务端配置 模板导入 grafana效果图 前言 此场景主要是利用process-export监控主机的进程存活、资源占用率&#xff0c;防止进程挂掉导致服务崩溃 gitlab地址&#xff1a;GitHub - ncabatoff/process-exporter: Prometheus exporter that…

unseping

nnnd&#xff0c;这道题谁标的难度1&#xff01;参考文章&#xff1a;江苏工匠杯-unseping&序列化&#xff0c;正则绕过(全网最简单的wp)_江苏工匠杯unseping-CSDN博客 这是这道题的源码&#xff0c;一看exec和unserialize就是反序列化和命令执行&#xff0c;还有个正则应…

安全防御拓扑1

目录 实验的拓扑&#xff1a; 要求&#xff1a; 我搭建的实验拓扑 步骤&#xff1a; 创建vlan&#xff1a; 接口配置&#xff1a; 防火墙&#xff1a; 防火墙配置&#xff1a; 建立安全策略&#xff1a; 防火墙的用户&#xff1a; 办公区的市场部和研发部用户 市场部…

camtasia怎么剪掉不用的部分 屏幕录制的视频怎么裁剪上下不要的部分 camtasia studio怎么裁剪视频时长 camtasia怎么剪辑视频教程

有时我们录制的屏幕内容&#xff0c;并不一定全部需要。那么&#xff0c;屏幕录制的视频怎么裁剪上下不要的部分&#xff1f;可以使用视频剪辑软件&#xff0c;或者微课制作工具来进行裁剪。屏幕录制的视频怎么旋转&#xff1f;录制视频的旋转也是一样的&#xff0c;均在编辑步…

kettle从入门到精通 第七五课 ETL之kettle血缘,数据血缘

在了解kettle血缘之前&#xff0c;咱们先来了解下什么是数据血缘&#xff1f; 1、数据血缘定义&#xff08;来自gpt&#xff09; 数据血缘&#xff08;Data Lineage&#xff09;是指在数据管理和数据分析中追踪数据的源头、流向和处理过程的能力。具体来说&#xff0c;数据血…