二分查找, 看起来简单写起来恶心

本文详细解析了二分查找算法的两种常见实现,包括左闭右闭和左闭右开两种区间定义,并通过示例代码进行说明。同时,提到了二分查找在寻找排序数组中目标值的下标、搜索插入位置、查找元素的第一个和最后一个位置等场景的应用。此外,还介绍了如何处理边界条件以及二分查找在解决浮点数平方根和完全平方数问题中的应用。

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

最基本的二分查找题目:

​ 给定一个 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        

提示:

  • 你可以假设 nums 中的所有元素是不重复的。

  • 1 <= nums <= 2^31 - 1

思路

这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不

是唯一的( 有些题目 元素可以重复,片尾有力扣练习题),这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。

​ 二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如

  • 到底是 while(left < right) 还是 while(left <= right)
  • 到底是right = middle呢,还是要right = middle - 1呢?

​ 写二分法经常写乱,主要是因为**对区间的定义没有想清楚。非递归写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。就是while( 条件中是否满足等号 )

二分法第一种写法,

第一种写法,我们定义 target 是在一个在左闭右闭的区间里 ,也就是[left, right] , 会检查上边界,当 letf = right 的时候也进行判断,

最后如果未查到target 退出while的时候 则 right +1 = letf 就是 下标 letf在right后面 ,这时候 返回的 right +1 = letf 是与target最近的 大的元素下标。

比较好好理解的 即 吧区间分为 [left, mid ] mid [mid + 1 , right]

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        //当left==right,区间[left, right]依然有效,所以用 <=
        while (left <= right) {
             // 防止溢出 等同于(left + right)/2 因为 在力扣上大部分题目 都会取到int 的整数上线部分 2^32 -1 
            int mid = left + ((right - left) >> 1);
            if (nums[mid] > target){
                right = mid - 1;   //这里是与 while 判断是  重点 
            }else if (nums[mid] < target){
                left = mid + 1;
            }else {
                return mid;
            }
        }
        return -1;
    }

二分法第二种写法

如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。吧区间分成[letf ,mid)

[mid ,right] 本次检查的时候不会检查上边界!!!即当只有两个元素的时候 如果 mid = 下元素角标,如果下元素不等于 那么 默认 返回

mid+1那个元素的角标 即 right不会检查。

最后如果未查到target 退出while的时候 则 right letf ,这时候 返回的是与target最近的 大的元素下标。

这个写法区间分为[letf , mid] ,[mid + 1 , right]

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length;
        //不检测left==right,即区间[left, right)依然有效,所以用 <
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (arr[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else {
                left = middle + 1; // target 在右区间,在[middle, right)中
            }
        }
        return letf;  //如果查找到 left 和 right 是一样的
    }
}

相关题目推荐 力扣

  • 35.搜索插入位置(opens new window)
    • 考前面几个方法最后放回的letf 与 right 之间的关系
  • 34.在排序数组中查找元素的第一个和最后一个位置
    • 与经典二分查找类似,只是需要找到nums[mid] = target 后需要找到前后边界
  • 69.x 的平方根
    • 如果浮点数的 几次方根,注意一下mid 要用 float 定义
  • 367.有效的完全平方数
  • 剑指 Offer II 076. 数组中的第 k 大的数字
    • 这个 也是 二分的实现 不过 mid 不等于 (left + right ) / 2 而是 mid = 快速排序一轮后 返回的下标
    • 只要是吧问题分成两个区间,不断缩小查找范围都可以考虑用到二分实现
class Solution {

    public int findKthLargest(int[] nums, int k) {

        //倒数K大 也就等于第K小
        k =  nums.length - k;
        //随便取一个数 初始化
        int key= nums[k];

        int postion = -1;
		int left = 0 , right =  nums.length-1;

		while(postion != k){
			postion = partition(nums, left, right);
            //只有 postion 比 k大的时候 对于左边排序才有意义  右边不需要排序 我们只是找
			if(postion > k ){
				right = postion -1;
				partition(nums, left, right);
			} else if (postion < k) {
				left = postion + 1;
				partition(nums , left ,right );
			} else {
                //下标为K的时候 返回key
				key = nums[k];
			}
			
		}
    return key;
    }

    // 快排 因为快排每次排序完成后都会 有一个数必定排序完成 利用这个特性进行选择
    private int partition(int[] nums, int lo, int hi) {
        int v = nums[lo];
        int i = lo, j = hi + 1;
        while (true) {
            while (++i <= hi && nums[i] < v);
            while (--j >= lo && nums[j] > v);
            if (i >= j) {
                break;
            }
            int t = nums[j];
            nums[j] = nums[i];
            nums[i] = t;
        }
        nums[lo] = nums[j];
        nums[j] = v;
        return j;
    }
}

参考链接
https://programmercarl.com/0704.%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.html#%E6%80%BB%E7%BB%93

侵删

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值