704. 二分查找
给定一个
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
中的所有元素是不重复的。
n
将在[1, 10000]
之间。
nums
的每个元素都将在[-9999, 9999]
之间。
首先我们定义区间[left,right]
,保证[0,left-1]
区间里面的数一定小于target
,[right+1,n-1]
区间的数一定大于target
。
也就是规定left
左边的数全都小于target
,right
右边的数全都大于target
。
然后一直维护这个定义,直到[left,right]
区间只有一个长度单位,或者right+1==left
的时候。
如果[left,right]
区间只有一个长度单位的时候,此时判断这个数是否等于target
即可。
如果right+1==left
,说明找不到等于target
的数。
为什么会出现right+1==left
的情况?这种情况只可能出现mid==left
或者mid==right
的时候。也就是区间长度为2
,left+1==right
的时候。
此时mid==left
,更新right=mid-1
就可以发生right+1=left
。
所以我们维护的一直是left
和right
的含义,left
的含义是[0,left-1]
区间的数全部小于target
,right
的含义是[right+1,n-1]
区间的数全部大于target
。
class Solution {
public:int search(vector<int>& nums, int target) {// 维护区间[left,right]可能存在目标元素[0,left][right+1,n-1]一定不存在目标元素// 使得区间收敛于一个元素,然后只需要判断这一个元素是否等于target即可int left = 0, right = nums.size() - 1;while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid - 1;} else {return mid;}}if (left == right && nums[left] == target)return left;elsereturn -1;}
};
这段代码定义了一个名为 Solution
的类,其中包含一个名为 search
的成员函数。这个函数的目的是在一个有序的整数数组 nums
中查找目标值 target
的索引。
int search(vector<int>& nums, int target) {
定义了 search
函数,它接受一个整数向量 nums
和一个整数 target
作为参数,并返回一个整数,表示 target
在 nums
中的索引,如果 target
不存在于 nums
中,则返回 -1
。
int left = 0, right = nums.size() - 1;
初始化两个指针 left
和 right
,分别指向数组的起始索引和结束索引。
while (left < right) {
使用二分查找,当 left
小于 right
时,执行循环。
int mid = left + (right - left) / 2;
计算中间索引 mid
。
if (nums[mid] < target) {
如果中间元素的值小于 target
,说明 target
在中间元素的右侧。
left = mid + 1;
将 left
指针移动到 mid + 1
。
} else if (nums[mid] > target) {
如果中间元素的值大于 target
,说明 target
在中间元素的左侧。
right = mid - 1;
将 right
指针移动到 mid - 1
。
} else {return mid;}}
如果中间元素的值等于 target
,返回中间索引 mid
。
if (left == right && nums[left] == target)return left;
最后,当 left
和 right
重合时,检查 left
指向的元素是否等于 target
,如果是,则返回 left
。
else return -1;}
如果 left
指向的元素不等于 target
,返回 -1
。
时间复杂度和空间复杂度分析
时间复杂度:O(log n),其中 n
是数组 nums
的长度。二分查找的时间复杂度为对数级别。
空间复杂度:O(1),代码中没有使用额外的存储空间,只使用了有限的几个变量。
上面的这种解法不是特别的好,因为我们并不是把nums数组分成两个部分,而是分成了三个部分,[0,x-1][x][x+1,n-1],[0,x-1]全都是小于target的数,x是等于target的数,[x+1,n-1]全都是大于target的数。但是并不是每一个nums都可以还分成这三个部分,有可能并不存在x的部分。
此时我们希望的是left和right都在x这个位置相遇,规定[0,left-1]全都是小于target的数,规定[right+1,n-1]都是大于target的数。当x不存在的时候,就会出现right+1==left的情况。
如果我们可以将nums数组精准的分成两个部分,就一定可以让left和right一定在某个位置相遇。
因此我们可以将nums划分成这样的两个部分,[0,x][x+1,n-1],[0,x]是小于等于target的数,[x+1,n-1]是大于target的数。我们希望left和right趋近x这个位置,所以规定[0,left]全都是小于等于target的数,[right+1,n-1]全都是大于target的数。
class Solution {
public:int search(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left < right) {int mid = left + (right - left + 1) / 2;if (nums[mid] <= target) {left = mid;} else if (nums[mid] > target) {right = mid - 1;}}if (nums[left] == target)return left;elsereturn -1;}
};
这段代码有一个小细节,就是mid
的取值,我们可以写成mid=left+(right-left)/2
,也可以写成mid=left+(right-left+1)/2
的形式。区别就是当left+1==right
的时候,前者算出来的mid
等于left
,后者算出来的mid
等于right
。
也可以写成mid=(left+right)/2
的形式,这种形式有可能发生溢出,left+right
有可能是一个很大的数,然后发生溢出。但是mid=left+(right-left)/2
或者mid=left+(right-left+1)/2
都不会发生移除,因为这是加法减法的运算。
这个证明也很简单,当left+1==right
的时候,带入mid=left+(right-left)/2
中得到mid=left+1/2=left
,如果mid=left+(right-left+1)/2,
此时mid=left+2/2=right
。
上述代码不可以写成mid=left+(right-left)/2
的形式,因为如果left+1==right
的时候,mid==left,left==mid
会发生死循环。
34. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组
nums
,和一个目标值target
。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值
target
,返回[-1, -1]
。你必须设计并实现时间复杂度为
O(log n)
的算法解决此问题。示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]
示例 3:
输入:nums = [], target = 0 输出:[-1,-1]
提示:
0 <= nums.length <= 10(5)
-10(9) <= nums[i] <= 10(9)
nums
是一个非递减数组
-10(9) <= target <= 10(9)
left=mid+1,right=mid,
此时可能出现right+1=left
的情况吗?如果要出现这种情况,left+1==right
,并且mid==right
,运行left=mid+1
就会发生这种情况。但是我们维护的意义是,[0,left-1]
全都是小于target
的数,[right,n-1]
全都是大于等于target
的数,此时如果出现了right+1==left
,那么right
这个数即小于target
又大于等于target
,显然不可能。所以不可能发生这种情况。
第二个while
循环同理。
寻找第一个出现的target
数的解题思路就是维护一个区间意义,[0,left-1]
全都是小于target
的数,[right,n-1]
全都是大于等于target
的数。
最后[left,right]
区间缩小到一个长度单位,判断这个数是否等于target
即可。
寻找第最后一个出现的target
数的解题思路就是维护一个区间意义,[0,left]
全都是小于等于target
的数,[right+1,n-1]
全都是大于target
的数。
最后[left,right]
区间缩小到一个长度单位,判断这个数是否等于target
即可。
我们的最终目的就是想办法让我们的目标值趋近于一个区间长度。然后维护[left,right]
区间外面的意义。
class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {if (nums.empty())return {-1, -1};int n = nums.size();if (target < nums[0] || target > nums[n - 1])return {-1, -1};//[left,right]维护一个区间,[0,left-1]一定小于target,[right+1,n-1]大于等于target// 我们希望区间收敛于一个元素int left = 0, right = n - 1;int begin = -1;while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] >= target) {right = mid;}}if (nums[left] != target)return {-1, -1};elsebegin = left;left = 0, right = n - 1;//[left,right]维护一个区间,[0,left]一定小于等于target,[right+1,n-1]大于target// 我们希望区间收敛于一个元素while (left < right) {int mid = left + (right - left + 1) / 2;if (nums[mid] <= target) {left = mid;} else if (nums[mid] > target) {right = mid - 1;}}return {begin, right};}
};
这段代码定义了一个名为 Solution
的类,其中包含一个名为 searchRange
的成员函数。这个函数的目的是在一个有序的整数数组 nums
中查找目标值 target
的起始和结束位置。
vector<int> searchRange(vector<int>& nums, int target) {
定义了 searchRange
函数,它接受一个整数向量 nums
和一个整数 target
作为参数,并返回一个整数向量,包含 target
在 nums
中的起始和结束位置。
if (nums.empty())return {-1, -1};
如果 nums
为空,返回一个包含两个 -1
的向量,表示 target
不存在于 nums
中。
int n = nums.size();
获取数组 nums
的长度。
if (target < nums[0] || target > nums[n - 1])return {-1, -1};
如果 target
小于数组的第一个元素或者大于数组的最后一个元素,则返回一个包含两个 -1
的向量,表示 target
不存在于 nums
中。
接下来,代码使用两次二分查找,第一次查找 target
的起始位置,第二次查找 target
的结束位置。
int left = 0, right = n - 1;int begin = -1;
初始化两个指针 left
和 right
,分别指向数组的起始索引和结束索引。初始化变量 begin
为 -1
,用于存储 target
的起始位置。
第一次二分查找:查找起始位置
while (left < right) {int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;}
else if (nums[mid] >= target) {
right = mid;}}
这个循环用来找到 target
的起始位置。如果 mid
处的值小于 target
,则 target
必定在 mid
右侧;如果 mid
处的值大于或等于 target
,则可能是起始位置,或者 target
在 mid
左侧。
if (nums[left] != target)return {-1, -1};else
begin = left;
循环结束后,检查 left
指向的元素是否等于 target
。如果不是,返回一个包含两个 -1
的向量。如果是,将 begin
设置为 left
。
第二次二分查找:查找结束位置
left = 0, right = n - 1;
重新初始化 left
和 right
指针。
while (left < right) {int mid = left + (right - left + 1) / 2;
if (nums[mid] <= target) {
left = mid;}
else if (nums[mid] > target) {
right = mid - 1;}}
这个循环用来找到 target
的结束位置。与查找起始位置的循环类似,但是当 mid
处的值小于或等于 target
时,mid
可能是结束位置,或者 target
在 mid
右侧,因此更新 left
指针。
return {begin, right};
返回一个向量,包含 target
的起始位置和结束位置。
时间复杂度和空间复杂度分析
时间复杂度:O(log n),其中 n
是数组 nums
的长度。该算法使用了两次二分查找,每次查找的时间复杂度为 O(log n)。
空间复杂度:O(1),代码中没有使用额外的存储空间,只使用了有限的几个变量。
35. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为
O(log n)
的算法。示例 1:
输入: nums = [1,3,5,6], target = 5 输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2 输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7 输出: 4
提示:
1 <= nums.length <= 10(4)
-10(4) <= nums[i] <= 10(4)
nums
为 无重复元素 的 升序 排列数组
-10(4) <= target <= 10(4)
将nums
整个数组分成两个部分。[0,x][x+1,n-1]
两部分,[0,x]
部分全都是小于等于target的数,[x+1,n-1]
全都是大于target
的数。我们希望找到x
这个位置的数字,所以我们希望left
和right
相遇的地点在x
这个位置,此时规定,[0,left]
全都是小于等于target
的数,[right+1,n-1]
全都是大于target
的数。不断的维护这个意义,直到区间长度为1
为止。因为我们确确实实可以划分出这两个部分,[0,x][x+1,n-1]
,所以最后一定可以到达区间长度为1
的时候。
class Solution {
public:int searchInsert(vector<int>& nums, int target) {if (nums.empty())return 0;int n = nums.size();if (target < nums[0])return 0;if (target > nums[n - 1])return n;int left = 0, right = n - 1;while (left < right) {int mid = left + (right - left + 1) / 2;if (nums[mid] <= target) {left = mid;} else {right = mid - 1;}}if (nums[left] == target)return left;elsereturn left + 1;}
};
这段代码定义了一个名为 Solution
的类,其中包含一个名为 searchInsert
的成员函数。这个函数的目的是在一个有序的整数数组 nums
中找到目标值 target
应该被插入的位置,使得数组仍然有序。
int searchInsert(vector<int>& nums, int target) {
定义了 searchInsert
函数,它接受一个整数向量 nums
和一个整数 target
作为参数,并返回一个整数,表示 target
应该插入的索引位置。
if (nums.empty())return 0;
如果 nums
为空,则 target
应该插入在索引 0
的位置。
int n = nums.size();
获取数组 nums
的长度。
if (target < nums[0])return 0;
if (target > nums[n - 1])return n;
如果 target
小于数组的第一个元素,则应该插入在索引 0
的位置。如果 target
大于数组的最后一个元素,则应该插入在数组末尾,即索引 n
的位置。
接下来,代码使用二分查找来确定 target
的位置。
int left = 0, right = n - 1;
初始化两个指针 left
和 right
,分别指向数组的起始索引和结束索引。
二分查找
while (left < right) {int mid = left + (right - left + 1) / 2;
计算中间索引 mid
。这里加 1
是为了向上取整,防止在 left
和 right
相邻时进入死循环。
if (nums[mid] <= target) {
left = mid;} else {
right = mid - 1;}}
如果 mid
处的值小于或等于 target
,则 target
应该在 mid
或 mid
右侧;如果 mid
处的值大于 target
,则 target
应该在 mid
左侧。
if (nums[left] == target)return left;elsereturn left + 1;}
循环结束后,检查 left
指向的元素是否等于 target
。如果是,则返回 left
作为插入位置。如果不是,说明 target
应该被插入在 left
指向的元素的右侧,即返回 left + 1
作为插入位置。
时间复杂度和空间复杂度分析
时间复杂度:O(log n),其中 n
是数组 nums
的长度。该算法使用了二分查找,时间复杂度为 O(log n)。
空间复杂度:O(1),代码中没有使用额外的存储空间,只使用了有限的几个变量。
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!