【力扣分模块练习】二分查找

二分查找思路简单,但细节很搞人。个人习惯用左闭右开的区间写法,以下是模板:

class Solution {
    
    
public:
    int searchInsert(vector<int>& nums, int target) {
    
    
        int n = nums.size();
        int left = 0;
        int right = n; // 我们定义target在左闭右开的区间里,[left, right)  
        while (left < right) {
    
     // 因为left == right的时候,在[left, right)是无效的空间
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
    
    
                right = middle; // target 在左区间,因为是左闭右开的区间,nums[middle]一定不是我们的目标值,所以right = middle,在[left, middle)中继续寻找目标值
            } else if (nums[middle] < target) {
    
    
                left = middle + 1; // target 在右区间,在 [middle+1, right)中
            } else {
    
     // nums[middle] == target
                return middle; // 数组中找到目标值的情况,直接返回下标
            }
        }
        return right;// l r重叠,这里写left也可以
    }
};

69.x 的平方根 【找一个数】

大意:
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

样例:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。

思路:相当于求x方 - a = 0的解。所以就是在[0,a]之间二分法找一个数可以x * x = a;

class Solution {
    
    
public:
	int mySqrt(int a) {
    
    
        if(a == 0 || a == 1) return a;
		int n = a - 1;
		int left = 1, right = a,sqrt;
		while (left < right)  //左闭右开
		{
    
    
			int mid = left + (right - left) / 2;
			sqrt = a / mid;
			if (sqrt == mid)
				return mid;
			else if (sqrt < mid) //目标值在左区间中,此时mid一定不是目标值
				right = mid;  //右边是开区间
			else
				left = mid + 1; //左闭是闭区间,而Mid已经不是目标值了,所以要加一
		}
		return right-1;  //退出时,左右重合
	}
};

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

大意:
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。

样例:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

class Solution {
    
    
public:
	vector<int> searchRange(vector<int>& nums, int target) {
    
    
		if (nums.empty())
			return vector<int>{
    
    -1, -1};
		int lower = lower_bound(nums, target);
		int high = high_bound(nums, target);

		if (lower == nums.size() || nums[lower] != target) //没有赋值的情况
			return vector<int>{
    
    -1, -1};

		return vector<int>{
    
    lower, high-1};
	}
	
	int lower_bound(vector<int>& nums,int target)
	{
    
    
		int l = 0, r = nums.size();
		while (l < r)
		{
    
    
			int mid = l + (r - l) / 2;
			if (nums[mid] < target)
				l = mid + 1;
			else if (nums[mid] >= target)  //等于也要继续往左边找
				r = mid;
		}
		return r;  //左闭右开写法,最后 跳出来l == r所以这里写l,r都可以
    }
    
	int high_bound(vector<int>& nums, int target)  //寻找第一个大于target的数
	{
    
    
		int l = 0,r = nums.size();
		while (l < r)
		{
    
    
			int mid = l + (r - l) / 2;
			if (nums[mid] > target)
				r = mid;
			else
				l = mid + 1;  //如果相等也要往右边找,因为要找到第一个大于Target的数
		}
		return r;
	}
};

后面的区间问题,常用找下界函数来做,以下是本题引申出的模板:
不知道要不要写等号的时候,想极端情况如{1,1,1,1,1},你找下界(第一个大于等于的数)肯定等于的情况往左找啊。

	int low_bound(vector<int>& vec,int target) //注意这里是寻找第一个大于或等于target的数字,即上题中的Lowbound函数
	{
    
    
		int l = 0, r = vec.size()-1; //这里减一是有助于下面return那里的判断,免得越界。不写减一,下面判断也要改成r == vec.size()
		while (l < r)
		{
    
    
			int mid = l + (r - l) / 2;
			if (vec[mid] >= target)
				r = mid;  //仅剩一个元素的情况下,右指针往左移
			else
				l = mid + 1;
 		}
		return (vec[r] >= target)? r:-1;  //如果小于说明target太大,没找到
	}

33. 搜索旋转排序数组 【旋转数组】

