详解“二分”系列算法

news2025/3/25 21:37:54

前言

1.学习建议

网上教二分系列算法的视频或者文章不在少数,每个人对于二分算法的理解都是不一样的,作者不建议小白刚学习二分系列算法就看很多不同的视频或者博客去学习,举个例子,有些教学提供的方法会把left赋值为-1,right赋值为n;而且每个人对于向上取整和向下取整的理解都是不一样的等等;大家都各有各的道理

新手小白看了不同的视频讲解很容易发懵,所以真心建议,找到一两个时长久高质量的视频,或者篇幅长,介绍完整的博客去认真的看,二分系列算法是有一定难度的,遇到看不懂的就去思考或者问问AI

作者看了很多视频讲解还有相关文章,总结出一套好理解的,主流的方法,供大家参考

一.背景介绍

二分法(Binary Search)是一种在有序数组或有序列表高效查找目标值的算法;通过不断将数组分为两半来缩小搜索范围,时间复杂度为O(logn),相比线性查找的O(n)效率大幅提升

二分算法的常见应用大概可以分为以下三种:

  • 二分查找(查找指定目标值,目标值存在则返回该值索引,不存在则返回-1)
  • 二分检索(通常是指给定目标值,让我们寻找大于等于目标值的最小值,或者小于等于目标值的最大值)
  • 二分答案(基于二分检索来解决问题的一种算法)

二.本文解决的逻辑问题

1.详解什么时候使用向上取整,什么时候使用向下取整

2.详解while循环结束条件:到底什么时候用小于,什么时候用小于等于

3.详解二分答案的基本思路

三.二分查找

1.源码(附带保姆级注释)

int Search(int arr[], int numsize, int target)
{
	//针对于二分查找和二分检索,直接无脑写,二分答案另说
	int left = 0;
	int right = numsize - 1;

	//注意:二分查找是要带等号的!!!
	while (left <= right)
	{	
		//二分查找,不需要你考虑向下或者向上取整的问题,直接无脑写默认的向下取整
		//这里解释一下为什么不写 int mid = (left+right)/2,其实这么写逻辑上是对的
		//但是,如果left和right特别大的时候,可能会导致mid溢出,用我下面的写法就可以避免溢出的问题
		int mid = left + (right - left) / 2;

		//无论是二分查找,还是二分检索,都直接无脑写下面的三板斧
		//因为无非就这三种情况呀,直接把模板打出来,再去具体思考如何处理if判断语句内部的问题

		if (arr[mid] < target)
		{
			//如果当前mid对应的值小于目标值,说明什么?
			//是不是说明mid前面的值,包括mid自己都要比target小,那我还需要把left移动到mid的当前位置吗?
			//不需要啊哥们,你问问自己,你要找的数字是不是target?
			//你当前的mid对应的数组中的值都比target小了,是不是说明当前mid对应的数组中的值,也不是我们要找的值?
			//但是我们无法确定,mid的下一个位置,mid+1对应的数组中的值是不是等于target
			//所以我们直接把left移动到mid+1的位置
			left = mid + 1;
		}
		else if (arr[mid] > target)
		{	
			//如果说当前mid对应的数组中的值大于目标值,说明什么?
			//是不是说明mid后面的值,包括mid自己对应的值都要比target大,所以跟上面left的移动方式同理啊
			//我们直接把right指针移动到mid的上一个位置mid-1就好了
			right = mid - 1;
		}
		else if (arr[mid] == target)
		{
			//我们要执行的是二分查找,所以mid对应的数组中的值一旦等于target,我们直接返回mid就好了
			return mid;
		}
	}
	//没找到就返回-1呗
	return -1;
}

四.二分检索

1.寻找满足条件的最小值(采用向下取整方法)

问题:在一个升序数组 {1, 3, 5, 7, 9} 中,找到第一个大于等于 5 的元素

