二分法篇——于上下边界的扭转压缩间,窥见正解辉映之光(1)

前言

二分法,这一看似简单却又充满哲理的算法,犹如一道精巧的数学之门,带领我们在问题的迷雾中找到清晰的道路。它的名字虽简单,却深藏着智慧的光辉。在科学的浩瀚星空中,二分法如一颗璀璨的星辰,指引着我们如何高效地寻找答案。本文将带领大家走进二分法的世界,探讨它的原理、应用及其在各种问题中的深远影响。

一. 原理讲解

二分法,顾名思义,是将一个问题或区间不断地分成两个部分,逐步逼近目标答案。最常见的应用是求解有序数列中的某个元素,或者求解某个函数的零点。
其基本思路如下:

  • 初始化区间: 在一维空间中,选择一个包含目标值的初始区间。
  • 逐步缩小区间: 将区间分为两半,判断目标值是否在左半区间或右半区间中,根据判断结果进一步缩小搜索区间。
  • 终止条件: 直到找到目标值或区间缩小到足够小为止。

这种"分而治之"的策略,极大地提高了搜索效率,尤其在处理大规模数据时,具有显著的优势。

下面我们将结合具体题型进行二分法的使用与讲解。

二. 二分查找

2.1 题目链接:https://leetcode.cn/problems/binary-search/description/

2.2 题目分析:

  1. 题目中给出一个升序排列的数组,其中元素有正有负
  2. 要求查找并返回数组内与target相同的元素的下标
  3. 如果不存在,则返回-1
  4. nums内的所有元素都不重复

2.3 思路讲解

暴力解法:

此题较为简单,查找目标值,直接遍历即可,且数据量不大,应该不会超时,不再给出示例代码。

二分法:

  1. 根据上文提到的原理可知,我们首先需要确定左右边界,因此令left=0,right=nums.size()-1.
  2. 由于该题数组为升序排列,二分之后具有二段性,即mid左侧区间内所有元素都小于mid,而mid右侧区间内所有元素都大于mid
  3. 因此我们进行while循环,逐次二分判断即可。如果循环期间内未成功返回target的下标,说明不存在,反之,返回mid即可。

代码实现:

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left=0,right=nums.size()-1;//确定左右边界
        //由于两个指针相交时还未判断是否等于target,因此需要取等号
        while(left<=right)
        {
            int mid=(left+right)/2;
            if(nums[mid]>target)
            {
                right=mid-1;
            }//更新右边界
            else if(nums[mid]<target)
            {
                left=mid+1;
            }//更新左区间
            else
            {
                return mid;
            }

        }
        return -1;
        
    }
};

三. 在排序数组中查找元素的第一个和最后一个位置

3.1 题目链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/

3.2 题目分析:

  1. 题目给出按照升序排列的数组nums以及目标值target,要求返回target在数组内的起始下标和结束下标。
  2. 如果不存在,则返回{-1,-1}
  3. 时间复杂度要求为logn。

3.3 思路讲解:

问题:时间复杂度的要求和数组的二段性已经很明显的提示我们需要使用二分法,可是二分法我们只尝试过查找单个target,面对一连串的target,我们又该如何处理呢?

虽然我们要查找的target是一串区间,但是数组仍满足二段性,在target起始区间的左侧,所有元素均小于target,而在target结束区间的右侧,所有元素均大于target。

因此,我们使用两次二分,分别查找该区间的左右边界即可,具体步骤如下:

  • 仍旧令left=0,right=nums.size()-1,确定左右区间进行二分查找

  • 查找target的起始下标(左边界)如图:

  • 查找target的结束下标(右边界)如图:

代码实现:

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size()==0)
        {
            return {-1,-1};
        }
        int left=0,right=nums.size()-1;
        int begin=0;
        //查找左边界
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<target)
            {
                left=mid+1;
            }//更新left
            else
            {
                right=mid;
            }//更新right
        }
        if(nums[left]!=target)
        {
            return {-1,-1};
        }//若左边界未查找成功,说明不存在target,直接返回
        begin=left;//查找成功,更新左边界
        //查找右边界
        right=nums.size()-1;//将right恢复为初始状态
        while(left<right)
        {
            int mid=left+(right-left+1)/2;
            if(nums[mid]==target)
            {
                left=mid;
            }//更新left
            else
            {
                right=mid-1;
            }//更新right

        }
        return {begin,right};
        
    }
};

四. x的平方根

4.1 题目链接:https://leetcode.cn/problems/sqrtx/description/

4.2 题目分析:

  • 给出x,要求计算x的算术平方根
  • 结果只保留整数部分,而非按照四舍五入规则
  • x的范围很大,使用int会存在越界情况
  • x可能为0,此时应特殊处理

4.3 思路讲解:

我们可假设目标值为target,那么该题所有答案都在[0,n]的这个数组内。

