搞定二分查找,套路深得人心

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

在排序与查找算法中,二分查找是一种常用的用来在有序排列中查找指定元素的方法。

二分查找也称折半查找,是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按照关键字有序排列。

二分查找的查找思想为:首先将待搜索序列进行排序,既可以按关键字从小到大,也可以从大到小进行排序,将有序序列从中间一分为二,比较中间值与目标值的大小关系,若待搜索序列是按从小到大进行排列的,且此时中间值比目标值小,那么说明目标值一定不在有序序列的左边,接下来重复上述步骤对右边的序列进行进一步二分查找,直到找到目标值或证明目标值不存在,从而完成一次对目标序列的完整的二分查找。其程序语言描述如下:

int binary_search(int *num, int n, int x) {
    int head = 0, tail = n - 1, mid;
    while ( head <= tail ) { 
        mid = (head + tail) >> 1;
        if ( num[mid] == x ) return mid;
        if ( num[mid] < x ) head = mid + 1;
        if ( num[mid] > x ) tail = mid - 1;
    }   
    return -1; 
}

二分查找算法考虑到目标元素与有序序列之间的关系,将搜索指定元素的时间复杂度降低到O(logn)级别。

以上是二分查找的最常规的用法(也可以称其为朴素二分),通过分而治之的思想,快速搜索目标序列中的特定元素是否存在。这里可以抽象的认为:在待搜索序列中,只要有一个元素满足了要求,就会返回该元素的索引位置,从而结束查找。但是当二分查找的应用场景变为“在待搜索序列(有序序列)中,找到‘最后一个’或‘第一个’满足要求的元素”时,就要对上述算法进行一点小小的改动。

  • 查找满足条件的最后一个元素

如果不能理解什么叫做“满足条件的最后一个元素”,不妨按照既定的“1”表示满足条件,“0”表示不满足条件,当一个有序序列仅由“1”和“0”构成时,例如:“111110000”,“满足条件的最后一个元素”就是该序列中的最后一个“1”。

如果直接使用“朴素二分”的算法,将会在找到任一满足条件的元素后就返回该元素的索引位置,而无法保证该元素是所有满足条件的最后一个元素。

要想找到“满足条件的最后一个元素”,可以这样来修改代码:

// 11111111100000000 find the last 1
int binary_last_search(int *num, int n) {
    int head = -1, tail = n - 1, mid;
    while ( head < tail ) { 
        mid = (head + tail + 1) >> 1;
        if ( num[mid] == 0 ) tail = mid - 1;
        if ( num[mid] == 1 ) head = mid;
    }   
    return head;
}

为什么要将左索引值(head)初始化为“-1”呢?假想,当一个序列中所有的元素都为“0”,也就是说没有一个元素是满足搜索条件的,此时循环会不断的向左缩小范围,直到左索引和右索引值相同时结束循环并返回左索引值,可想而知,此时返回的左索引值为下标“0”,但是连续存储空间的“0”位置原本是有数据的,且该数据不符合查找条件。为了在表示“未找到符合要求的元素”时不引起歧义,将左索引值初始化为“-1”来表示“该序列中无目标元素”。

除了每次对左、右索引值的修改以外,还要注意每次循环中对中间值的修改,mid = (head + tail + 1) >> 1;,之所以要“+1”,是考虑到整型数除2向下取整,防止mid = (head + tail + 1) >> 1;if ( num[mid] == 1 ) head = mid;在满足一定条件时导致出现死循环。

  • 查找满足条件的第一个元素

在理解查找“满足条件的第一个元素”时,可以照葫芦画瓢,即从有序序列“000011111”中找到第一个“1”。不再赘述,不理解如何这样处理的话,请再仔细揣摩一下“满足条件的最后一个元素”的描述。直接上代码:

// 00000111111111 find the first 1
int binary_first_search(int *num, int n) {
    int head = 0, tail = n, mid;
    while ( head < tail ) { 
        mid = (head + tail) >> 1;
        if ( num[mid] == 0 ) head = mid + 1;
        if ( num[mid] == 1 ) tail = mid;
    }   
    return head == n ? -1 : head;
}

