LeetCode704.二分查找
二分查找的难点在于边界处理
- while循环条件是
left < right
还是left <= right
- 更新区间:
right = middle
还是right = middle-1
边界处理的关键在于定义搜索区间,有两种常见的区间定义:左闭右闭[left, right]、左闭右开(left, right]。这些区间的定义是自己设定的,在while循环中每一次边界的处理都要坚持根据区间的定义来操作,即如果设定寻找区间为[left, right],则每次循环的搜索区间都是[left, right],我们每次在这个区间中寻找target元素,这就是循环不变量规则
左闭右闭
- while循环条件,搜索区间被定义为左闭右闭的,允许出现类似[1,1]的搜索区间,因此
left<=right
是合法的 - 如果nums[middle] > target,则需要更新right。更新时要坚持区间的左闭右闭原则,不能把已经确定不是target的元素(这次循环确定nums[middle]不是target)放到下一个循环的搜索区间中继续寻找,因此right = middle-1,不能包含middle处的元素
- 同理,如果nums[middle] < target,则需要更新left,left = middle+1,不能将middle处的元素包含在下一个搜索区间中
class Solution
{
public:
// 左闭右闭
int search(vector<int> &nums, int target)
{
int front = 0;
int back = nums.size() - 1;
while (front <= back)
{
int mid = front + (back - front) / 2;
if (nums[mid] < target)
{
front = mid + 1;
}
else if (nums[mid] == target)
{
return mid;
}
else
{
back = mid -1;
}
}
// 只要数组内有target元素,必定会在while循环中找到并return
return -1;
}
};
左闭右开
- while循环条件,搜索区间定义为左闭右开的,类似[1,1)这样的区间是不合法的,所以循环条件为
left < right
- 如果nums[middle] > target,更新right。middle处的元素不是target,而区间定义为左闭右开,区间不包含右边界的元素,因此middle处的元素可以包含在下一个搜索区间中,
right = middle
- 如果nums[middle] < target,更新left。middle处的元素不是target,而区间定义为左闭右开的,因此middle处的元素不能包含在下一个搜索区间中,
left = middle+1
- 初始时,right = nums.szie()而不是nums.szie()-1,因为区间被我们定义为左闭右开的,是不包含right处的元素的
// 版本二
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在[middle + 1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
LeetCode27.移除元素
数组是一个内存上连续存放的元素集合,“删除数组的某个元素”实际上是把要删除的元素覆盖掉。
vector的erase函数用来删除某个元素,erase是一个O(n)的操作,它将数组整体前移,覆盖掉要删除的元素,完成删除
暴力解法
这也是我一开始想的方法,两层for循环,时间复杂度O(n2)
- 外层for循环:遍历整个数组
- 内层for循环:当外层循环遍历到某一个val元素时,内层for循环从这个val元素的后一个元素开始,将数组中各个元素向前移动一位,覆盖掉val元素
注意:内层循环执行后,要有size--,i--(外层循环的计数器)
。原因:删除一个元素后需要size-1;同时,因为下标i
以后的元素都向前移动了一位,所以i
也向前移动一位
// 解法一:暴力解法
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];
}
size--; // nums整体向前移动一位,size-1
i--; // 要检验刚刚移过来的元素是否为val
}
}
return size;
}
};
双指针解法
O(n)时间就能移除元素
使用双指针法,关键在于明确两个指针的含义,它们的任务是什么。这里定义快慢指针的作用:
-
快指针:遍历整个数组,挑出非val的元素,这些元素将组成新数组,新数组就是不含有val元素的数组
-
慢指针:当前新数组的下标。这个指针实际上在收集非val的元素,遍历形成新数组:它将快指针挑出的非val的元素放到自己指向的位置,然后再向前移动一位,等待收集下一个非val的元素
整个过程可以理解为:快指针在探测,获得新数组中的元素;慢指针在收集,是新数组的下标。二者同时在同一个数组上操作
- 快指针遍历整个数组,挑出非val元素,然后慢指针紧随其后,将快指针挑出的非val元素放到自己指向的位置,二者一起向前移动一位
- 如果快指针指向一个val元素,则慢指针不动,快指针继续向前移动,直至找到一个非val元素,然后将这个元素赋给慢指针指向的位置,二者再同时向前移动一位。这样的过程保证了慢指针左边的所有元素都是非val的元素
// 解法二:快慢指针
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
// 快指针遍历整个数组,慢指针指向更新,覆盖所有等于val的元素
int slow = 0;
for(int fast=0; fast<size; ++fast)
{
if(val != nums[fast])
{
nums[slow++] = nums[fast]; // fast指向非val元素,将这个值赋给slow指向的位置。if更新了新数组的元素
}
}
return slow;
}
};
总结
这两道题都是比较简单的。第一道题涉及到二分查找,看了Carl的视频学到了边界处理问题,这是我原来一直都不太清楚的。第二道题涉及到数组的删除操作,快慢指针法是很精巧的思想,这种方法在数组和链表的操作中是非常常见的。