二分算法与练习

关于二分算法的定义等基础知识在上篇文章中已有详细讲解,那么今天我们主要来讨论一下二分算法的具体操作。

二分算法的常见操作分为二分查找,二分答案,浮点二分,二分模板,下面我们就分别来介绍

二分查找

由上篇文章可知,二分查找所需要条件为:

①用于查找的内容逻辑是有序的

②查找的数量只能是一个,不能是多个

在二分查找中,目标元素的查找区间定义也非常重要,不同的区间定义写法是不同的,因为查找的区间是不断迭代的,所以确定查找的范围十分重要,主要就是左右区间的开和闭的问题,开闭不一样,对应的迭代方式也不一样,有以下两种方式:

①左闭右闭[left, right]

②左闭右开[left, right)

那了解完基本的思想,就来做一道题巩固一下吧(题目来源于力扣)

704、给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例 1:

输入: nums= [-1,0,3,5,9,12], target= 9

输出: 4

解释: 9 出现在 nums中并且下标为 4

示例 2:

输入: nums= [-1,0,3,5,9,12], target= 2

输出: -1

解释: 2 不存在 nums中因此返回 -1

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。

方法一:二分查找(二分查找题解
在升序数组 nums中寻找目标值 target,对于特定下标 i,nums[i] 和 target 的大小关系有三种情况:

①nums[i]=target,则下标i即为要寻找的下标;

②nums[i]>target,则 target 只可能在下标i的左侧;

③nums[i]<target,则 target 只可能在下标i的右侧。

定义查找的范围 [left,right],初始查找范围是整个数组。每次取查找范围的中点 mid,比较 nums[mid] 和 target的大小,如果相等则 mid 即为要寻找的下标,如果不相等则根据 nums[mid] 和 target的大小关系将查找范围缩小一半。

由于每次查找都会将查找范围缩小一半,因此二分查找的时间复杂度是 O(log⁡n),其中 n 是数组的长度。

二分查找的条件是查找范围不为空,即 left≤right。如果 target在数组中,二分查找可以保证找到 target,返回 target在数组中的下标。如果target不在数组中,则当 left>right时结束查找,返回 −1。

具体的代码为

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        while(left <= right){
            int mid = (right - left) / 2 + left;
            int num = nums[mid];
            if (num == target) {
                return mid;
            } else if (num > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return -1;
    }
};

掌握了这道经典例题,就说明你已经掌握了最基本的二分思想,那么下面学习一下进阶内容,二分答案。

二分答案

从名字就可以显而易见的看出来,二分答案就是对答案进行二分查找的算法。算法思想就是先二分枚举可能的答案,然后对答案并进行判断,直至找到正确的答案。这个算法的核心难点就是判断函数的设计,也就是验证答案是否可以通过题目中给的条件得到。

二分答案最常见的题型就是二分答案求最小和二分答案求最大以及第k小/大,那么接下来我们以例题的方式分别讲述一下这几种题型(题目均来源于力扣)

二分答案求最小

1283.使结果不超过阙值的最小除数(原题
给你一个整数数组 nums 和一个正整数 threshold  ,你需要选择一个正整数作为除数,然后将数组里每个数都除以它,并对除法结果求和。

请你找出能够使上述结果小于等于阈值 threshold 的除数中 最小 的那个。

每个数除以除数后都向上取整,比方说 7/3 = 3 , 10/2 = 5 。

题目保证一定有解。

示例 1:

输入:nums = [1,2,5,9], threshold = 6      输出:5
解释:如果除数为 1 ,我们可以得到和为 17 (1+2+5+9)。
如果除数为 4 ,我们可以得到和为 7 (1+1+2+3) 。如果除数为 5 ,和为 5 (1+1+1+2)。

示例 2:

输入:nums = [2,3,5,7,11], threshold = 11     输出:3

示例 3:

输入:nums = [19], threshold = 5       输出:4

提示:
  • 1 <= nums.length <= 5 * 10^4
  • 1 <= nums[i] <= 10^6
  • nums.length <= threshold <= 10^6

这道题的做题思路是二分查找,假设我们选择了除数d,当d增加时,正整数数组nums中的每个数num[i]除以d的结果num[i]/d单调递减,他们的和total同样也单调递减。那么本题最重要的单调性就被我们从这道题中剥离出来了。

那么显而易见下一步就是利用二分查找的方法,找出满足total<=threshold的最小除数d。此时有两种情况

①若total>threshold,说明d不符合要求。根据单调性我们可以知道:d减小,则total增大,那么此时[l,d]

②若total<=threshold,说明d符合要求。由于题目要求除数尽可能小,所以我们可以忽略(d,r],而在区间[l,d)中继续进行二分查找。

然后我们应该考虑最重要的上下限问题,显然,二分查找的下限是1,上限可以设置为数组nums中的最大值M,因为当除数d>=M时,数组nums中的每个数除以d的结果均为1,那么total的值就恒等于数组nums的长度。(注意:因为题目中要求除数尽可能小,所以d=M的情况一定不能漏掉

具体代码为

class Solution {
public:
    int smallestDivisor(vector<int>& nums, int threshold) {
    int l=1,r=*max_element(nums.begin(),nums.end());
    int ans=-1;
    while(l<=r)
    {
        int mid=(l+r)/2;
        int total=0;
        for(int num:nums)
        {
            total +=(num-1)/mid+1;
        }
        if(total<=threshold)
        {
            ans=mid;
            r=mid-1;
        }
        else
        {
            l=mid+1;
        }
    }
    return ans;
    }
};
最小化最大值

本质是二分答案求最小

给大家一道例题,结合上面所讲的知识点与思路,自行思考一下,大家可以把自己的答案放在评论区,大家一起讨论,如果有更好的方法也欢迎评论。

410.分割数组的最大值(力扣原题)

给定一个非负整数数组 nums 和一个整数 k ,你需要将这个数组分成 k 个非空的连续子数组。

设计一个算法使得这 k 个子数组各自和的最大值最小。

示例 1:

输入:nums = [7,2,5,10,8], k = 2    输出:18
解释:一共有四种方法将 nums 分割为 2 个子数组。 
其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。

示例 2:

输入:nums = [1,2,3,4,5], k = 2    输出:9

示例 3:

输入:nums = [1,4,4], k = 3     输出:4

提示:

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 106
  • 1 <= k <= min(50, nums.length)
二分答案求最大

275.H指数II(力扣原题)

给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。

h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)至少 有 h 篇论文分别被引用了至少 h 次。

请你设计并实现对数时间复杂度的算法解决此问题。

示例 1:

输入:citations = [0,1,3,5,6]      输出:3

解释:给定数组表示研究者总共有5篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6次。由于研究者有3篇论文每篇至少被引用了3次,其余两篇论文每篇被引用不多于3次,所以她的h指数是3。

示例 2:

输入:citations = [1,2,100]       输出:2

提示:

  • n == citations.length
  • 1 <= n <= 105
  • 0 <= citations[i] <= 1000
  • citations 按 升序排列

这道题显而易见是H指数这道题的延伸扩展,升序排列是本体相对创新的部分,正是因为数组有序的特点,使得本题可以采用二分查找的方法。

设查找范围的初始左边界left为0, 初始右边界right为n−1其中n为数组 citations 的长度。每次在查找范围内取中点 mid,则有 n−mid篇论文被引用了至少 citations[mid] 次。如果在查找过程中满足 citations[mid]≥n−mid,则移动右边界 right,否则移动左边界 left。

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int n = citations.size();
        int left = 0, right = n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (citations[mid] >= n - mid) {
                right = mid - 1;
            } 
            else 
            {
                left = mid + 1;
            }
        }
        return n - left;
    }
};