//向下取整方法,找到第一个大于等于target的元素
int fun1(int arr[], int numsize,int target)
{	
	//针对于二分查找和二分检索,直接无脑写,二分答案另说
	int left = 0;
	int right = numsize - 1;

	//注意:二分检索不带等号!!!
	while (left < right)
	{	
		//使用向下取整方法
		int mid = left + (right - left) / 2;

		//找到第一个大于等于target的元素
		//无论是二分查找,还是二分检索,都直接无脑写下面的三板斧
		//因为无非就这三种情况呀,直接把模板打出来,再去具体思考如何处理if判断语句内部的问题
		if (arr[mid] < target)
		{	
			//如果当前mid对应的值小于target,我可以确定的是mid及mid前面的元素都比target要小
			//所以我可以直接把left移动到mid+1的位置
			left = mid+1;
		}
		else if (arr[mid] > target)
		{	
			//如果当前mid对应的值大于target,我可以确定的是mid及mid后面的值都比target大
			//但是mid前面的值是否还有比target大的呢(如果存在就说明当前mid不是目标值),我不清楚
			//所以说我只能把right移动到mid的位置
			right = mid;
		}
		else if(arr[mid] == target)
		{	
			//如果当前mid对应的值等于target,我可以确定的是mid及mid后面的值都大于等于target
			//但是我无法确认mid之前是否还存在等于target的值,所以我们只能把right移动到mid的位置
			right = mid;
		}
	}
	//这里return left或者right都可以
	//return left;
	return right;
}

2.寻找满足条件的最大值(采用向上取整方法)

问题:在一个升序数组 {1, 3, 5, 7, 9} 中,找到最后一个小于等于 5 的元素

//向上取整方法,找到最后一个小于等于target的元素
int fun2(int arr[], int numsize, int target)
{	
	//针对于二分查找和二分检索,直接无脑写,二分答案另说
	int left = 0;
	int right = numsize - 1;

	//注意:二分检索不带等号!!!
	while (left < right)
	{
		//使用向上取整方法
		int mid = left + (right - left + 1) / 2;
		//找到最后一个小于等于target的元素
		//无论是二分查找,还是二分检索,都直接无脑写下面的三板斧
		//因为无非就这三种情况呀,直接把模板打出来,再去具体思考如何处理if判断语句内部的问题
		if (arr[mid] < target)
		{	
			//如果当前mid对应的值小于target,我可以确定的是mid及mid以前的值都比target小
			//但是mid的下一个值是否还存在比target要小的值呢(如果存在就说明当前mid不是目标值),我不知道
			//所以我们可只能把left移动到mid的位置
			left = mid;
		}
		else if (arr[mid] > target)
		{
			//如果当前mid对应的值大于target,我可以确定的是mid及mid以后的值都比target大
			//所以直接把right移动到mid的前一个位置(mid-1)
			right = mid - 1;
		}
		else if (arr[mid] == target)
		{	
			//如果当前mid对应的值等于target,那就说明mid及mid之前的值都小于等于target
			//但是我无法确认mid之后是否还存在等于target的值,所以我们只能把left移动到mid的位置
			left = mid;
		}
	}
	//这里return left或者right都可以
	//return left;
	return right;
}

五.解释什么时候使用向上取整,什么时候使用向下取整

向下取整

适用于寻找最小值的情况,例如第一个大于等于某个值的元素
适用于 right = mid 的更新规则

向上取整

适用于寻找最大值的情况,例如最后一个小于等于某个值的元素
适用于 left = mid 的更新规则
如果你使用错误,程序在执行某些用例的时候会出现死循环情况

六.解释while循环结束条件:到底什么时候用小于,什么时候用小于等于

二分查找为什么使用left<=right

这是因为二分查找的目标是在有序数组中精确查找某个目标值是否存在

  • 二分查找需要确保所有可能的候选值都被检查
  • 如果使用 left < right,当 left == right 时,会跳过最后一个元素的检查,可能导致漏判
  • 当 left > right 时,说明搜索范围已经无效,目标值不存在

二分检索为什么使用left<right

这是因为二分检索的目标是在有序数组中寻找满足条件的最大值或最小值

  • 二分法查找最大/最小值的核心是逐步缩小搜索范围,直到 left == right,此时 left 或 right 就是最终答案
  • 如果使用 left <= right,可能会导致死循环(例如在 left == right 时,mid 永远等于 left,无法缩小范围)
  • 当 left == right 时,搜索范围已经缩小到单个值,这个值就是答案,无需继续循环

七.解释二分检索中返回值为什么return right 或者 left 都可以

在二分检索中,循环结束时,right和left是处于相同位置的,所以返回他们哪个都可以,这里可以联系到while循环的结束条件去理解

八.二分查找和二分检索的本质区别

在二分查找中;假如我们要找到5这个数字,5这个数字要么在数组中,被找到,要么不在数组中,找不到
在二分检索中;假如我们要找到大于5的最小值,5这个数字可以不在数组中
这就是二分检索和二分查找的本质区别!!!

九.二分答案

