二分查找的思想

先看看leetcode两道相关的题目,都是二分思想的应用。
leetcode 35 https://leetcode-cn.com/problems/search-insert-position/
leetcode 34 https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/

二分思想用一句话概况就是“一看就懂,一写就废”,花了很长时间都没有弄得懂,每个人都是不同的写法,新手很难从中发现规律。
其实二分查找最重要的一句话就是“查找范围”,如何确定查找范围?只要确定了查找范围,二分查找其实都是一个套路。而二分查找大致分为以下三种:

①普通二分,也就是给定一个排序数组nums和目标值target,在nums中查找target,如果找到就返回下标。写法也非常简单:

int left = 0;
int right = nums.size() - 1;
while(left <= right){    //查找范围[left, right]
	int mid = left + (right - left) / 2;
	if(nums[mid] < target){
		left = mid + 1;  //查找范围缩小为[mid+1, right]
	} else if(nums[mid] > target){
		right = mid - 1; //查找范围缩小为[left, mid-1];
	} else if(nums[mid] == target){
		return mid;
	}
}
return -1;

初学建议不要写else,一步一步else if分析会更容易理解其中的“查找范围”如何思考。
下面两种是二分查找的变种,比基本二分要难一点。由于需要确定边界,所以不能一找到nums[mid] == target就返回,而应该继续缩小查找范围,不断逼近边界。

②确定左侧边界的二分查找
需求:给定一个排序数组nums和一个目标值target,在nums中找到target出现的最左侧位置,返回该下标。先给出代码再作解释:

int left_bound(vector<int> nums, int target){
	if(nums.size() <= 0){
		return -1;
	}
	int left = 0;
	int right = nums.size();
	while(left < right){     //查找范围为[left, right)
		int mid = left + (right - left) / 2;
		if(nums[mid] > target){
			right = mid;     //查找范围缩小为[left, mid)
		} else if(nums[mid] < target){
			left = mid + 1;  //查找范围缩小为[mid + 1, right)
		} else if(nums[mid] == target){
			right = mid;     //注意这里!right往左靠, 查找范围为[left, mid)
		}
	}
	if(left == nums.size() || nums[left] != target){
		return -1;           //target比nums所有数都大,那么就找不到了
	}
	return left;             //否则此时left就是左边界
}

注意到上面的代码,首先确定查找范围!这篇文章我全部采用左闭右开区间的查找范围,当然也可以左闭右闭。
而right一开始为什么是nums.size()而不是nums.size() - 1呢?这是因为查找范围为[left, right)左闭右开,为了确保所有的数都能被用到,所以right = nums.size(); 这样就可以取到nums[nums.size() - 1],也即最后一个数了。
好了,上面说完查找范围的确定,那确定左边界就是不断往左逼近了。简单地说,就是区间尽量往左靠,不断往左边缩小区间,具体的体现就是当 nums[mid] == target的时候,并不是直接返回,而是把right = mid; 把查找区间往左边缩小为[left, mid),因为mid已经被我们用过了(也就是已经和target比较过了),而右区间又是开区间,所以right = mid而非mid - 1!
那为什么left = mid + 1呢?这是因为我们左区间是闭区间,当mid被用过之后,下一个区间就是mid + 1开始了。
到了这里,相信大家对思路已经明白了,最后就是循环退出的时候,进行异常情况的判断,循环退出的时候,left == right。
此时,有两种情况:1. 找到了左边界。 2.找不到左边界。
找不到左边界是什么情况呢?要么left = nums.size()超出数组下标(target比所有nums的数都大),要么nums[left] != target,至此结束。

③确定右侧边界的二分查找
需求:给定一个排序数组nums和一个目标值target,在nums中找到target出现的最右侧位置,返回该下标。
还是先给出代码:

int right_bound(vector<int> nums, int target){
	if(nums.size() <= 0){
		return -1;
	}
	int left = 0;
	int right = nums.size();
	while(left < right){     //查找范围为[left, right)
		int mid = left + (right - left) / 2;
		if(nums[mid] > target){
			right = mid;     //查找范围缩小为[left, mid)
		} else if(nums[mid] < target){
			left = mid + 1;  //查找范围缩小为[mid + 1, right)
		} else if(nums[mid] == target){
			left = mid + 1;     //注意这里!left往右靠, 查找范围为[mid + 1, right)
		}
	}
	return right - 1;         //注意这里的返回值
}

分析思路同上:既然要找最右边界,那么left就要不断往右靠,查找范围同样是[left, right),相信大家已经可以自己分析出来了。
那么最终的返回值是什么呢?我们知道,循环退出的时候left ==rigiht,由于有可能出现在最后一个数,所以我们返回right - 1(left - 1也可以)。这里是一个小细节,因为上面找到target并缩小区间时,是这样写的

else if(nums[mid] == target){
		left = mid + 1;     //注意这里!left往右靠, 查找范围为[mid + 1, right)
}

所以真正的mid = left - 1(mid就是我们找到的目标值下标),这也是上面返回left的原因。

到此,大家可以去试一试上面两道题,采用这思路解决,会在O(logn)复杂度解决。下面是我写的代码:

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

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size() <= 0){
            return vector<int>{-1, -1};
        }
        vector<int> res(2, -1);
        int left = 0;
        int right = nums.size();
        while(left < right){        //搜索区间[left, right)
            //寻找左边界, right左靠
            int mid = left + (right - left) / 2;
            if(nums[mid] > target){
                right = mid;
            } else if(nums[mid] < target){
                left = mid + 1;     //mid已经被搜索过了, 下一个[mid + 1, right)
            } else if(nums[mid] == target){
                right = mid;        //right左靠
            }
        }
        //异常情况判断: 比数组所有数都大 / 不存在target
        if(left == nums.size() || nums[left] != target){
            return res; 
        }
        res[0] = left;              //确定左边界

        left = 0;
        right = nums.size();
        while(left < right){        //搜索区间[left, right)
            //寻找右边界, left左靠
            int mid = left + (right - left) / 2;
            if(nums[mid] > target){
                right = mid;        //mid已经被搜索过了, 右开区间, 所以 [left, mid)
            } else if(nums[mid] < target){
                left = mid + 1;     //mid已经被搜索过了, 下一个[mid + 1, right)
            } else{
                left = mid + 1;     //left右靠
            }
        }
        if(nums[left - 1] != target){
            return res;
        }
        res[1] = left - 1;          //注意, 因为搜索到mid的时候, left = mid + 1
        return res;
    }
};

35. 搜索插入位置

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) 
    {
        if(nums.size() <= 0){
            return 0;
        }
        int left = 0;
        int right = nums.size();
        while(left < right){  //搜索区间[left, right)
            int mid = left + (right - left) / 2;
            if(nums[mid] < target){
                left = mid + 1;
            } else if(nums[mid] > target){
                right = mid;
            } else{
                return mid;
            }
        }
        return left;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值