题目: 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
提示: 你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。
想法: 这个是比较经典的问题,因为数组已经排好序,所以二分查找应该是效率较高的查找算法。
基本过程如下:1、获取数组元素个数 n n n 2、在二分过程中需要有三个指针(下标,不一定是C++语言的指针),一个指向当前查找区间的左端点(例如开始时为0),一个指向查找区间的右端点(开始时为n-1),最后一个指向查找区间的二分位置,用于对比二分位置的元素和目标值 3、对比二分位置元素和目标值,如果当前值>目标值,说明目标值在当前值左侧,更新查找区间右侧指针为二分位置,反之更新查找区间左侧指针。
结束查找有两种情况:1、二分位置元素就是目标值,返回当前二分位置下标即可 2、没有查找到目标值,这个涉及到左右指针具体的意义,这里选取左右指针初始值分别为 0 , n − 1 0,n-1 0,n−1的情况,那么也就是说左右指针是在查找区间范围内的,可能是需要被访问到的,不能排除掉,那么在更新左右指针时就更新到二分位置减1或者加1的位置,因为如果二分位置不是目标值,那么是已经可以排除在查找区间外的,同时查找区间是左右端都包含的,也就是左右指针可能相等,这个时候二分位置就是左右指针的位置,对比如果不等于目标值,再次更新左右指针时,左指针会大于右指针,这时候返回-1代表没有查找到目标值。
其他细节补充:二分位置的计算不需要考虑太多,(左指针+右指针)/2即可。算法时间复杂度应该是 l o g ( n ) log(n) log(n),时间复杂度计算过程如下:一次二分查找需要对比中间指针对应元素和目标值,修改一次左指针或右指针,都是常数级别操作,假设k次二分找到了目标值,第一次在 n n n个元素内查找,第二次在 n / 2 n/2 n/2个元素内查找,以此类推,最坏的情况就是最后只剩一个元素才刚好查找到目标值,此时就是第k次查找,只剩一个元素, n / 2 k = 1 n/2^k = 1 n/2k=1,则 k = l o g 2 n k = log_2 n k=log2n,所以时间复杂度为 O ( l o g n ) O(logn) O(logn)级别。
自己写的代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int l = 0, r = n - 1;
int m;
while(l <= r) {
m=(l + r) / 2;
//找到了
if(nums[m] == target) return m;
//如果小于target,说明target在右区间,更新左指针
if(nums[m] < target) {
l = m + 1;
}else {
r = m - 1;
}
}
//查找失败
return -1;
}
};
顺利通过了
1、空间复杂度忘记计算了,因为只用了3个指针,所以是 O ( 1 ) O(1) O(1)
2、我写的对应文章讲解的左闭右闭区间的讲解
3、取中间下标的计算可以用右移一位代替,计算效率更高,平常看到的代码有人确实会直接这样写, 编译器也会把除以2优化为右移一位,这个看具体编译器的情况
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int l = 0, r = n - 1;
int m;
while(l <= r) {
m=(l + r) >> 1;
//找到了
if(nums[m] == target) return m;
//如果小于target,说明target在右区间,更新左指针
if(nums[m] < target) {
l = m + 1;
}else {
r = m - 1;
}
}
//查找失败
return -1;
}
};
我对右移这个知识点不熟悉,应该还有要注意的点,应该是只有整数才可以这样做
题目: 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
想法: 要求原地移除,且不需要考虑数组中超出新长度后面的元素,那么就遍历数组,把等于val的值通过直接覆盖移除掉即可,要覆盖就要同时用两个指针(下标),就是双指针法,两个指针同时从开头出发,不等于val的值两指针都向右移动,等于val时,一个停留在原地等待被覆盖,另一个指针继续移动,然后赋值。
考虑一些特殊情况,假设有多个连续的val值存在,慢指针动,快指针移动直至不等于val时再进行覆盖,覆盖以后慢指针和快指针均移动一次,然后继续判断。这样想的话,不管是进行覆盖操作还是双指针一起后移操作,每种情况快指针都要移动,那么循环就以快指针为条件,快指针遍历整个数组,在循环中如果慢指针不等于val值,慢指针移动一次,如果慢指针等于val值,慢指针就不动,快指针移动,那么什么时候覆盖呢?前面提到快指针不等于val值时才覆盖,所以每次循环开头先判断一次快指针是否等于val以及慢指针是否等于val,只有慢指针等于val,快指针不等于val时才进行一次覆盖,然后此时慢指针不等于val,再判断慢指针是否等于val自然让慢指针后移。
第二个问题是考虑一个特殊情况,如果数组开头就是val会怎么样,推算后不需要特殊处理步骤。
第三个问题是什么时候结束循环,是否可以提前结束循环,不可以提前结束,快指针遍历完以后才能确定所有val都被删除了,所以还是要循环遍历完。
由于慢指针每次覆盖后一定会后移,所以移除后数组的新长度就是慢指针。
综上所述,循环以快指针为条件进行,快指针一直右移,循环中进行以下操作:
1、如果慢指针此时为val,快指针不为val,进行一次覆盖操作。
2、判断当前慢指针是否为val,如果不是val,慢指针右移,如果是val,慢指针不动,等待被覆盖,通过循环右指针会不断右移,满足条件后就会进行覆盖操
作,慢指针就可以移动了。
写出来代码出现了bug,发现错误在于少考虑了一种情况,如果慢指针为val,快指针也为val,那么应该跳过本次循环。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0;
int n = nums.size();
for(int fast = 0; fast<n;fast++){
if(nums[slow]==val && nums[fast]!= val) {
nums[slow] = nums[fast];
}
if(nums[slow]==val && nums[fast]== val) continue;
if(nums[slow]!=val) slow++;
}
return slow;
}
};
然后又发现条件判断可以整合如下:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0;
int n = nums.size();
for(int fast = 0; fast<n;fast++){
if(nums[slow]==val) {
if(nums[fast]!= val) {
nums[slow++] = nums[fast];
}
else{
continue;
}
} else{
slow++;
}
}
return slow;
}
};
还是有bug,看讲解去了。
思路没什么大问题
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};
对比自己的代码和这个代码,区别在于我考虑了更多的情况,因为我自己思考的时候觉得如果慢指针不等于val值,就没必要进行覆盖操作,会浪费时间。
但是我忽略了一个问题,当出现一次val值时,进行一次覆盖后,后面每次其实都需要不停覆盖,所以没必要想那么多,仅通过快指针决定是否覆盖就可以,慢指针仅在覆盖后才需要右移。这样思路清晰很多。