且[0,target]内,所有元素的平方和均小于x,[target,x]内,所有元素的平方和均大于x,即满足二段性,因此可考虑使用二分法进行解决。

具体步骤如下:

  1. 令left=1,right=x,确实左右区间进行遍历
  2. 进行取中点操作,mid=left+(right-left+1)/2,如果mid*mid>x,说明target在[left,mid]内,right更新为mid-1.
  3. 如果mid*mid<=x,说明target在[left,mid]内,left更新为mid。
  4. 进行循环遍历,最终left即为所求。

4.4 代码实现:

cpp 复制代码
class Solution {
public:
    int mySqrt(int x) {
        int left=1,right=x;
        if(x==0)
        {
            return 0;
        }
        while(left<right)
        {
            long long mid=left+(right-left+1)/2;//防止越界
            if(mid*mid<=x)
            {
                left=mid;
            }//更新left
            else
            {
                right=mid-1;
            }//更新right
        }
        return left;
        
    }
};

五. 山脉数组的峰顶索引

5.1 题目链接:https://leetcode.cn/problems/peak-index-in-a-mountain-array/description/

5.2 题目分析:

  • 该数组呈山脉分布,设峰值元素为target,则[0,target]内元素递增,[target,n-1]内元素递减,要求返回峰值元素的下标
  • 时间复杂度要求为logn

5.3 思路讲解:

由上述分析不难发现可是用二分法。

  1. 令left=1,right=nums.size()-2,作为左右边界。
    注意:此处如此初始化是因为至少需要3个元素才能组成一个山峰,因此下标为0和下标为n-1的元素不可能为峰顶元素
  2. 求取中点mid=left+(right-left+1)/2,并令nums[mid]与nums[mid-1]进行比较。
  • 如果nums[mid]>=nums[mid-1],说明mid处在上升区间内,target一定位于[mid,right]内,因此更新left为mid
  • 如果nums[mid]<nums[mid-1],说明mid处在下降区间内,target一定位于[left,mid]内,因此更新right为mid-1
  1. while循环二分操作,最后left即为所求target

5.4 代码实现:

cpp 复制代码
class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left=1,right=arr.size()-2;//峰顶元素即为最大值
        while(left<right)
        {
            int mid=left+(right-left+1)/2;
            if(arr[mid]>arr[mid-1])
            {
                left=mid;
            }//mid在上升区间内
            else 
            {
                right=mid-1;
            }
        }
        return left;
        
    }
};

六. 小结

6.1 局限性

尽管二分法在许多情况下都表现出极高的效率,但它也并非万能。在应用二分法时,要求数据必须是有序的,否则无法直接应用。此外,二分法在处理某些特殊类型的问题时,可能需要额外的技巧或调整。例如,求解无序数据中的元素时,二分法并不能直接使用,需要先进行排序或采取其他的算法。

6.2 时间复杂度

二分法的时间复杂度为 O(log n),这使得它在处理大规模数据时,具有非常高的效率。在最坏的情况下,每一步都将问题规模缩小一半,从而大大减少了运算的次数。与线性搜索相比,二分法能大幅度提高搜索效率,尤其是在数据量极大的情况下。

6.3 结语

二分法作为一种简单而高效的算法,已经成为计算机科学与数学中不可或缺的一部分。它不仅仅是一个算法工具,更是我们思考问题、解决问题的哲学。在这条"二分之间"的道路上,我们不仅找到了问题的解答,也探索到了求解问题的一种智慧。它教会我们,在复杂问题面前,不妨将问题拆解,逐步攻克,最终发现通往答案的光明之路。

本篇关于二分法的介绍就暂告一段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!

相关推荐
涛ing33 分钟前
32. C 语言 安全函数( _s 尾缀)
linux·c语言·c++·vscode·算法·安全·vim
独正己身1 小时前
代码随想录day4
数据结构·c++·算法
我不是代码教父4 小时前
[原创](Modern C++)现代C++的关键性概念: 流格式化
c++·字符串格式化·流格式化·cout格式化
利刃大大4 小时前
【回溯+剪枝】找出所有子集的异或总和再求和 && 全排列Ⅱ
c++·算法·深度优先·剪枝
子燕若水4 小时前
mac 手工安装OpenSSL 3.4.0
c++
*TQK*4 小时前
ZZNUOJ(C/C++)基础练习1041——1050(详解版)
c语言·c++·编程知识点
Rachela_z5 小时前
代码随想录算法训练营第十四天| 二叉树2
数据结构·算法
细嗅蔷薇@5 小时前
迪杰斯特拉(Dijkstra)算法
数据结构·算法
追求源于热爱!5 小时前
记5(一元逻辑回归+线性分类器+多元逻辑回归
算法·机器学习·逻辑回归
ElseWhereR5 小时前
C++ 写一个简单的加减法计算器
开发语言·c++·算法