代码随想录算法训练营第一天 | 704. 二分查找、27. 移除元素
704. 二分查找
文档讲解:代码随想录 | 数组 | 二分查找
视频讲解:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找
状态:第一遍一写就废,看了讲解后明白了!
题目链接:704. 二分查找
解题思路:二分查找
- 前提:数组严格升序/降序,必须无重复元素
- 两种思路:左闭右闭 [left, right] + 左闭右开 [left, right)
- 注意要点:应严格遵循循环/区间不变量原则
- 时间复杂度: O ( l o g n ) O(log n) O(logn)
- 空间复杂度: O ( 1 ) O(1) O(1)
二分法:左闭右闭
思路
- 定义查找空间: 左闭右闭写法,我们定义
target
在一个左闭右闭的区间里 [ l e f t , r i g h t ] [left, right] [left,right] 中,这个区间的定义十分重要! - 循环二分,缩窄查找区间:使用向下取整除法,定义
middle = left + ((right - left) / 2)
因为定义target
在 [ l e f t , r i g h t ] [left, right] [left,right] 区间,所以有如下几点:- 因为
l
e
f
t
=
r
i
g
h
t
left = right
left=right 有意义,所以
while (left <= right)
要使用<=
,才能保证查找区间完整 - 当
nums[middle] > target
时(target
在左区间 [ l e f t , m i d d l e ] [left, middle] [left,middle] 时),更新查找左区间右边界right
;由判断条件可知当前nums[middle]
一定不是target
,则左区间不能包含 m i d d l e middle middle,下一轮循环的查找区间结束下标位置应为 m i d d l e − 1 middle - 1 middle−1,即right
应赋值为 m i d d l e − 1 middle - 1 middle−1 - 当
nums[middle] < target
时(target
在右区间 [ m i d d l e , r i g h t ] [middle, right] [middle,right] 时),更新查找右区间左边界left
;由判断条件可知当前nums[middle]
一定不是target
,则右区间不能包含 m i d d l e middle middle,下一轮循环的查找区间开始下标位置就是 m i d d l e + 1 middle + 1 middle+1,即left
应赋值为 m i d d l e + 1 middle + 1 middle+1 - 若
nums[middle] == target
,说明找到target
,返回索引middle
即可
- 因为
l
e
f
t
=
r
i
g
h
t
left = right
left=right 有意义,所以
- 不满足
l
e
f
t
<
=
r
i
g
h
t
left <= right
left<=right 时跳出循环,此时代表无法在数组中找到
target
,因此返回 − 1 -1 −1
代码
C++ 代码如下:
// 版本一:左闭右闭
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, 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;
}
};
Python 代码如下:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 定义target在左闭右闭的区间里,[left, right]
while left <= right:
middle = left + (right - left) // 2
if nums[middle] > target:
right = middle - 1 # target在左区间,所以[left, middle - 1]
elif nums[middle] < target:
left = middle + 1 # target在右区间,所以[middle + 1, right]
else:
return middle # 数组中找到目标值,直接返回下标
return -1 # 未找到目标值
二分法:左闭右开
思路
左闭右开思路与左闭右闭仅有三个地方有差异:
target
定义在一个左闭右开的区间里 [ l e f t , r i g h t ) [left, right) [left,right) 中,因此为了遍历整个数组,右边界right
初始值从nums.size() - 1
变为nums.size()
target
在 [ l e f t , r i g h t ] [left, right] [left,right] 区间,因此 l e f t = r i g h t left = right left=right 无意义,所以while (left < right)
要使用<
- 当
nums[middle] > target
时(即target
在左区间时),在 [ l e f t , m i d d l e ) [left, middle) [left,middle) 中,因为查找区间始终为 [ l e f t , r i g h t ) [left, right) [left,right) ,因此right
应赋值为 m i d d l e middle middle
代码
C++ 代码如下:
// 版本二:左闭右开
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, 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;
}
};
Python 代码如下:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) # 定义target在左闭右开的区间里,即:[left, right)
while left < right: # 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
middle = left + (right - left) // 2
if nums[middle] > target:
right = middle # target 在左区间,在[left, middle)中
elif nums[middle] < target:
left = middle + 1 # target 在右区间,在[middle + 1, right)中
else:
return middle # 数组中找到目标值,直接返回下标
return -1 # 未找到目标值
注意要点
middle
的写法问题:使用middle = left + ((right - left) / 2)
可等效替代(left + right)/2
,且可防止数值溢出;此外对于整型的正数之间,/
也可由右移运算符<<
等效替换,不过用位运算的好处是比直接相除的操作更快- 对区间的定义不同,影响的有三个地方
- 指针的初始化
- 循环结束条件
- 指针更新方式
- 二分的最大优势在于其时间复杂度为 O ( l o g n ) O(log n) O(logn),当看到有序数组都要第一时间反问自己是否可以使用二分!
- 二分的应用场景很多且有着许多变体,比如说查找第一个大于
target
的元素或者第一个满足条件的元素,本质原理都是一样的,根据是否满足题目的条件来缩小答案所在的区间,这个就是二分的本质 - 思维应当灵活变通,要知道二分的输入不一定是数组,也可以是数组中某一区间的起始位置和终止位置
相关题目推荐
待补充
27. 移除元素
文档讲解:代码随想录 | 数组 | 移除元素
视频讲解:数组中移除元素并不容易! | LeetCode:27. 移除元素
状态:第一遍写个暴力搜索困难重重,纯菜比www根本没想到双指针法
题目链接:27. 移除元素
题目分析: 此题要求仅使用
O
(
1
)
O(1)
O(1) 额外空间完成,因此只能在原数组上进行操作,最后返回一个长度,不过元素顺序是可以改变的
解题思路-1:双指针法
- 优势:不会改变元素顺序
- 时间复杂度: O ( n ) O(n) O(n) ,其中 n n n 为序列长度,且我们只需要遍历该序列至多两次
- 空间复杂度: O ( 1 ) O(1) O(1)
解题思路-2:双指针法优化
- 优势:时间复杂度优于思路1,两个指针在最坏的情况下合起来只遍历了数组一次
- 劣势:改变了元素的顺序(不过本题允许改变元素顺序)
- 时间复杂度: O ( n ) O(n) O(n) ,其中 n n n 为序列长度,且我们只需要遍历该序列至多一次
- 空间复杂度: O ( 1 ) O(1) O(1)
解题思路-3:暴力法
- 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n 为序列长度
- 空间复杂度: O ( 1 ) O(1) O(1)
双指针法
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作
思路
- 定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素(需删除值)的数组
- 慢指针:指向更新后新数组下标的位置
- 快指针先移动,如果不是目标元素,快指针指向值赋值给慢指针指向位置,慢指针移动;如果是目标元素,则慢指针不操作,快指针继续移动
- 返回慢指针最后指向的下标,就是新数组的大小
代码
C++ 代码如下:
// 时间复杂度: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;
}
};
Python 代码如下:
# (版本一)快慢指针法
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# 快慢指针
fast = 0 # 快指针
slow = 0 # 慢指针
size = len(nums)
while fast < size: # 不加等于是因为,a = size 时,nums[a] 会越界
# slow 用来收集不等于 val 的值,如果 fast 对应值不等于 val,则把它与 slow 替换
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
双指针法优化
思路
- 依然使用双指针,但两个指针初始时分别位于数组的首尾,向中间移动遍历该序列
- 如果左指针
left
指向的元素等于val
,此时将右指针right
指向的元素复制到左指针left
的位置,然后右指针right
左移一位。如果赋值过来的元素恰好也等于val
,可以继续把右指针right
指向的元素的值赋值过来(左指针left
指向的等于val
的元素的位置继续被覆盖),直到左指针指向的元素的值不等于val
为止
代码
C++ 代码如下:
// 时间复杂度优于双指针法,两个指针在最坏的情况下合起来只遍历了数组一次
// 左闭右闭写法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
if (nums[left] == val) {
nums[left] = nums[right--];
} else {
left++;
}
}
return left;
}
};
暴力法
思路
两层 for 循环,第一层 for 循环遍历数组元素,第二个 for 循环用来更新数组;每当第一层循环找到需要删除的目标值 val
,第二层循环将后面元素前移一位
代码
C++ 代码如下:
// 时间复杂度: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;
}
};
Python 代码如下:
# (版本三)暴力法
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
i, l = 0, len(nums)
while i < l:
if nums[i] == val: # 找到等于目标值的节点
for j in range(i+1, l): # 移除该元素,并将后面元素向前平移
nums[j - 1] = nums[j]
l -= 1
i -= 1
i += 1
return l
注意要点
- 移除元素暴力法易错点:发现需要移除的元素
val
并将数组集体向前移一位后,由于下标 i i i 以后的数值都向前移动了一位,所以 i i i 需要向前移动一位,且 i i i 的循环次数这里是size
也需要减一 - 暴力法中发现需要移除的元素
val
只需要将数组集体向前移一位:即nums[j - 1] = nums[j]
;不需要申明临时变量temp
做多余的交换操作!
相关题目推荐
待补充