1.解题步骤

  • 证明问题单调性所在
  • 确定问题单调区间的上下界
  • 设计check函数,方便每一次二分求解的时候,判断当前值是否满足指定的限定条件
  • 在单调区间上下界之内二分答案(通过循环实现二分):若当前值不行缩小一半的查询区间,继续查询;若当前值可行,为候选解,但继续缩小一半的空间求更优解

2.实战演示

题目描述:

农夫约翰有 n 个牛舍,它们的位置排列在一条直线上,坐标分别为 x₁, x₂, …, xₙ。他需要将 m 头牛分配到这些牛舍中,且每头牛必须放在不同的牛舍。为了确保牛之间有足够的活动空间,约翰希望使得任意两头相邻牛之间的最小距离尽可能大。请你帮助他找到这个最大的最小距离

输入格式:​

第一行包含两个整数 n 和 m(2 ≤ n ≤ 1e5,2 ≤ m ≤ n),分别表示牛舍数量和牛的数量
第二行包含 n 个整数 x₁, x₂, …, xₙ(0 ≤ xᵢ ≤ 1e9),表示牛舍的坐标。数据保证坐标按升序排列

输出格式:​

输出一个整数,表示最大的最小距离

示例输入:​

5 3
1 2 4 8 9

示例输出:​

3

本题思路

  1. 确定我们要求的值,“相邻两牛之间最短距离的最大值”
  2. check函数设计的原理是:检查是否能以至少 d 的距离放置至少 m 头牛
  3. 相邻两牛之间的距离可以看作一个升序数组,也就是说要找到这个升序数组中,合法(可以通过check检验)的最大值
  4. 既然是求取最大值,我们就可以通过二分检索中向上取整的方法设计函数

解题代码:

check函数代码:

// 检查是否能以至少 d 的距离放置至少 m 头牛
bool check(int d, int m, const vector<int>& arr) {
	int cnt = 1;                // 已放置的牛的数量(第一个牛舍必须放)
	int prev = arr[0];          // 上一头牛的位置

	for (int i = 1; i < arr.size(); ++i) {
		if (arr[i] - prev >= d) {
			cnt++;
			prev = arr[i];
			if (cnt >= m) {     // 提前终止条件:已满足数量要求
				return true;
			}
		}
	}
	return cnt >= m;           // 最终是否满足条件
}

二分答案主干代码:

int fun_asr(const vector<int>& arr, int m) {
	int left = 1;
	int right = arr.back() - arr[0];

	while (left < right) {
		int mid = (left + right + 1) / 2;  // 向上取整
		if (check(mid, m, arr)) {          // 当前距离可行,尝试更大的值
			left = mid;
		}
		else {                           // 不可行,缩小右边界
			right = mid - 1;
		}
	}
	return left;  // 最终 left == right,即为答案
}

完整的代码:

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

//二分答案

// 检查是否能以至少 d 的距离放置至少 m 头牛
bool check(int d, int m, const vector<int>& arr) {
	int cnt = 1;                // 已放置的牛的数量(第一个牛舍必须放)
	int prev = arr[0];          // 上一头牛的位置

	for (int i = 1; i < arr.size(); ++i) {
		if (arr[i] - prev >= d) {
			cnt++;
			prev = arr[i];
			if (cnt >= m) {     // 提前终止条件:已满足数量要求
				return true;
			}
		}
	}
	return cnt >= m;           // 最终是否满足条件
}

int fun_asr(const vector<int>& arr, int m) {
	int left = 1;
	int right = arr.back() - arr[0]; //极限思想,相邻两头牛的最大距离不会超过第一个牛舍到最后一个牛舍的距离

	while (left < right) {
		int mid = (left + right + 1) / 2;  // 向上取整
		if (check(mid, m, arr)) {          // 当前距离可行,尝试更大的值
			left = mid;
		}
		else {                           // 不可行,缩小右边界
			right = mid - 1;
		}
	}
	return left;  // 最终 left == right,即为答案
}

int main() {
	// 加速输入输出
	ios::sync_with_stdio(false);
	cin.tie(nullptr);

	// 读取输入
	int n, m;
	cin >> n >> m;
	vector<int> arr(n);
	for (int i = 0; i < n; ++i) {
		cin >> arr[i];
	}

	// 必须先将牛舍坐标排序(题目虽说明输入有序,但实践中建议强制排序)
	sort(arr.begin(), arr.end());

	// 计算并输出结果
	cout << fun_asr(arr, m) << endl;
	return 0;
}