这里同样要注意如何处理序列中不存在满足条件的元素的返回值问题。

以上两段代码描述的就是当“朴素二分”遇到特殊情况时的处理方式,当然,特殊情况下的二分查找并不是真的只适合用来在一堆“1”和“0”中找到那个满足条件的最后一个“1”或者第一个“1”,这里的“1”表示满足条件,既然是“条件”,当然就可以根据具体问题具体分析了,举一个简单的例子,幼儿园里有一群小朋友,每个小朋友手中都拿着一定数量的糖果,按照每个小朋友手中的糖果数量对小朋友进行排队,糖果数量多的小朋友站在前面,现在要找到队伍中所有手中糖果数量超过5个的小朋友的最后一位。是不是就可以简化为“11111000”模型,从而套用对应的算法来解决问题。

除了上面提到的“朴素二分”和“特殊情况下的二分”以外,在处理一些实际问题时,还经常遇到一些限制条件,从而使得“二分查找”复杂化,但万变不离其宗,究其根本,还是要回归到“二分”的思想中来的。总结了在练习中遇到的一些题目,找其共性,发现这些带有“附加(限制)条件”的问题,也是有规律的,或者说有解题套路的。

首先归纳遇到的题目:

  1. 某林业局现在有 N 根原木,长度分别为 Xi,为了便于运输,需要将它们切割成长度相等的 M 根小段原木(只能切割成整数长度,可以有剩余),小段原木的长度越长越好,现求小段原木的最大长度。例如,有 3 根原木长度分别为 6, 15, 22,现在需要切成 8 段,那么最大长度为 5。
  2. 某公司的程序猿每天都很暴躁,因为他们每个人都认为其他程序猿和自己风格不同,无法一同工作,当他们的工位的编号距离太近时,他们可能会发生语言甚至肢体冲突,为了尽量避免这种情况发生,现在公司打算重新安排工位,因为有些关系户的工位是固定的,现在只有一部分工位空了出来,现在有 N 个程序猿需要分配在 M 个工位中,第 i 个工位的编号为 Xi,工位编号各不相同,现在要求距离最近的两个程序猿之间的距离最大,求这个最大距离是多少。Xi 和 Xj 工位之间距离为 |Xi−Xj|。例如,1, 2, 8, 4, 9 号工位上安排3个程序猿,其最近的两个程序猿的距离最大为3。
  3. 小明小时候很贪玩,在他童年时期的某一天,他在地上丢了 A 个瓶盖,为了简化问题,我们可以当作这 A 个瓶盖丢在一条直线上,现在他想从这些瓶盖里找出 B 个,使得距离最近的 2 个瓶盖间距离最大,他想知道,最大可以到多少呢?
  4. 有 N 条绳子,它们的长度分别为 Li。如果从它们中切割出 K 条长度相同的绳子,这 K 条绳子每条最长能有多长?答案保留到小数点后 2 位(直接舍掉 2 位后的小数)。
  5. “跳石头”比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。
  6. 现在要把 m 本有顺序的书分给 k 人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三、第四本书给同一个人抄写。现需要设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。如果有多解,则尽可能让前面的人少抄写。

考虑到可能会占用太长的篇幅,具体的代码就不在这里展示了,如果小伙伴们对其中的哪个题目有问题,可以私信我进行交流。下面主要说明上述问题的解题套路。

就上述这样一类在“特殊二分”情况下还有附加限制的问题,将其归结为“答案二分”。顾名思义,就是在所有可能的答案中进行二分查找,找到最符合要求的那一项。

