704.二分查找
二分查找着重关注几点
- 如何定义区间
- 如何定循环条件
- 如何缩减搜索区间
第一种方式:左闭右闭方式定义区间
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1; //因为是左闭右闭区间
while (left <= right) { //因为是左闭右闭区间
int middle = left + (right -left) / 2;
if (nums[middle] > target)
{
right = middle - 1; //因为是左闭右闭区间
}
else if (nums[middle] < target) {
left = middle + 1;
}
else if (nums[middle] == target)
return middle;
}
return -1;
}
};
所谓左闭右闭方式定义区间,即按照如下方式描述区间
这样能够通过[left, right]来描述数组中的元素。因此,初始化right应该等于size - 1,这样才能通过左闭右闭方式描述
如何判定循环条件?即:区间中只要还有元素就要一直循环。很容易想到while (left < right),区间中肯定是有元素的。在这里,因为按照左闭右闭方式定义的区间,当left = right时,[left, right]区间中也有元素,故也需要进循环体。故while中条件应该为 while (left <= right)
如何缩减搜索区间?关键是缩减后的区间应该尽可能排除掉不可能的区间。左闭右闭区间中,right要赋值为middle - 1,因为当前这个middle一定不是target,接下来要查找的区间范围希望不包含当前这个middle
第二种方式 :左闭右开方式定义区间
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, 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 if (nums[middle] == target)
return middle;
}
return -1;
}
};
所谓左闭右开方式定义区间,即按照如下方式描述区间
这样能够通过[left, right)来描述数组中的元素。因此,初始化right应该等于size,这样才能通过左闭右开方式描述
如何判定循环条件?即:区间中只要还有元素就要一直循环。很容易想到while (left < right),区间中肯定是有元素的。在这里,和左闭右闭方式定义区间不同的是,当left = right的时候,区间[left, right)中已经没有元素了,因此不需要进循环体。故while中的条件就是while (left < right)
如何缩减搜索区间?关键是缩减后的区间应该尽可能排除掉不可能的区间,也不能漏掉可能的区间。左闭右开区间中,right赋值为middle即可,因为当前这个middle一定不是target,接下来要查找的区间范围[left, right)本来就不包含当前这个middle,这里和左闭右闭区间也是不一样的。如果这里也设置成right - 1了,那么会漏掉搜索middle前一个元素
27. 移除元素
暴力解法
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; ++i) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; ++j) {
nums[j - 1] = nums[j];
}
i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
需要注意i--的操作和size--的操作,主打一个不漏遍历元素,也不多遍历元素。如果没有i--操作,那么
这种两层for循环,时间复杂度O(n^2) ,不妥
另一种解法:双指针法
这种方法能够以O(n)时间复杂度实现移除元素,一层for循环即可
关键理解快指针和慢指针是干什么用的
- 慢指针用于指向新数组需要更新的位置
- 快指针用于遍历原数组,并寻找满足条件的作为新数组中的元素
只不过这里的逻辑上的新数组原数组都是在一个数组上操作
哪里体现移除元素了?在快指针指向的元素为非待删除值时,才将其赋值给慢指针所指向的位置。(即只把不等于待删除值的元素添加到新数组中,就相当于移除元素了)
看代码
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0, fast = 0;
while (fast < nums.size()) {
if (nums[fast] != val) { //在快指针指向的元素不为待删除值时
nums[slow] = nums[fast]; //才将其赋值给慢指针所指向的位置
slow++; //慢指针向后移动,表示新数组中下一个待填充位置
}
++fast;
}
return slow;
}
};
主打一种思想:快指针往前找,看看哪些元素是符合条件的(这里是不等于val值的),然后把符合条件的填充进慢指针所指位置,然后慢指针指向下一个待填充位置
注意几个点:
- 为了找到所有能够填入新数组的值,fast应遍历完原数组,故while (fast < size)
- fast每次循环都要往后移动,fast++,因为fast一直在找
一种简化的实现
// 时间复杂度: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;
}
};
回顾总结
二分查找题目应该熟悉两种区间定义方式以及连带影响
移除元素中应该掌握快指针和慢指针分别是干什么的