题意:给你一个整数数组 nums ,和一个整数 target 。
该整数数组原本是按升序排列,但输入时在预先未知的某个点上进行了旋转。(例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。

和下面那题的差别就是这题不允许重复。

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

			if (nums[mid] <= nums[right-1]) //右区间有序  注意这里一定要有等于号
			{
    
    
				if (target > nums[mid] && target <= nums[right-1])
					left = mid + 1;
				else
					right = mid;
			}
			else //左区间有序
			{
    
    
				if (target >= nums[left] && target < nums[mid])
					right = mid;
				else
					left = mid + 1;
			}
		}
		return -1;
	}
};

81. 搜索旋转排序数组 II 【旋转数组】

大意:假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。

即给的数组是由某个递增数组平移变形而来的。

输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true

思路:二分时,先找确定的区间。如确定右边是有序的,而且target又可以落入区间内,就先去右边找target。在这里要注意,target必须在有序的范围内 (这里上届下届都要写,因为如果不写,可能会漏查)。如果不在,就要到另一半区间去尝试二分寻找。
如给数组[3,1],和target = 3;
一开始判断1是有序的递增数列,如果不写上界,就会直接结束程序,然而3是在mid的左边。

class Solution {
    
    
public:
	bool search(vector<int>& nums, int target) {
    
    
		int left = 0, right = nums.size();
		while (left < right)
		{
    
    
			int mid = left + (right - left) / 2;
			if (nums[mid] == target)
				return true;
			
			if (nums[mid] == nums[left])  //没法知道有序无序
				++left;
			else if (nums[mid] > nums[left]) //中间大于最左边,说明左边有序
			{
    
    
				if (nums[mid] > target && target >= nums[left]) //target落入左边且大小合法
					right = mid;								
				else																			
					left = mid + 1;		
			}
			else
			{
    
    
				int end = (right == nums.size() ? nums.size() - 1 : right);
				if (nums[mid] < target && target <= nums[end])//target落入右边且合法
					left = mid + 1;						// 如输入[3, 1], target = 3
				else
					right = mid;	//落入右边但大小不合法,说不定在左边可以找到,所以要去尝试一下左边
			}
		}

		return false;
	}
};

153. 寻找旋转排序数组中的最小值【旋转数组变型】


唯一注意一点,注意r的初始写法。这样写方便比较,可以把MID正好是最小值的情况纳入下一步的寻找区间内。

class Solution {
    
    
public:
	int findMin(vector<int>& nums) {
    
    
		int l = 0, r = nums.size()-1;//任是左闭右开,这里只不过方便下面的比较
                                    //这样可以让mid正好是最小值的时候,也被纳入寻找区间
		while (l < r)
		{
    
    
			int mid = l + (r - l) / 2;
			//最小值必不可能在有序部分
			if (nums[mid] < nums[r]) //右边有序
			{
    
    
				r = mid;
			}
			else
			{
    
    
				l = mid+1;
			}
		}

		return nums[l];
	}
};

154. 寻找旋转排序数组中的最小值 II【旋转数组变型】

题目:
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
注意数组中可能存在重复的元素。

样例:
输入: [3,1,5]
输出: 1

输入: [2,2,2,2]
输出: 2

本题核心思想:最小值一定不在判断为有序的那一部分内。(因为数组是旋转平移得来的,所以最大和最小的交接是无序的,所以最小的那个数就在这个无序区间内)。

一开始没想明白,自己写的版本:
可以发现主要的坑点就是一直重复的那个数就是最小值,在没法判断有序区间的时候,不知道如何处理了

class Solution {
    
    
public:
	int findMin(vector<int>& nums) {
    
    
		int n = nums.size();

		int left = 0, right = n;

		int MIN = 99999;
		while (left < right)
		{
    
    
			int mid = left + (right - left) / 2;
			if (nums[mid] == nums[left])  //没法判断哪边有序
			{
    
    
				if (left == right - 1 && nums[left] < MIN)  //如果是最小区间了
					MIN = nums[left]; //单独判断一下
				++left;
			}
			else if (nums[mid] > nums[left]) //左边有序
			{
    
    
				if (nums[left] < MIN) //左边最小的数是Left
					MIN = nums[left];
				left = mid + 1;
			}
			else  //右边有序
			{
    
    
				if (nums[mid] < MIN) //右边最小的数是mid
					MIN = nums[mid];
				right = mid;
			}
		}

		return MIN;
	}
};