3.作者对二分答案的理解

  • 二分答案的题目都可以用二分检索的方法解决,题目要求我们求取最大值,我们就用二分检索的方式结合向上取整解决问题,题目要求我们求取最小值,我们就用二分检索的方式向下取整解决问题

十.总结

建议吃透本文源码后多练习几道算法题,即使是作者本人,对于一些细节的理解也不是很熟练,也要勤加练习

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

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

相关文章

Python实现deepseek接口的调用

简介&#xff1a;DeepSeek 是一个强大的大语言模型&#xff0c;提供 API 接口供开发者调用。在 Python 中&#xff0c;可以使用 requests 或 httpx 库向 DeepSeek API 发送请求&#xff0c;实现文本生成、代码补全&#xff0c;知识问答等功能。本文将介绍如何在 Python 中调用 …

文档处理控件Aspose.Words 教程:.NET版中增强的 AI 文档摘要功能

Aspose.Words是一个功能强大的 Word 文档处理库。它可以帮助开发人员自动编辑、转换和处理文档。 自 24.11 版以来&#xff0c;Aspose.Words for .NET 提供了 AI 驱动的文档摘要功能&#xff0c;使用户能够从冗长的文本中快速提取关键见解。在 25.2 版中&#xff0c;我们通过使…

19,C++——11

目录 一、 C11简介 二、 新增的列表初始化 三、 新增的STL容器 四、 简化声明 1&#xff0c;auto 2&#xff0c;decltype 3&#xff0c;nullptr 五、右值引用 1&#xff0c;左值引用和右值引用 2&#xff0c;两种引用的比较 3&#xff0c;左值引用的使用场景 4&…

风尚云网|前端|前后端分离架构深度剖析:技术革新还是过度设计?

前后端分离架构深度剖析&#xff1a;技术革新还是过度设计&#xff1f; 作者&#xff1a;风尚云网 在数字化转型浪潮中&#xff0c;前后端分离架构已成为现代Web开发的主流模式。但这项技术真的是银弹吗&#xff1f;本文将从工程实践角度&#xff0c;剖析其优势与潜在风险&am…

CMS网站模板设计与用户定制化实战评测

内容概要 在数字化转型背景下&#xff0c;CMS平台作为企业内容管理的核心载体&#xff0c;其模板架构的灵活性与用户定制能力直接影响运营效率。通过对WordPress、Baklib等主流系统的技术解构发现&#xff0c;模块化设计理念已成为行业基准——WordPress依托超过6万款主题库实…

搭建个人博客教程(Hexo)

如何快速搭建一套本地的博客系统呢&#xff1f;这里有一套gitNode.jsHexo的部署方案来进行解决。 安装git Git 是一款免费开源的分布式版本控制系统&#xff0c;由 Linus Torvalds 于 2005 年为 Linux 内核开发设计。它通过本地仓库和远程仓库实现代码管理&#xff0c;支持分支…

Docker 可视化工具 Portainer

Docker 可视化工具 Portainer安装 官方安装地址&#xff1a;https://docs.portainer.io/start/install-ce/server/docker/wsl 一&#xff0c;首先&#xff0c;创建 Portainer Server 用来存储数据库的卷&#xff1a; docker volume create portainer_data二&#xff0c;然后…

数据库基础知识点(系列二)

1&#xff0e;关系数据模型由哪三个要素组成。 答&#xff1a;关系数据模型由关系数据结构、关系操作集合和关系完整性约束三部分组成。 2&#xff0e;简述关系的性质。&#xff08;关系就是一张二维表格&#xff0c;但不是任何二维表都叫关系&#xff09; 答&#xff1a;(1…

如何进行灌区闸门自动化改造-闸门远程控制系统建设

改造背景 操作效率低‌&#xff1a;人工启闭耗时耗力&#xff0c;单次操作需2-3人配合&#xff0c;耗时长。 ‌水资源浪费‌&#xff1a;依赖经验估算放水量&#xff0c;易导致漫灌或供水不足。 ‌管理滞后‌&#xff1a;无法实时监控水位、流量&#xff0c;故障响应延迟。 …

【算法笔记】图论基础(二):最短路、判环、二分图

目录 最短路松弛操作Dijkstra朴素Dijkstra时间复杂度算法过程例题 堆优化Dijkstra时间按复杂度算法过程例题 bellman-ford时间复杂度为什么dijkstra不能处理负权边&#xff1f;dijkstra的三个步骤&#xff1a;反例失效的原因 算法过程例题 spfa时间复杂度算法过程例题spfa求最短…

