代码随想录算法训练营第一天 | 704、27
数组
704. 二分查找
力扣链接:704.二分查找
题目描述
给定一个
n
n
n 个元素有序的(升序)整型数组
n
u
m
s
nums
nums 和一个目标值
t
a
r
g
e
t
target
target ,写一个函数搜索
n
u
m
s
nums
nums 中的
t
a
r
g
e
t
target
target,如果目标值存在返回下标,否则返回
−
1
-1
−1。
示例1:
输入:
n
u
m
s
=
[
−
1
,
0
,
3
,
5
,
9
,
12
]
,
t
a
r
g
e
t
=
9
nums = [-1,0,3,5,9,12],target = 9
nums=[−1,0,3,5,9,12],target=9
输出:4
解释:9 出现在
n
u
m
s
nums
nums中并且下标为4
示例2:
输入:
n
u
m
s
=
[
−
1
,
0
,
3
,
5
,
9
,
12
]
,
t
a
r
g
e
t
=
2
nums = [-1,0,3,5,9,12],target = 2
nums=[−1,0,3,5,9,12],target=2
输出:-1
解释:2不存在
n
u
m
s
nums
nums中因此返回-1
思路
看到题目时可以很容易发现是有序数组,且中间没有重复元素,所以是一个考察二分查找的题目,而二分查找中最重要的部分就是对于二分时左右边界值部分的把握。这里展示两种边界值规定方法,分别是 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。
方法一
在方法一中使用的边界值规定方法是
l
e
f
t
<
=
r
i
g
h
t
left<=right
left<=right,这里表达的是:
首先,在最后一次循环时左右指针是可以相等的;
其次,在循环内部时,左右指针表达的是闭区间的范围,那么在指针移动的时候需要将左或右指针移动到
m
i
d
mid
mid指针的前一位或后一位,因为此时
m
i
d
mid
mid指向的元素必定不与
t
a
r
g
e
t
target
target相等。
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
int left = 0,right = n-1,mid;
while(left <= right){
mid = (right-left)/2 +left;
if(nums[mid] == target){
return mid;
}
else if(nums[mid]<target){
left = mid+1;
}
else if(nums[mid]>target){
right = mid-1;
}
}
return -1;
}
}
一些需要注意的点
第一,在规定
m
i
d
mid
mid的值时使用
(
r
i
g
h
t
−
l
e
f
t
)
/
2
+
l
e
f
t
(right-left)/2+left
(right−left)/2+left而不是
(
r
i
g
h
t
+
l
e
f
t
)
/
2
(right+left)/2
(right+left)/2,是为了防止因为相加导致越界而发生错误。如果
l
e
f
t
+
r
i
g
h
t
left+right
left+right大于
i
n
t
int
int能表示的最大值
2
31
−
1
2^{31}-1
231−1,那么会取模,算出来就不对了,所以需要使用减法。
第二,在循环内部对左右指针进行移动的时候需要注意此时选取的是左闭右闭的区间,所以左右移动均需要跳过此时
m
i
d
mid
mid所指的位置。
方法二
在方法一中使用的边界值规定方法是
l
e
f
t
<
r
i
g
h
t
left<right
left<right,这里表达的是:
首先,在最后一次循环时左右指针是可以不可以取等的;
其次,在循环内部时,左右指针表达的是左闭右开的范围,那么在指针移动的时候需要将左指针的移动需要跳过目前
m
i
d
mid
mid所指的位置,而右指针则不需要,因为此时的右边界值并没有被取到过。
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
int left = 0,right = n;
while(left < right){
int mid = (right-left)/2 +left;
if(nums[mid] == target){
return mid;
}
else if(nums[mid]<target){
left = mid+1;
}
else if(nums[mid]>target){
right = mid;
}
}
return -1;
}
}
需要注意的点
首先,在判断时,因为是左闭右开的区间,所以右侧的范围边界需要设置为
n
n
n而不是
n
−
1
n-1
n−1,因为需要将最后一个元素也包括进来;
其次,在右指针移动时直接移动到当前
m
i
d
mid
mid所指的位置即可,因为是开区间,并没有包含过。
总结
二分查找是数组中很基础的一种双指针操作方式,在其中最需要重视的就是区间的规定,在规定了区间边界的开闭之后需要从始至终地使用这一套逻辑,而不能混用,否则必乱。两种方法中我更加偏爱第一种,毕竟在移动指针时会有左右对称的感觉。
相关题目
35. 搜索位置插入
力扣链接:35.搜索位置插入
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为
O
(
l
o
g
n
)
O(log n)
O(logn) 的算法。。
示例1:
输入:
n
u
m
s
=
[
1
,
3
,
5
,
6
]
,
t
a
r
g
e
t
=
5
nums = [1,3,5,6] , target = 5
nums=[1,3,5,6],target=5
输出:
2
2
2
示例2:
输入:
n
u
m
s
=
[
1
,
3
,
5
,
6
]
,
t
a
r
g
e
t
=
2
nums = [1,3,5,6] , target = 2
nums=[1,3,5,6],target=2
输出:
1
1
1
示例3:
输入:
n
u
m
s
=
[
1
,
3
,
5
,
6
]
,
t
a
r
g
e
t
=
7
nums = [1,3,5,6] , target = 7
nums=[1,3,5,6],target=7
输出:
4
4
4
思路
首先这是一个无重复元素的升序排列数组,其次,题目需要我们使用时间复杂度为 O ( l o g n ) O(log n) O(logn)的算法,这样就可以锁定这是一个二分搜索的题目了。
代码
本题使用了与704中基本一模一样的代码,只不过在返回值上需要注意,其他的逻辑就是简单的二分搜索。
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0,right = nums.length-1,mid;
while(left<=right){
mid = (right-left)/2+left;
if(nums[mid] == target){
return mid;
}
else if(nums[mid] > target){
right = mid-1;
}
else if(nums[mid] < target){
left = mid+1;
}
}
return right+1;
}
}
需要注意的点
一定要记住最后要返回的是什么,可以在草稿纸上用一些例子来验证,记住,不是返回 m i d mid mid!不是返回 m i d mid mid!
总结
这道题没什么好总结的,只要能够熟练地应用二分搜索即可。
34. 在排序数组中查找元素的第一个和最后一个位置
题目描述
给你一个按照非递减顺序排列的整数数组
n
u
m
s
nums
nums,和一个目标值
t
a
r
g
e
t
target
target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值
t
a
r
g
e
t
target
target,返回
[
−
1
,
−
1
]
[-1, -1]
[−1,−1]。
你必须设计并实现时间复杂度为
O
(
l
o
g
n
)
O(log n)
O(logn)的算法解决此问题。
示例1:
输入:
n
u
m
s
=
[
5
,
7
,
7
,
8
,
8
,
10
]
,
t
a
r
g
e
t
=
8
nums = [5,7,7,8,8,10] , target = 8
nums=[5,7,7,8,8,10],target=8
输出:
[
3
,
4
]
[3,4]
[3,4]
示例2:
输入:
n
u
m
s
=
[
5
,
7
,
7
,
8
,
8
,
10
]
,
t
a
r
g
e
t
=
6
nums = [5,7,7,8,8,10] , target = 6
nums=[5,7,7,8,8,10],target=6
输出:
[
−
1
,
−
1
]
[-1,-1]
[−1,−1]
示例3:
输入:
n
u
m
s
=
[
]
,
t
a
r
g
e
t
=
0
nums = [] , target = 0
nums=[],target=0
输出:
[
−
1
,
−
1
]
[-1,-1]
[−1,−1]
思路
看到题目中的非递减顺序整数数组,一个目标值和时间复杂度为
O
(
l
o
g
n
)
O(log n)
O(logn)就能够感受到一种扑面而来的二分搜索的味道了。
第一,我们要明确的是我们在搜索什么?
本题中需要返回的是目标值在数组中的开始和结束的位置,那么我们需要查找两个东西,一个是范围的左边界,一个是范围的右边界。
第二,我们需要明确的就是,如何寻找边界?
寻找边界有两种方式。
(1)直接找到边界
(2)首先找到值的位置,但不知道值是否为边界,并进一步寻找边界
第三,我们需要明确,找到边界后如何判断此时target与边界的关系?
本题在查找到相应值时返回的是边界,而没有查找到时需要返回-1,那么这就需要我们来对边界状态来进行判定。
(1)target在数组边界之外,此时判定为未找到
(2)target在数组边界之内,但是没找到,此时仍判定为未找到
(3)target在数组边界之内,找到了,此时判定为找到了
方法一
方法一是直接寻找边界的方法,二分搜索的作用在这里不是在寻找某一个值的位置,而是通过搜索时范围的大小关系来判定边界值。
class Solution {
public int[] searchRange(int[] nums, int target) {
int leftBound = findLeftBound(nums,target);
int rightBound = findRightBound(nums,target);
if(leftBound == -2 || rightBound == -2) return new int[]{-1,-1};
if(rightBound - leftBound >1) return new int[]{leftBound+1,rightBound-1};
else return new int[]{-1,-1};
}
public int findRightBound(int[]nums,int target){
int left = 0,right = nums.length-1,rightBound = -2;
while(left <= right){
int mid = (right-left)/2+left;
if(nums[mid]>target){
right = mid-1;
}
else{
left = mid+1;
rightBound = left;
}
}
return rightBound;
}
public int findLeftBound(int[]nums,int target){
int left = 0,right = nums.length-1,leftBound = -2;
while(left <= right){
int mid = (right-left)/2+left;
if(nums[mid] < target){
left = mid +1;
}
else{
right = mid-1;
leftBound = right;
}
}
return leftBound;
}
}
需要注意的点
1、需要注意,二分搜索在本解中的作用是寻找边界,因此不需要设置相等的判断,只需要通过大小比较找到边界即可。
2、需要注意
t
a
r
g
e
t
target
target与边界的关系才能得到最终的结果。
方法二
这里二分搜索的作用在于找到一个元素,然后在元素的左右寻找边界。这也相当于将边界判定状态的前两个合并。
class Solution {
public int[] searchRange(int[] nums, int target) {
int index = search(nums,target);
if(index == -1) return new int[]{-1,-1};
int left = index,right = index;
while(left-1>=0 && nums[left-1]==target){
left--;
}
while(right+1<nums.length && nums[right+1] == target){
right++;
}
return new int[]{left,right};
}
public int search(int[] nums,int target){
int left = 0,right = nums.length-1;
while(left <= right){
int mid = (right-left)/2 + left;
if(nums[mid] == target){
return mid;
}
else if(nums[mid]<target){
left = mid+1;
}
else {
right = mid-1;
}
}
return -1;
}
}
总结
本题重在考查二分查找的应用。这里最需要明确的是我们需要应用二分查找做什么,并将情况分类。
27. 移除元素
力扣链接:27. 移除元素
题目描述
给你一个数组
n
u
m
s
nums
nums 和一个值
v
a
l
val
val,你需要原地移除所有数值等于
v
a
l
val
val的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用
O
(
1
)
O(1)
O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例1:
输入:
n
u
m
s
=
[
3
,
2
,
2
,
3
]
,
v
a
l
=
3
nums = [3,2,2,3] , val = 3
nums=[3,2,2,3],val=3
输出:
2
,
n
u
m
s
=
[
2
,
2
]
2, nums = [2,2]
2,nums=[2,2]
示例2:
输入:
n
u
m
s
=
[
0
,
1
,
2
,
2
,
3
,
0
,
4
,
2
]
,
v
a
l
=
2
nums = [0,1,2,2,3,0,4,2] , val = 2
nums=[0,1,2,2,3,0,4,2],val=2
输出:
5
,
n
u
m
s
=
[
0
,
1
,
4
,
0
,
3
]
5, nums = [0,1,4,0,3]
5,nums=[0,1,4,0,3]
思路
首先,可以想到尝试暴力破解的方式,毕竟只有一个一维数组,循环只需两层即可,而且这题居然真的不超时,惊喜。
想要降低时间复杂度则可使用快慢指针的方式。快指针查找非目标元素,慢指针负责更新覆盖,这样只需一层循环即可完成。
方法一
本方法为暴力破解法,外层循环找到等于 v a l val val的元素,内层循环通过移动后面的元素实现覆盖。
class Solution {
public int removeElement(int[] nums, int val) {
int n = nums.length;
for(int i=0;i<n;i++){
if(nums[i] == val ){
for(int j = i+1;j<n;j++){
nums[j-1] = nums[j];
}
i--;
n--;
}
}
return n;
}
}
方法二
此方法使用快慢指针来降低算法的时间复杂度。快指针寻找非目标元素,慢指针指向需要更新覆盖的位置,进行数组的更新。
class Solution {
public int removeElement(int[] nums, int val) {
int slow = 0;
for(int fast = 0;fast<nums.length;fast++){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
}
需要注意的点
本题中需要注意的点是,在使用快慢指针时,要能够想到需要更新和覆盖的虽然是等于 v a l val val值的元素,但是快指针指到的应该是非 v a l val val元素,这样才能实现更新。有点类似于一个小小的反逻辑。
总结
在本地移除或者移动数组中元素,一般都会使用双指针。但使用双指针时一定要注意找到这两个指针的含义,不要瞎想,否则很容易走错。
文章介绍了二分查找算法在不同问题中的应用,如寻找目标值的下标、搜索插入位置以及在排序数组中查找元素的第一个和最后一个位置。通过两种不同的二分查找方法,分别展示了如何处理边界条件,并提供了相关题目的解题思路和注意事项。最后,提到了移除数组中特定元素的解决方案,强调了快慢指针在优化时间复杂度中的作用。

1086