理清思路后,发现了本题核心思想。这样在没法判断有序区间的时候把right减一,缩小这个区间。
这里出现了第二个坑点,最好是用right-1(左闭右开写法)指向的数来判断有序区间,因为如果用left来做,在无法判断的情况下要写left++,这样很容易数组越界。

为什么总喜欢和右边比?
因为(l+r)/2是向下取整,也就是说,假如 l=0, r=1, 那么mid = 0, 这样就不能使用mid-1进行比较。反正之后都和右边比就完事了。
(在下一题中十分明显,所以养成习惯都和右边比不容易出错,右边初始为 vec.size() -1)

此外左闭右开也可以写成 right = nums.size() -1的。这样可以方便当前区间最右边的那个数的判断。

class Solution {
    
    
public:
	int findMin(vector<int>& nums) {
    
    
		int n = nums.size();
		int left = 0, right = n-1; //这里仍然是左闭右开,只不过为了判断区间方便
								//决定是否是开闭是根据while里面是否有等于号,以及r,l的移动方式来的

		/*最小值必定不在有序的那部分,所以每次都要去无序的部分寻找*/
		while (left < right)
		{
    
    
			int mid = left + (right - left) / 2;
			if (nums[mid] > nums[right])  //左边有序
				left = mid + 1;
			else if (nums[mid] < nums[right])  //右边有序
				right = mid;
			else   //无法判断左还是右有序
				right--;

		}	
		return nums[left];
	}
};

540. 有序数组中的单一元素 【找第一个不重复的数字】


大意:给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。

样例:
输入: [1,1,2,3,3,4,4,8,8]
输出: 2

本题核心思想:
如何判断要去左边还是右边呢?就是通过左边区间size的奇偶来判断。这样相当于在数组之中一次删除两个相同的元素,并且分区间判断。
同时,左闭右开只是left < right描述区间为1的时候是固定的,否则在讨论low和high的移动的时候,总是死卡着这点容易数组越界。

class Solution {
    
    
public:
	int singleNonDuplicate(vector<int>& nums) {
    
    
		int low = 0, high = nums.size() - 1;

		while (low < high)
		{
    
    
			int mid = low + (high - low) / 2;
			bool BehindMidEven = (high - mid) % 2 == 0; //mid后的部分是否是偶数

			if (nums[mid + 1] == nums[mid]) //mid后面那个数和Mid这个相同
			{
    
    
				if (BehindMidEven) //后面那部分不相同的部分是奇数了
					low = mid + 2;
				else
					high = mid - 1;
			}
			else if (nums[mid - 1] == nums[mid])//mid前面的数和Mid相同
			{
    
    
				if (BehindMidEven)
					high = mid - 2;
				else
					low = mid + 1;
			}
			else
				return nums[mid];
		}
		return nums[low];
	}
};

162. 寻找峰值【二分找峰值】


峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞。

样例:
输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。

思路:
用当前Mid的元素和mid+1的元素相比,如果小于则mid当前属于一个递减区间。这样peak必然在mid的左边区间(这个新的查找区间包含了Mid本身)。去左边区间进行新一轮二分查找。
此外,因为(l+r)/2是向下取整,也就是说,假如 l=0, r=1, 那么mid = 0, 这样就不能使用mid-1进行比较。反正之后都和右边比就完事了。

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

		while (l < r)
		{
    
    
			int mid = l + (r - l) / 2;
			if (nums[mid] > nums[mid + 1]) //若成立说明Mid在一个递减区间内,peak在其左边
				r = mid;
			else
				l = mid + 1;
		}
        return l;
	}
};

猜你喜欢

转载自blog.csdn.net/abyss_miracle/article/details/110849064
今日推荐