EMS小车技术特点与优势:高效灵活的自动化输送解决方案

北成新控伺服技术丨EMS小车调试视频 EMS小车是一种基于单轨运行的电动输送系统&#xff0c;通过电力驱动实现物料的高效搬运和输送&#xff0c;具有高效灵活、节能环保、多功能集成、行业适配性强等特性&#xff0c;广泛应用于汽车制造、工程机械、家电生产、仓储物流等行业自动…

uniapp运行到支付宝开发者工具

使用uniapp编写专有钉钉和浙政钉出现的样式问题 在支付宝开发者工具中启用2.0构建的时候&#xff0c;在开发工具中页面样式正常 但是在真机调试和线上的时候不正常 页面没问题&#xff0c;所有组件样式丢失 解决 在manifest.json mp-alipay中加入 "styleIsolation&qu…

C++ 性能优化隐藏陷阱:从系统调用到并发开销的深度反思

作为一名C++技术专家,我深知性能优化不仅是代码层面的艺术,更是理解硬件与语言交互的科学。在现代计算中,C++的抽象为开发者提供了便利,却也隐藏了硬件的复杂性。如何揭开这些“谎言”,让代码与硬件协同工作?本文将以小案例为载体,通过优化前后的对比,深入剖析每个章节…

Unity 使用 Protobuf(Pb2)二进制数据全流程工具详解

前言 在Unity游戏开发中&#xff0c;高效、快速、安全地读取配置数据是一项重要需求。本文介绍一种完整的解决方案——使用Protobuf二进制格式&#xff08;Pb2&#xff09;存储和读取游戏数据&#xff0c;并详细分享实现全流程的Unity工具。 一、技术流程概览 实现Unity读取…

基于QT(C++)实现绘图程序

绘图程序 1 核心算法 1.1 图元生成 1.1.1 直线 画直线的算法采用了课上讲到的 Bresenhan 算法&#xff0c;采用整数增量运算&#xff0c;精确而有效的光栅设备生成算法。 基本思想是&#xff1a;当直线斜率的绝对值小于 1 时&#xff0c;从左端点开始作为起点&#…

深入剖析ReLU激活函数:特性、优势与梯度消失问题的解决之道,以及Leaky ReLU 和 Parametric ReLU

深入剖析ReLU激活函数&#xff1a;特性、优势与梯度消失问题的解决之道 在深度学习领域&#xff0c;激活函数的选择直接影响神经网络的训练效果和性能。整流线性单元&#xff08;Rectified Linear Unit&#xff0c;简称ReLU&#xff09;因其简单性、高效性以及对梯度消失问题的…

服务注册/服务发现-Eureka

目录 1.引言&#xff1a;如果一个父项目中有多个子项目&#xff0c;但是这些子项目如何如何相互调用彼此的业务呢&#xff1f; 2.什么是注册中心 3.CAP理论 4.EureKa 5.服务注册 6.服务发现 7.负载均衡 1.引言&#xff1a;如果一个父项目中有多个子项目&#xff0c;但是…

计算机网络——数据链路层的功能

目录 物理链路 逻辑链路 封装成帧&#xff08;组帧&#xff09; 帧定界 透明传输 SDU 差错控制 可靠传输 流量控制 介质访问控制 主机需要实现第一层到第五层的功能&#xff0c;而路由器这种节点只需要实现第一层到第三层的这些功能 假设左边用户需要给右边用户发送…

第60天:Web攻防-XSS跨站文件类型功能逻辑SVGPDFSWFPMessageLocalStorage

#知识点 1、Web攻防-XSS跨站-文件类型-html&pdf&swf&svg 2、Web攻防-XSS跨站-功能逻辑-postMessage&localStorage 术语&#xff1a;上传xss->其实就是将有恶意js代码的各类文件&#xff08;swf,pdf,svg,html.xml等&#xff09;上传->访问该文件->让浏…

C/C++都有哪些开源的Web框架?

CppCMS CppCMS是一个采用C语言开发的高性能Web框架&#xff0c;通过模版元编程方式实现了在编译期检查RESTful路由系统&#xff0c;支持传统的MVC模式和多种语言混合开发模式。 CppCMS最厉害的功能是WebSocket&#xff0c;10万连接在内存中长期保存占用的大小不超过600MB&…