最大化最小值

本质是二分答案求最大

给大家一道例题,结合上面所讲的知识点与思路,自行思考一下,大家可以把自己的答案放在评论区,大家一起讨论,如果有更好的方法也欢迎评论。

2517.礼盒的最大甜蜜度(原题)

给你一个正整数数组 price ,其中 price[i] 表示第 i 类糖果的价格,另给你一个正整数 k 。

商店组合 k 类 不同 糖果打包成礼盒出售。礼盒的 甜蜜度 是礼盒中任意两种糖果 价格 绝对差的最小值。

返回礼盒的 最大 甜蜜度

示例 1:

输入:price = [13,5,1,8,21,2], k = 3     输出:8
解释:选出价格分别为 [13,5,21] 的三类糖果。
礼盒的甜蜜度为 min(|13 - 5|, |13 - 21|, |5 - 21|) = min(8, 8, 16) = 8 。
可以证明能够取得的最大甜蜜度就是 8 。

示例 2:

输入:price = [1,3,1], k = 2      输出:2
解释:选出价格分别为 [1,3] 的两类糖果。 
礼盒的甜蜜度为 min(|1 - 3|) = min(2) = 2 。
可以证明能够取得的最大甜蜜度就是 2 。

示例 3:

输入:price = [7,7,7,7], k = 2     输出:0
解释:从现有的糖果中任选两类糖果,甜蜜度都会是 0 。

提示:

  • 2 <= k <= price.length <= 105
  • 1 <= price[i] <= 109
第K小/大

