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),代码中没有使用额外的存储空间,只使用了有限的几个变量。
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!