解题步骤为:

  1. 根据题目要求,找到“答案”的可能范围,如果无法精确到具体值,可以适当的扩大范围来简化问题;
  2. 找到答案的约束条件,例如上面题目中的第一题,要使切割完的最大长度为5,还要同时满足“切成8段”的约束;
  3. 判断是在所有满足条件中查找“最后一个”还是“第一个”,这里介绍一种简单的做法,就是结合所给输入输出样例,列举样例两边的值,若左边的值比样例小但同时满足条件,那么说明是在“11111000”中找到最后一个“1”,反之就是找“000011111”中的第一个“1”;有时候题目中的“尽可能少”、“尽可能多”一类的修饰词也可以作为线索。
  4. 在答案范围中套用对应的“特殊二分”,对中间值对应的答案进行处理,判断是否满足约束条件,缩小答案范围,继续进行“二分”,直到找到“最佳”答案。

可能上述的解题步骤看起很是生硬,不容易理解,那下面以切割木头的问题为例,描述如何通过“答案二分”来解决这一类问题。

问题描述:

某林业局现在有 N 根原木,长度分别为 Xi,为了便于运输,需要将它们切割成长度相等的 M 根小段原木(只能切割成整数长度,可以有剩余),小段原木的长度越长越好,现求小段原木的最大长度。例如,有 3 根原木长度分别为 6, 15, 22,现在需要切成 8 段,那么最大长度为 5。

输入:

第一行两个整数 N,M。(1≤N≤100,000,1≤M≤100,000,000),

接下来 N 行,每行一个数,表示原木的长度 Xi。(1≤Xi≤100,000,000)

输出:

输出小段原木的最大长度, 保证可以切出 M 段。

样例输入:

3 8

6

15

22

样例输出:

5

代码如下:

#include <iostream>
using namespace std;

int n, m, num[100005], lr;

int func(int len) {
	int s = 0;
	for (int i = 0; i < n; i++) {
		s += num[i] / len;
	}
	return s;
}

int main() {
	cin >> n >> m;
	for (int i = 0; i < n; i++) {
		cin >> num[i];
        lr = max(lr, num[i]);
 	}
 	int l = 1, r = lr;
 	while (l != r) {
 		int mid = (l + r + 1) / 2;
 		int s = func(mid);
 		if (s >= m) {
 			l = mid;
 		} else {
 			r = mid - 1;
 		}
}
 	cout << r << endl;
 	return 0;
}

首先,答案所指即问题所问,问:输出小段原木的最大长度,那么答案就是木头切割完的长度,确定答案的范围,因为要切割成整数长度,那么最短为1,最长为lr(所有木头原本长度的最大值);

其次,找到约束条件,由问题描述得,在满足段数的要求上,尽可能长的切割木头,这里的段数就是约束条件,这里定义func函数来计算对应答案的木头段数;

然后,判断是套用“1111000”还是“0001111”模板,样例输出为5,表示切割完的木头长度为5满足8段的要求,通过计算可以发现,当长度为4, 长度为3时都是满足要求的,但不是最佳的解,因此可以得知这是一个“1111000”找最后一个“1”的问题。(满足约束条件的最长木头,最长);

最后,准备工作做好了,开始对答案进行二分,套用模板,直到找到解。

总结一下,本文由“二分查找”的算法思想出发,将常见的“二分”问题归纳为三类,分别为“朴素二分”、“特殊二分”以及“答案二分”,并分析了具体使用场景。

