二分专题----如何优雅的写出二分

本文详细介绍了二分查找算法的基本原理,以及如何将其应用于不同编程问题,如在排序数组中查找元素、搜索插入位置、求平方根、找峰值等。通过红蓝染色法,展示了二分查找的核心思想:不断划分数组并判断性质,最终找到目标元素或范围。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、如何编写二分

二、题目练习

1、二分查找----点击跳转题目

2、在排序数组中查找元素的第⼀个和最后⼀个位置 点击跳转题目

3、搜索插⼊位置----点击跳转题目

4、x的平⽅根----点击跳转题目

5、⼭峰数组的峰顶---点击跳转题目

6、寻找峰值----点击跳转题目

7、搜索旋转排序数组中的最⼩值----点击跳转题目


本文使用的方法是基于b站up-- 五点七边 的视频---点击跳转视频

二分算法是一种在有序数组中查找特定元素的高效算法。它的基本思想是通过不断将数组分成两部分,然后比较目标元素与中间元素的大小,从而逐步缩小查找范围,直到找到目标元素或确定目标元素不存在。

二分算法是针对数组的查找算法,要求数组具有单调性,但请不要把单调性简单理解为数字的递增递减,其实只要是数组,无论是什么类型的数组,只要满足单调性,都可以使用二分算法;那究竟这个单调性是什么意思呢?

当我们需要在数组中查找某一元素时,如果我们能根据数组和元素的特征,把数组分成两部分,两部分各自持有不同的性质,那么就可以使用二分算法

红蓝各自代表一种性质,此时,红蓝的边界就是我们查找的元素

大多数情况就是找到一个性质,性质具有二段性,答案是二段性的分界点

一、如何编写二分

我们从前学习的二分算法是定义两个指针l、r,把l~r区间视为有效搜索区间,通过不断的对[l , r]的中点判断性质为缩小l~r区间,当区间长度为1时(l=r)即找到了目标,区间长度为0时(l>r)代表没有符合的目标,但如果按照这种搜索有效区间的思路来编写代码时,在代码的细节问题上总是容易出现边界问题,为了处理各种各样的边界问题,又有几种对应的二分模板;其实如果换一种角度,可以把这些二分模板统一为一个,这篇文章我们换一种角度来编写二分,使得二分算法有且仅有一个统一的模板。

我们暂且把这种方法叫做红蓝染色法:把待搜索的数组看成灰色(待搜索),把我们挖掘出来的性质分别看作红色和蓝色,所谓二分,此时就变成了不断判断数组元素的性质,对数组染色的过程,查找完成时,数组完全被红色、蓝色占据,红蓝边界即为查找结果。

此时再理解二分,其实就是想象一个挡板(黄色),把数组分为两部分(蓝、红),通过不断的迭代,l会移动到黄色的左边,r会移动到黄色的右边,再根据题意酌情返回l或者r。

伪代码如下:

l=-1, r=N  //在(l,r)的数组索引开区间内,数组元素都是灰色的

while l+1≠r  //l+1=r时,蓝红区域接触

	m = ⌊(l+r)/2⌋  //开区间中间元素

	if IsBlue(m)

		l=m  //蓝色区域向右拓展到中间元素

	else

		r=m  //红色区域向左拓展到中间元素

//根据实际情况返回l或r,但要注意:返回谁就要对谁做越界判断!
return l or r  

需要注意的是,一开始定义l、r指针时,它们是指向数组外的,这样才能代表数组是灰色待搜索的,也就是刚开始蓝色区域、红色区域在数组索引区域外,因为数组区域可能全蓝或全红,为保证考虑到所有情况不得不这么做。

二、题目练习

1、二分查找----点击跳转题目

题意:在单调递增的数组中查找值为target,可能不存在target

思考:把数组以target分割,左边元素的性质为:小于等于target;右边元素的性质为:大于target;最后找到的分界线中,左边为l,右边为r,如果l位置的元素不等于target则数组中没有值为target的元素

class Solution {
public:
    int search(vector<int>& nums, int target) 
    {
        int l = -1, r = nums.size();

        while(l+1 < r)
        {
            int mid = l + r >> 1;
            if(nums[mid] <= target)
                l = mid;
            else
                r = mid;
        }
        //引用l、r前需要判断是否为合法下标
        if( l == -1 || nums[l] != target) return -1;
        else return l;
    }
};

2、在排序数组中查找元素的第⼀个和最后⼀个位置 点击跳转题目

二分两次,分别找到红色、黄色的划分挡板

