代码随想录刷题第一天
二分查找法 (LC704)
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
示例1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
解题思路:
这道题重要运用的思想是二查找分法。 需要主要的是,二分查找法在使用前需要注意以下内容:
-
查找的数组必须是升序排列,即 [1,2,3,4,5] 而非 [3,2,1,4,5], 否则无法更新边界。 如果没有升序排序,需要先排序
list.sort()
-
数组不可以有重复元素,如果有重复元素,则返回的下标不唯一。LC34 找区间 是这道题的变体,其数组内有重复元素,目的是找到重复元素区间。(请看附录)
-
一定要注意循环不变量。在这里是对于区间的定义。左右区间是
左闭右开 [0,1)
还是左闭右闭 [0,1]
。两者在代码实现上有所区别。
左闭右闭区间
左闭右闭区间,也就是target 在[left, right] 区间,由此定义了代码的书写规范:
- 循环二分时使用
while (left <= right)
, 因为left == right
依然在左闭右闭区间中依然是有意义的,需要继续查找。例如当 left=right=1时,仍需查看nums[left]是否为target - 在更新左区间的右边界时,即:if (nums[middle] > target), 因为num[middle] != target, 所以根据左闭右闭区间原则,我们不需要将middle放在区间中,所以right = middle-1。
- 同理,在更新右区间的左边界时,应该使用left = middle+1
- 右指针初始化时使用 right = len(nums)-1, 因为包含最右边的下标
while (left <= right):
middle = ...
if (nums[middle]>target):
right = middle-1
.....
左闭右开区间
左闭右开区间,也就是target在[left, right)区间,由此定义了如下代码规范:
- 循环二分时使用
while (left < right)
, 因为left == right
不在左闭右开区间中,没有意义 - 更新左区间的右边界时,num[middle] != target,由于区间右边不包括最右边的元素,所以更新时使用right = middle
- 左边界更新不变
- 右指针初始化时使用 right = len(nums), 因为不包含最后一个数的下标,所以需要加一位。
while (left < right):
middle = ...
if (nums[middle]>target):
right = middle
.....
代码实现:
左闭右闭
class Solution(object):
def search(self, nums, target):
if len(nums) == 0 or nums is None:
return -1
# 初始化坐标
l = 0
r = len(nums)-1 # 初始化,闭区间包含最右侧,即nums中最后一个下标
# 循环二分
while (l<=r): # 右闭,l<=r
mid = l+(r-l)//2 # 计算mid
if (nums[mid] < target):
l = mid+1
elif (nums[mid]>target):
r = mid-1 # 右闭,包含最右侧,需要排除middle
else:
return mid
return -1
左闭右开
class Solution(object):
def search(self, nums, target):
if len(nums) == 0 or nums is None:
return -1
# 初始化坐标
l = 0
r = len(nums) # 右开,不包含最右侧,使用nums最后一个下标+1
# 循环二分
while (l<r): # 右开,l<r
mid = l+(r-l)//2 # 计算mid
if (nums[mid] < target):
l = mid+1
elif (nums[mid]>target):
r = mid # 右闭,不包含最右侧,使用middle
else:
return mid
return -1
移除元素 (LC27) 双指针
题目链接
给你一个数组 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],也会被视作正确答案。
暴力法
简单的方法就是使用两次循环,第一次循环遍历整个数组,如果当前所指的数字是val
,则使用第二层循环,将给下标后面的所有内容往前移动,完成向前覆盖。
!!! 这里使用Python的时候与代码随想录中的写法有区别,debug了很久~
- 注意: Python 在使用
for i in range(xx)
的时候,i 这个变量是固定递增的,无法在循环内部更改,永远是在循环的时候逐渐递增。 例如:
for i in range(5):
print(i) # [0,1,2,3,4] 永远按顺序,不会因为在当前循环中改变而在下一次循环时改变
i-=1
print(i) # [-1,0,1,2,3] 循环中调用i后可以改变数值
- 所以当循环次数取决于循环内部时,需要使用
while
循环。在本题中,当所有元素向前覆盖时,需要在当前位置再判断一次(i不变
), 所以使用while
循环。 - 同时还需注意的是,循环的结束条件不取决于原数组的长度,而是取决于新数组长度(
size
)。因为i>size
为向前覆盖后留下来的没用内容,继续运行会循环超时。
暴力法代码实现:
class Solution(object):
def removeElement(self, nums, val):
i = 0
size = len(nums)
while (i<size): # while 循环结束条件取决于新数组的长度
if nums[i]==val: # 判断是否需要向前覆盖
for j in range(i+1, len(nums)):
nums[j-1] = nums[j]
j+=1
size-=1 # 向前覆盖一次,新数组长度减一
i-=1 # 需要在当前位置再循环一次,保证新覆盖的数字不为val
i+=1
return size
双指针法
除了暴力法外,这道题更有效的方法是双指针法,只需要O(n)时间复杂度就可完成。这里我们使用快慢双指针法。
-
在快慢双指针算法中,我们有两个指针,快指针和慢指针。两者初始时都指在0。一个
for
循环更新快指针,每个循环快指针向前移动一次。 -
慢指针只在快指针指的不是target的时候更新。当目标不为
target
是,这个数据是要保留的,则将该数据赋值给慢指针所指的单元格,并将慢指针右移一格。如果快指针所指的是target,则跳过赋值,快指针直接向后移动一格
双指针代码实现:
class Solution(object):
def removeElement(self, nums, val):
slow = 0
for fast in range(len(nums)):
# 快指针所指的数不为target,新数组需要这个数
if nums[fast]!=val:
nums[slow] = nums[fast]
slow+=1 #更新赋值完后,慢指针加一
fast+=1
return slow # 最后慢指针的下标就是新数组的长
附加题 (LC34) 查找元素区间,二分查找法变体
题目链接
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
由于题目要求使用 O(logn)
算法解决此问题,所以传统的暴力解法不适用,只可以使用二分法。
因为题目说数组是非递减数组,则即使有重复值,也是紧邻的重复值。由于需要返回一个target的区间范围,有两个下标,因此思路是使用两次
二分法查找target的左右边界。
然而,传统的二分法单次只能准确的找到数组中独一无二的数(非重复数)。如果数字重复,则二分法所返回的下标可能会是任意一个位置。传统二分法会在找到target后停止查找并返回index。 这里我们当我们找到target后继续更新左/右边界,继续查找不返回,设置新的边界。
right = middle-1
找重复数的左边界,看看左区间内是否还有target, 尽量向左挪动边界left = middle+1
找重复数的右边界,看看右区间内是否还有target, 尽量向右挪动边界
代码实现:
class Solution(object):
def searchRange(self, nums, target):
left = 0
right = len(nums)-1
first = -1
last = -1
# 搜索重复数组的第一个位置(左边界)
while (left <= right):
middle = left + (right-left)//2
if (nums[middle]>target):
right = middle-1
elif (nums[middle]<target):
left = middle+1
else:
first = middle
right = middle-1 # 继续搜索
# 记得在完成一次二分法后要重新初始化区间
left = 0
right = len(nums)-1
# 搜索重复数组的最后一个位置(右边界)
while (left <= right):
middle = left + (right-left)//2
if (nums[middle]>target):
right = middle-1
elif (nums[middle]<target):
left = middle+1
else:
last = middle
left = middle+1 # 继续搜索
return (first, last)
总结:
今天主要学习了解决数组问题的两种算法,二分查找法和快慢双指针。编程萌新,在手撕代码的时候所花时间较长,尤其是在debug第二题暴力解法的时候花了点时间。希望能够坚持学习打卡,在一刷完力扣后能够在编程上有所提升。加油!!!