378.有序矩阵中第K小的元素(力扣原题)

给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。

你必须找到一个内存复杂度优于 O(n2) 的解决方案。

示例 1:

输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8      输出:13
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13

示例 2:

输入:matrix = [[-5]], k = 1      输出:-5jing

提示:

  • n == matrix.length
  • n == matrix[i].length
  • 1 <= n <= 300
  • -109 <= matrix[i][j] <= 109
  • 题目数据 保证 matrix 中的所有行和列都按 非递减顺序 排列
  • 1 <= k <= n2
bool check(int **matrix, int mid, int k, int n) {
    int i = n - 1;
    int j = 0;
    int num = 0;
    while (i >= 0 && j < n) {
        if (matrix[i][j] <= mid) {
            num += i + 1;
            j++;
        } else {
            i--;
        }
    }
    return num >= k;
}

int kthSmallest(int **matrix, int matrixSize, int *matrixColSize, int k) {
    int left = matrix[0][0];
    int right = matrix[matrixSize - 1][matrixSize - 1];
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        if (check(matrix, mid, k, matrixSize)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

浮点二分

以上我们所讲述的都是对于整数而言的二分查找,那么浮点数能不能进行二分查找呢?

答案当然是肯定的,并且浮点数的二分查找相对整数来说要更加简单、方便理解,因为不需要考虑整数二分中最为麻烦的边界问题以及死循环问题。那么其他的方面就完全可以套用二分查找的模板,那么接下来我们一起来做一道题巩固一下吧。

790.数的三次方根

给定一个浮点数 n,求它的三次方根。

输入格式

共一行,包含一个浮点数 n。

输出格式

共一行,包含一个浮点数,表示问题的解。

注意,结果保留6位小数。

数据范围

−10000≤n≤10000

输入样例:
1000.00
输出样例:
10.000000

这个题我在acwing上看到过一个大佬的纯手写思路,感觉讲的非常详细,可以分享给大家,大家可以学习一下这种做题思路,遇到复杂问题时,向做理科题一样在演草纸上分析一下题意,画画图,有利于理清思路,提高算法能力。

#include <iostream>
#include <cstdio>
using namespace std;
double x;
int main() {
    cin >> x;
    // 确定边界值
    double l = -100000, r = 100000;
    // 注意循环条件处理精度问题
    while (r - l > 1e-8) {
        // 步骤 A: 找中间值
        double mid = (l + r) / 2;
        // 步骤 B: 判断
        if (mid * mid * mid < x) l = mid;
        else r = mid;
    }
    printf("%.6f", r);
    return 0;
}

二分模板

那么最后一起回顾一下最基本的二分模板

//查找左边界 SearchLeft 简写SL
int SL(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid; 
        else l = mid + 1; 
    }	
    return l;
 // 最后r=l
}
//查找右边界 SearchRight 简写SR
int SR(int l, int r) 
{
    while (l < r)
    {					
        int mid = l + r + 1 >> 1; //需要+1 防止死循环
        if (check(mid)) l = mid;
        else r = mid - 1; 
    }
    return r;  // 最后r=l
}

  • 22
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法与数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。
逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法与数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。
作为一个程序员,算法与数据结构是你必须掌握的基本知识之一。它们是解决问题和优化代码性能的关键工具。 算法是一系列定义良好的步骤,用于解决特定问题或执行特定任务。它们可以是数学公式、逻辑推理或一系列编程指令。算法有助于解决各种问题,例如查找、排序、图形处理等。 数据结构是组织和存储数据的方式。它们可以是简单的变量、数组、链表、栈、队列、树或图等。选择合适的数据结构可以显著提高算法的效率和性能。 入门算法与数据结构,你可以从以下几个方面开始学习: 1. 掌握基本的数据结构:了解数组、链表、栈和队列等基本数据结构的概念、特点及其操作。 2. 学习常见的算法:了解常见的排序算法(如冒泡排序、插入排序、快速排序)、查找算法(如线性查找、二分查找)以及递归算法等。 3. 理解复杂度分析:学习如何分析算法的时间复杂度和空间复杂度,了解如何评估算法的效率和性能。 4. 解决实际问题:通过练习和实践,尝试用算法与数据结构解决一些实际的编程问题,例如字符串处理、图形处理等。 除了自学,你可以参考一些经典的教材和在线教程来加深理解。一些推荐的资源包括《算法导论》、《数据结构与算法分析》、LeetCode等在线平台。 记住,算法与数据结构是一个长期学习的过程,不仅需要理解概念,还需要实践和不断地思考如何应用它们解决实际问题。祝你在算法与数据结构的学习中取得好成果!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值