代码:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) 
    {
        int l = -1, r = nums.size();
        int ret1,ret2;
        if(r == 0) return {-1,-1};

        while(l +1 < r)
        {
            int mid = l + r >> 1;
            if(nums[mid] < target)
                l = mid;
            else
                r = mid;
        }
        if(l+1 >= nums.size() || nums[l+1] != target) return {-1,-1};
        ret1 = l + 1;
        l = -1, r = nums.size();
        while(l +1 < r)
        {
            int mid = l + r >> 1;
            if(nums[mid] <= target)
                l = mid;
            else
                r = mid;
        }
        ret2 = l;
        return {ret1,ret2};
    }
};

3、搜索插⼊位置----点击跳转题目

以小于等于target为标准划分数组,对比l指向的元素是否为target

代码:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) 
    {
        int l = -1 ,r = nums.size();

        while(l + 1 < r)
        {
            int mid = l + r >> 1;
            if(nums[mid] <= target)
                l = mid;
            else
                r = mid;
        }
        int index = 0;
        if(l == -1 ) index = 0;
        else if(nums[l] != target) index = l + 1;
        else index = l;

        return index;
    }
};

4、x的平⽅根----点击跳转题目

对0~INT_MAX做二分,挡板左边:平方小于等于x;挡板右边:平方大于x

代码:

class Solution {
public:
    //l: l^2 <= x
    //r: r^2 > x
    int mySqrt(int x) 
    {
        int l = -1, r = INT_MAX;

        while(l + 1 < r)
        {
            //做平方时防止溢出
            long long mid = l + r >> 1;
            if(mid*mid <= x)
                l = mid;
            else
                r = mid;
        }
        if(l==-1) return 0;
        else return l;
    }
};

5、⼭峰数组的峰顶---点击跳转题目

以山峰元素的右侧为挡板

挡板左侧:arr[i-1] < arr[i]

挡板右侧:arr[i-1] > arr[i]

代码:

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) 
    {
        //l:arr[l] < arr[l+1] 
        //r: arr[r] > arr[r+1] 
        //返回r
        int l = -1, r = arr.size();
        while(l + 1 < r)
        {
            int mid = l + r >> 1;
            if(arr[mid] < arr[mid+1])
                l = mid;
            else
                r = mid;
        }
        return r;
    }
};

6、寻找峰值----点击跳转题目

观察可知,峰值元素的左侧满足递增、右侧满足递减;由于是返回任意峰值,所以和上一题一样;

挡板左侧:arr[i-1] < arr[i]

挡板右侧:arr[i-1] > arr[i]

代码:

class Solution {
public:
    int findPeakElement(vector<int>& nums) 
    {
        int l = -1, r = nums.size();

        while(l +1 < r)
        {
            int mid = l + r >> 1;
            if(mid-1 >= 0)
            {
                if(nums[mid-1] < nums[mid])
                    l = mid;
                else
                    r = mid;
            }
            else    l = mid;//数组越界时说明在比较nums[-1]和nums[0]
            
        }
        return l;
    }
};

7、搜索旋转排序数组中的最⼩值----点击跳转题目

由于数组是由单调递增的数组旋转得到,那么得到的数组满足左侧区间永远大于右侧区间

以最小值左侧为挡板,取数组最后一个值x来比较(因为他是第二段区间的最大值)

挡板左侧:左侧元素 > x

挡板右侧:右侧元素 <= x (因为右侧区间可能只有一个元素,所以取等)

代码:

class Solution {
public:
    int findMin(vector<int>& nums) 
    {
        int l = -1, r = nums.size();
        
        int x = nums[r-1];
        while(l + 1 < r)
        {
            int mid = l + r >> 1;
            if(nums[mid] > x)
                l = mid;
            else
                r = mid;
          
        }
        return nums[r];
    }
};

8、点名----点击跳转题目

数组中记录的是未缺勤的学生,通过观察我们发现,如果所有学生都未缺勤,那么数组下标就会和学号正好相等,那么在点名还没点到缺勤学生时,这个性质依然成立,以缺勤学生本该在的位置为挡板:

挡板左侧:数组下标 等于 对应元素

挡板右侧:数组下标 不等于 对应元素

代码:

class Solution {
public:
    int takeAttendance(vector<int>& records) 
    {
        //l:下标和值相等 r:下标和值不想等
        int l = -1, r= records.size();

        while(l + 1 < r)
        {
            int mid = l + r >> 1;
            if(records[mid] == mid)
                l = mid;
            else
                r = mid;
        }
        return l+1;
    }
};

通过以上题目的练习我们发现,⼆分的本质:找到⼀个判断标准,使得查找区间能够⼀分为⼆,分界中存在一个挡板,挡板左侧为l,挡板右侧为r

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值