代码随想录算法训练营第一天| 704. 二分查找、27. 移除元素、34. 在排序数组中查找元素的第一个和最后一个位置、35. 搜索插入位置
704. 二分查找
题目链接:二分查找
给定一个n个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在nums中而且下标为4
左闭右开还是左闭右闭?
两种做法其实都可以,一开始选定一种做法之后就需要连贯下去保持不变,关于怎么确定边界则取决于现在选择的区间是否合法。
左闭右闭
左闭右开:[left,right]
nums[right]这个元素其实是包括在这个区间内的,且当left==right时,这个区间依然是合法的,所以之后的操作都要注意这一点。
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
#左闭右闭:[left,right]
left = 0
right = len(nums)-1 #right在搜索区间里,注意nums[length(nums)]其实是不存在的
while left <= right: #left==right区间依然合法所以可以写等于
middle = int((left+right)/2) #记得加上int
if nums[middle] > target:
right = middle - 1 #right在搜索区间里,但是nums[middle]已经确定不是target了所以不要
elif nums[middle] < target:
left = middle + 1 #一样,确定不是了所以不要
else:
return middle
return -1
左闭右开
左闭右开:[left,right)
nums[right]这个元素是不包括在这个区间内的,且当left==right时,这个区间是不合法的。
class Solution:
def search(self, nums: List[int], target: int) -> int:
#左闭右开:[left,right)
left = 0
right = len(nums) #right不在搜索区间里,所以可以取到len(nums)
while left < right: #因为取不到,所以left==right时,[left,left)是不合法的所以不可以等于
middle = int((left+right)/2)
if nums[middle] > target:
right = middle #本来就取不到所以不用-1
elif nums[middle] < target:
left = middle + 1 #取得到所以要+1
else:
return middle
return -1
总结
不管是哪种方法注意前后一致。
27. 移除元素
题目链接:移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],
也会被视作正确答案。
数组的删除并不是直接就拿掉这个元素,其实是覆盖。删完之后会返回删掉之后的size但实际的物理空间其实还是原来的长度,最后几个没覆盖的并没有被处理。
list.remove()移除函数其实是个O(n)的函数,删除掉一个元素然后后面的元素整体向前移一个位置。
如果一道题可以直接用库函数解决,那就不要用,如果要使用也要知道这个库函数的时间复杂度
暴力解法
暴力解法需要两个循环,一个用来遍历整个数组,另一个用来更新数组(把要移除的元素后面的元素都往前移一位),这种做法的时间复杂度是O(n2)。
这里要注意是当找到了val然后把后面的元素都往前移一位之后,此时的i依然是原先val位置的i不需要往后走一位,因为后面那一位已经移上来了。
class Solution(object):
def removeElement(self, nums, val):
"""
:type nums: List[int]
:type val: int
:rtype: int
"""
#暴力解法
l = len(nums)
i = 0
while i < l:
#print(nums[i], val)
if nums[i] == val:
for j in range((i+1),l):
nums[j-1] = nums[j]
l = l - 1
else: #需要一个else,if成立的情况下i是不需要+1的
i = i + 1
return l
双指针
可以用O(n)来实现这个问题。有一个快指针和一个漫指针,快指针用来找到新数组的元素,慢指针代表我们需要更新的下标,当快指针找到一个非val元素就通知慢指针去覆盖,这里其实是遍历和更新在同时进行。
val = 3, nums = [1, 3, 4, 3, 5] slow=0, fast=0
同时指向元素1,不是val,slow=1, fast=1
同时指向元素3,是val,不用更新,slow=1, fast=2
slow还指向元素3,fast指向元素4,不是val,更新,slow=2, fast=3, nums = [1, 4, 4, 3, 5]
slow还指向元素4,fast指向元素3,是val,不用更新,slow=2, fast=4
slow还指向元素4,fast指向元素5,不是val,更新,slow=3, fast=5结束
nums=[1, 4, 5, 3, 5],size = slow = 3。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
#双指针解法
fast = 0 #指向新数组的元素
slow = 0 #代表需要更新的下标
while fast < len(nums):
if nums[fast] != val:
nums[slow] = nums[fast]
fast += 1
slow += 1
else:
fast += 1
return slow
总结
双指针快指针用于遍历,慢指针用于更新,只用一个循环,所以复杂度是O(n)。
34. 在排序数组中查找元素的第一个和最后一个位置
题目链接:在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
这个题和704很相似,一开始就觉得应该用二分法来做,但是想了很久都不知道怎么来处理前后是否也是target的方法,一直都想能不能一步到位,但还是没有做到后来还是选择了先写了二分查找的函数再找前后,这个应该不是O(log n)了,应该是O(n/2)
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
#[left, right)
def searchtarget(nums: List[int], target: int) -> int:
left = 0
right = len(nums)
while left < right:
middle = int((left+right)/2)
if nums[middle] < target:
left = middle + 1
elif nums[middle] > target:
right = middle
else:
return middle
return -1
index = searchtarget(nums, target)
if index == -1: return [-1,-1]
else:
left = index
right = index
#这个地方是不是out of range 一定要写在是不是等于target之前,超过了就nums[left - 1]和nums[right + 1]就走不了
while left - 1 >= 0 and nums[left - 1] == target:
left -= 1
while right + 1 < len(nums) and nums[right + 1] == target:
right += 1
return [left, right]
O(log n)的做法可以先用二分法找到左右边界再往下做,写的时候搞混了好几次,所以简单记录一下几种情况,首先左右边界其实就是距离target最近的左右位置,如果target 在nums里那就是target所在的位置的左右位置。以下是四种情况:
nums = [3,5,7]
- taget = 2,左边界不存在右边界存在,(-2, 0)
- taget = 8,左边界存在右边界不存在,(2, -2)
理清这两种情况可以帮助写左右边界的function - taget = 4,左边界存在右边界存在,(0, 1),target不存在,这种情况下因为nums是有序的,target一定会卡在两个相邻的位置之间,所以只要左右边界相邻那就一定是target不存在。
- taget = 5,左边界存在右边界存在,(0, 2),target存在,这种情况下target有自己的位置所以左右边界也是隔开的,左边界往前一位,右边界往后一位就可以找到target所在的位置。
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
def rightrange(nums, target):
# [left, right)
left = 0
right = len(nums)
r = -2
while left < right:
middle = int((left+right)/2)
if nums[middle] <= target:
left = middle + 1
r = left
else:
right = middle
return r
def leftrange(nums, target):
# [left, right)
left = 0
right = len(nums)
l = -2
while left < right:
middle = int((left+right)/2)
if nums[middle] < target:
left = middle + 1
else:
right = middle
l = right - 1
return l
l = leftrange(nums, target)
r = rightrange(nums, target)
if (l == -2) or (r == -2): return [-1, -1]
if r - l > 1: return [l+1, r-1]
return [-1,-1]
35. 搜索插入位置
题目链接:搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
输入: nums = [1,3,5,6], target = 5
输出: 2
输入: nums = [1,3,5,6], target = 7
输出: 4
也是用二分法来做,我用的是左闭右开,这个地方我插入了一个index用来记录target应该在位置。如果最后能找到target的位置就不用index,如果找不到就输出index。
index记录方法是如果区间的中位数是小于target的那么target的位置应该在中位数之后;如果区间的中位数是大于target的那么target的位置应该在中位数之前,每次循环都可以做一个更新直到找到target或者循环结束。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
#[left, right)
left = 0
right = len(nums)
index = 0
while left < right:
middle = int((left + right)/2)
if nums[middle] > target:
right = middle
index = middle
elif nums[middle] < target:
left = middle + 1
index = middle + 1
else:
return middle
return index