题目一:LeetCode 704.二分查找
一、按照自己的思路求解
这个题目比较简单,直接遍历就可以了(虽然题目就是二分查找,但是最容易的还是直接遍历哈哈),因为元素不会重复,所以找到目标值target之后直接返回其下标即可。代码实现如下:
//cpp
class Solution {
public:
int search(vector<int>& nums, int target) {
size_t size = nums.size();
for(int i=0;i<size;i++)
{
if(nums[i] == target)
return i;
}
return -1;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
二、参考代码随想录的求解方法
题目中有一个很重要的信息:有序数组,也是本题能够用二分查找的重要前提。另外还有一点是无重复,如果有重复的元素但有序的话二分查找得到的结果可能不是唯一的。
(一)二分查找写法一
定义一个左闭右闭的区间[left,right],这种情况下因为左右边界都是在查找范围内,因此循环条件是left <= right,包含等号,当left == right是,查找区间就剩下一个数。代码实现如下:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size()-1;
int ret = -1;
while(left <= right)
{
//int middle = (left + right) / 2;
int middle = left + (right - left) / 2;
if(nums[middle] > target)
right = middle - 1;
else if(nums[middle] < target)
left = middle + 1;
else
{
ret = middle;
break;
}
}
return ret;
}
};
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
参照代码随想录给出的代码,在计算 middle 的值时用middle = left + (right - left) / 2;
代替middle = (left + right) / 2
来防止溢出,但就本题而言不用这么做也是能通过的。
代码随想录给出的代码如下:
// 版本一
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
(二)二分查找写法二
定义左闭右开的区间[left,right),因为当left == right的时候区间中已经没有数据了,所以循环的条件是left < right。代码实现如下:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
while(left < right)
{
int middle = left + (right - left) / 2;
if(nums[middle] > target)
right = middle;
else if(nums[middle] < target)
left = middle + 1;
else
return middle;
}
return -1;
}
};
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
小结
- 注意使用二分法的条件:有序,另外需要注意元素是否重复,如果重复得到的结果可能不唯一。
- 注意查找区间的定义,区间的定义会影响到查找时的循环条件,个人更喜欢左闭右闭,当然不必死记硬背,主要理解什么时候查找的区间内没有数据,比如左闭右闭的区间内,当left > right时,区间内就没有数据了,因此循环的条件是 left <= right,同理,左闭右开也是一样的道理。
- 另外需要注意每次如何更新区间,在左闭右闭情况下, left = mid + 1或者right = mid - 1,如果少了减1或者加1则会导致边界值重复判断,更主要的是会导致死循环。
题目二:LeetCode 27.移除元素
一、按照自己的思路求解
可以用最简单粗暴的方法求解,即暴力求解,两层循环,外层循环遍历数组,内层循环更新数组。注意,不能使用额外的空间,可以打乱原来的顺序。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
size_t size = nums.size();
for(int i=0;i<size;i++) //注意这里不能用nums.size(),必须用size,size是会变化的
{
if(nums[i] == val)
{
//移动(更新)数组
for(int j=i;j<size-1;j++)
nums[j] = nums[j+1];
--size; //更新数组长度
--i; //原来i指向的位置存入了新的数据,所以要重新判断
}
}
return size;
}
};
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
自己在写的时候第一次只通过了部分测试用例,问题是缺少了--i
这条语句,因为如果移动数组之后原来i指向的位置放入了新的数据,因此要对这个位置的数据进行判断,所以要加入--i
这条语句,再在for语句中加1,最终的结果就是i的值不变。另外,移除(准确地说覆盖)了一个数据之后要更新数组的长度。
二、参考代码随想录的思路求解
代码随想录中给出两种方法:暴力求解和双指针法,暴力求解已经实现,这里补充双指针法。
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环内完成两个for循环的工作。个人理解,就本题而言,一个指针遍历数组,另一个指针指向下一个存储位置。代码实现如下:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
size_t size = nums.size();
int j = 0;
for(int i=0;i<size;i++)
{
if(nums[i] != val)
nums[j++] = nums[i];
}
return j;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
相比暴力求解,这种方法时间复杂度更低,更简洁。
代码随想录给出的代码如下:
// 时间复杂度: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;
}
};
小结
本题用暴力求解也能通过,但暴力求解的一大缺点是时间复杂度高,双指针法是应该掌握的一种方法,相比于快慢指针的说法,就本题而言,个人觉得一个指针遍历数组,另一个指针指向下一个i存储位置的说法更容易理解。