二分法常见套路 众所周知,二分法能非常高效地从有序数组中找出特定元素,时间复杂度为 O(logN)。 一般而言,遇到要求 O(logN) 时间复杂度的搜索问题,十有八九会用到二分查找。 注意:求二分法的中点时注意防止溢出,两个 32 位正整数相加可能会溢出。 下面整理下二分法常见的套路套路一:搜索数组中等于某个元素的位置 这种问题基本就是套二分法的模版,二分法模版整理如下: publicintbinarySearch(int[]nums,inttarget){ intl... 阅读详情

相关推荐

二分查找Binary Search套路解题模板【LeetCode刷题套路教程3】

二分查找Binary_Search套路解题模板【LeetCode刷题套路教程3】

Python算法系列-双指针问题

python算法-双指针问题一、数组合并1. 使用模拟指针并两个有序数组2.模拟指针说明:二、二分法(折半查找法)1.有序数组的二分法查找2. 二分法说明三、链表(双链表单链表区别) 一、数组合并 1. 使用模拟指针并两个有序数组 # 使用指针合并两个数组 arr1 = [1,3,4,6,7] arr2 = [2,5,8,9,10] #定义两个有序数组,并初始化赋值 ind = 0 # a...

TianYao 7003

二分查找代码讲解

#include<stdio.h> int binary_search(int *arr,int n,int x){ int head=0,tail=n-1,mid;//定义头尾指针中间指针 while(head<=tail){ mid=(head+tail)>>1;//中间指针等于头尾指针相加的一半 if(arr[mid]==x) return mi...

Rosiness^的博客 548

有序数组的二分查找---查找元素第一次最后一次出现的位置

package searching; public class BinSearch_first_last { public static void main(String[] args) { // TODO Auto-generated method stub int[] a={3,5,10,10,10,13,13,19,23}; System.out.println(first...

Baymax_wyl的博客 4298

二分查找

最近在看算法第四版,其中有说到二分搜索,也就是二分查找,也在leetcode上刷题,觉得也算是一种比较厉害的算法思想吧,总结下: 定义 二分查找又叫折半查找,要求待查找的序列有序。每次取中间位置的值与待查关键字比较,如果中间位置的值比待查关键字大,则在前半部分循环这个查找的过程,如果中间位置的值比待查关键字小,则在后半部分循环这个查找的过程。直到查找到了为止,否则序列中没有待查的关键字。 优缺点 ...

ForeverSen的博客 1272

二分算法 && 二分的特殊情况 && 小数的二分 && 二分答案

二分算法 一、 sort()排序函数 1、要点: 头文件 #include <algorithm> // algorithm是算法的意思 sort(num, num + n) // n是数组的长度,表示从num[0] 排序到num[n - 1], 默认是升序的 降序排序 bool cmp(const int &a, const int &b) {//默认写法 return a > b;//降序排序 } sort(num, num + 8, cmp);

zi2242975806的博客 531

我写了一个套路,助你随心所欲运用二分搜索

后台回复进群一起刷力扣点击卡片可搜索关键词????读完本文,可以去力扣解决如下题目:875.爱吃香蕉的珂珂(Medium)1011.在D天内送达包裹的能力(Medium)我们前文 我作了首...

fdl123456的博客 863

headtail命令

head 输出文件的头部 tail 输出文件的尾部 两个命令都是打印显示文件内容 head 默认打印前10行内容 如果有多个文件,在每个文件输出之前添加文件名 -c +NUM 按字符大小进行输出显示 -n +NUM 按行数进行输出显示 tail head的选项后还附加 -f == --follow=descriptor -F == --follow=name 区别 de...

zhou_para的博客 4874

查找算法二分查找,二分特殊查找算法

一.二分查找算法 int binary_search(int *num,int n,x) { int head =0,tail = n-1,mid; while(head <= tail) //这边可以相等就是为了查找数组的边界是否为解 { mid =(head+tail) >> 1; if(num[mid] = x) return mid; else if(num[mid] < x) head = mid+1; else

weixin_42224577的博客 271

关于二分查找的解释以及实现

前言: 首先,我个人认为二分查找是一种很好用的查找方式,其有时也被称作折半查找。这个查找方法虽然简单,但是刚接触程序语言的好兄弟,可能也会有点懵。最近,刚好遇到了这样的题,正好就说说二分查找吧。 关于二分查找的解释以及个人理解: 其实二分查找我们数学上的二分法很像。查找的时候每次都是从数据的中间开始,按照一定规则向左或者向右继续查找。也因此,二分查找的数据被要求具有一定的规则。一般是有序性。也就是说,二分查找的数据一般是从大到小排序,或者从小到大排序。当然,也不排除...

qq_58754996的博客 644

暴躁的程序员

题目描述 某公司的程序猿每天都很暴躁,因为他们每个人都认为其他程序猿自己风格不同,无法一同工作,当他们的工位的编号距离太近时,他们可能会发生语言甚至肢体冲突,为了尽量避免这种情况发生,现在公司打算重新安排工位,因为有些关系户的工位是固定的,现在只有一部分工位空了出来,现在有 N 个程序猿需要分配在 M 个工位中,第 i 个工位的编号为 Xi,工位编号各不相同,现在要求距离最近的两个程序猿之间的距离最大,求这个最大距离是多少。Xi Xj 工位之间距离为|XiXj|。 输入 输入共 M+1 行。 第一行两

m0_67401270的博客 223

排序二分查找(从小到大排序)

// 版权所有 (C)2007, ***(成都)有限公司。 // // 描述: //作业4.3:排序二分查找(从小到大排序) //将strList排序,需写出三种排序算法(冒泡排序、插入排序、快速排序) //然后查找字符串,需写出两种查找算法(顺序查找算法二分查找算法) // // 作者:陈进宇 chenjinyu_china@yeah.net // 日期:start:2009/3/6 en

chenjinyu的专栏 1416

二分查找套路

二分查找套路 二分查找思路很简单,细节是魔鬼 int binary_search(int *nums, int n, int x) { int head = 0, tail = ...; while (...) { int mid = head + (tail - head) >> 1; if (nums[mid] == x) return ...; if (nums[mid] > x) head = ...; tail

HSalome的博客 225

算法】【二分法二分查找解题套路框架【转载】

原文链接:https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie/er-fen-cha-zhao-xiang-jie 读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: 704.二分查找 34.在排序数组中查找元素的第一个最后一个位置 前言 先给大家讲个笑话乐呵一下: 有一天阿东到图书馆借了 N 本书,出图书馆的时候,警报响了,于是保安把阿东拦下,要检查一下哪本书没有登记出借。阿东正准备把每一本书在报警.

weixin_43868754的博客 704

丢瓶盖..

题目描述 ​ 小明小时候很贪玩,在他童年时期的某一天,他在地上丢了 A 个瓶盖,为了简化问题,我们可以当作这 A 个瓶盖丢在一条直线上,现在他想从这些瓶盖里找出 B 个,使得距离最近的 2 个距离最大,他想知道,最大可以到多少呢? 输入 ​ 第一行两个整数 A,B。(2≤B≤A≤100,000) ​ 接下来 A 行,每行一个数,表示瓶盖的位置坐标 Ai。(1≤Ai≤100,000,000) 输出 ​ 一个正整数,相邻的两个瓶盖的最大距离。 样例输入 5 3 1 2 3 4 5 样例输出 2 数据规模与约定

qq_40713201的博客 327

1.11.1二分查找最接近的数

01:查找最接近的元素 描述 在一个非降序列中,查找与给定值最接近的元素。 输入第一行包含一个整数n,为非降序列长度。1 第二行包含n个整数,为非降序列各元素。所有元素的大小均在0-1,000,000,000之间。 第三行包含一个整数m,为要询问的给定值个数。1 接下来m行,每行一个整数,为要询问最接近元素的给定值。所有给定值的大小均在0-1,000,000,0

AIGC Studio:分享AIGC前沿知识和好玩应用,公众号同名。 4945

三种常见的查找方法(二分查找的两种实现以及顺序查找

二分查找 概念 二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。 查找过程 首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表...

Hello什么来着? 4694

C++string类常用方法

C++string类常用方法 1、string(const char *s) :将 string 对象初始化为 s 指向的字符串 string str("hello"); 2、string(size_type n,char c) :创建一个包含 n 个元素的 string 对象,其中每个元素都被初始化为字符 c string str(10, 'a'); 3、string(const string &str) :将一个 string 对象初始化为 string 对象 str(复制构造函数) str

Goallegoal的博客 9万+
上一篇: 求两个三位数相乘得到的最大回文数
下一篇: 怒刷排列组合,如何解“排列组合”一类的问题
Goallegoal
博客等级 码龄9年 210粉丝 242原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值