34. 在排序数组中查找元素的第一个和最后一个位置
暴力解法思路:
我们通过遍历我们的数组来找到我们的目标值第一次和最后一次出现的位置,然后我们再进行返回;
暴力解法实现的代码:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.size()==0) return{-1,-1}; //处理我们的空数组;
int begin=0;
int end=0;
int count=2; //进行统计我们找到的目标元素的个数;
int i=0;
//下面是我们将数组;先顺着遍历一遍,我们如果找到了一个目标元素
//那我们再将数组反过来遍历;
while( count && i<nums.size()){
if(count==2 && nums[i]==target){
count--;
begin=i;
i=nums.size()-1;
}
while(count==1 && begin<=i){
if(nums[i]==target){
count--;
end=i;
}
i--;
}
i++;
}
if(count==2) return{-1,-1}; //处理没有找到的情况
return{begin,end};
}
};
正如我们上面说的,我们这个代码是正反来进行遍历;我们的最坏的情况是我们的目标元素在中间;
那我们的时间复杂度是O(N),并且这样我们的代码写出来,很难看,没有太多可以用于其他题目的思路;
优化:
我们要找元素,就是找一个数,那我们知道找数有一个很快的算法;二分算法,我们被别人告知二分算法的前提是我们的数组有序,但是我们这里是没有序的;因为我们题目里面虽然说是非递减;但是也存在一段相等的数据区间呀;我从我老师那里知道了二分算法还可以有更广泛的用法,我们的使用的前提就是数组的“二段性“
二段性:
这样就是二段性,我们的数据可以以一个标准,分为两段,这个就是二段性,这样我们就可以用我们的二分查找;那我们先通过一道题目回顾一下二分算法的最基本的算法思维;
二分查找:
704. 二分查找
算法的思路:
我们先固定我们的左右指针,指向我们区间的左右端点,然后我们根据我们区间中点的值来进行调整我们的区间;这个调整区间的代码很容易写出死循环所以我们要好好的讲解一下;
题目解析:
1.数组是有序的,且不重复;
2.找目标值返回下标;
代码编写的区间定义情况:
1.左闭右闭:
这个情况我们的代码编写要注意的是
循环条件是左右指针相等也要进循环,因为他也是一个有效的区间;
其次就是我们更新的时候:
如果我们中间值大于目标值;因为我们的中间值已经不符合我们的要求了,我们且是右闭的,我们右边界直接更新到中间值的左边一个位置;
如果我们中间值小于目标值;因为我们的中间值已经不符合我们的要求了,我们且是左闭的,我们左边界直接更新到中间值的右边一个位置;
2.左闭右开:
我们右指针的位置应该指向数组的长度的位置;因为我们不包含右边界;
循环条件是左右指针相等不要进循环;
其次就是我们更新的时候:
如果我们中间值大于目标值;因为我们的中间值已经不符合我们的要求了,我们且是右开的,我们右边界直接更新到中间值的位置,如果更新到左边一个位置,那我们就少判断元素了;
如果我们中间值小于目标值;因为我们的中间值已经不符合我们的要求了,我们且是左闭的,我们左边界直接更新到中间值的右边一个位置;
左闭右闭c语言版二分查找
#include<stdio.h>
int search(int* nums, int numsSize, int target) {
int left = 0, right = numsSize - 1;
int mid = 0;
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid - 1;
else return mid;
}
return -1;
}
测试左闭右闭c语言版二分查找
#include<stdio.h>
int search(int* nums, int numsSize, int target) {
int left = 0, right = numsSize - 1;
int mid = 0;
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid - 1;
else return mid;
}
return -1;
}
int main() {
int test[] = { 1,2,4,5,6 };
printf("%d",search(test, 6, 3));
return 0;
}
int search(int* nums, int numsSize, int target) {
//我们右指针的位置应该指向数组的长度的位置;因为我们不包含右边界;
int left = 0, right = numsSize ;
int mid = 0;
while (left < right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid;
else return mid;
}
return -1;
}
这个代码实现左闭右开的二分查找
c++的代码就不实现了和C语言差不多;
优化的二分查找:
好了,我们回顾完了最基本的二分查找的算法的思路,我们可以发现我们一直在干两件事情,
一个就是确定区间;还有一个就是确定中间值和我们目标值的大小关系;
这个也就是我们下面代码优化的思路,
我们可以将我们的数据分为二段来进行查找左,右端点的位置;
我们要暴露左端点,我们就要将我们的数据分为:[left,mid]完全小于target,[mid+1,right]大于等于target;
我们要暴露右端点,我们就要将我们的数据分为:[left,mid]完全小于等于target,[mid+1,right]大于target;
这样我们就完成区间的划分了;我们以后更新区间就直接按照这个标准来就行;
那下一步,更新中间值:
我们就是按照区间的特点来更新就和上面普通的二分一样的更新策略;
那这个代码需要注意的点在于:
1.循环条件:
区间可以分为3种情况
(1)有目标值:我们按照二分的思想,最后的情况就是左右指针指向同一个位置,为结果;
(2)全大于目标值;就直接判断最左边值是否为目标值;
(3)全小于目标值;直接判断最右边值是否为目标值;
所以我们循环的条件就是左右指针不相等;
2.中间值的最后落点:
我们偶数数组最后会有两个元素;
那我们更新中间值的方法也有两个:
(1)int mid=left+(right-left)/2;
(2)int mid=left+(right-left+1)/2;
在找左端点的边界就是用(1),(2)的话,我们会在 mid指向值落在数组被划分两段的右边区间的时候,死循环;
在找右端点的边界就是用(2),(1)的话,我们会在 mid指向值落在数组被划分两段的左边区间的时候,死循环;
代码实现:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.size()==0) return{-1,-1};
int begin=0;
//查找左端点
int left=0,right=nums.size()-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<target) left=mid+1;
else{
right=mid;
}
}
if(nums[left] != target) return{-1,-1};
begin=left;
int end=0;
//查找右端点
left=0;
right=nums.size()-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{-1,-1};
end=left;
return{begin,end};
}
};