704. 二分查找
暴力:单个for循环遍历,时间复杂度为 O(n)。
思路:由于本题是一个有序数组,并且无重复元素,因此可以考虑使用效率更高的二分法,使得每次的搜索规模减半。首先将整个数组设想为一个左闭右闭的区间 [left, right],然后将区间中点的元素 mid 与目标元素 target 对比。在本题非递减数组的情况下,如果 mid < target 则说明我们的 target 在 mid 右边,此时只需将左边界 left 更新为 mid + 1。
class Solution:
def search(self, nums: List[int], target: int) -> int:
# 使用二分法的前提:1.有序数组 2.无重复元素
left, right = 0, len(nums)-1
# 这里采用左闭右闭区间
# 因此左边界=右边界(left == right)是合法的
while left <= right:
# 避免溢出,等效于 (left + right) // 2
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return -1
# 时间复杂度:
# O(log n)
27. 移除元素
暴力:遍历数组,若遇到需要移除的元素,将此元素后面所有的元素向前挪动一位。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# 暴力
length = len(nums)
i = 0
while i < length:
if nums[i] == val:
for j in range(i + 1, length):
nums[j-1] = nums[j]
length -= 1
else:
i += 1
return length
# 时间复杂度:
# O(n²)
思路:使用快慢指针,快指针寻找新数组元素,慢指针更新新数组元素,只需遍历一次。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
fast, slow = 0, 0
while fast < len(nums):
# 若快指针找到了新数组元素,则通过慢指针更新,然后两指针继续后移
if nums[fast] != val:
nums[slow] = nums[fast]
fast += 1
slow += 1
# 若快指针指向的元素不属于新数组,则慢指针保持不动,直到快指针找到新数组元素
else:
fast += 1
return slow
# 时间复杂度:
# O(n)
拓展1:35. 搜索插入位置
本题与704的不同:若目标不存在于数组,不说返回 -1,而是返回元素按顺序插入的位置。因此我们只需要判断,当二分搜索结束后,左右边界分别在什么位置。
在左闭右闭且为升序数组的情况下,有几个关键的观察点:
1. 每次 nums[mid] < target,都需要将 left 向右移动,直到到达第一个不小于 target 的位置。
2. 每次 nums[mid] > target,都需要将 right 向左移动,直到到达第一个不大于 target 的位置。
因此,若数组不存在 target,则应该安插到 left 处或 right + 1 处。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# nums为无重复元素的升序数组,符合使用二分法的前提
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
# 与704返回-1不同,这次需要返回按顺序插入的位置
return left
# 时间复杂度:
# O(log n)
拓展2:34. 在排序数组中查找元素的第一个和最后一个位置
和704的区别是,本题可能包含重复元素,但是依然可以通过使用两次二分法来分别找出第一个和最后一个位置,只需要在标准二分法的基础上多写一个边界判断。
以寻找第一个位置举例,若二分法找到了一个 mid == target,此时我们需要额外判断 mid 是否为 target 出现的第一个位置。如果 mid 为数组第一个元素,或者 mid 的前一个元素(mid - 1)依然等于 target,则说明此 mid 并不是第一个位置,此时我们需要将右边界左移,再次进行二分搜索。
简而言之,本题搜索的目标不仅仅是 value == target,还需要满足“是第一个出现的”位置条件。
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def findFirst(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
# 由于元素可能不唯一,此时需要向左寻找target出现的第一个位置
else:
# 判断当前找到的mid是否为target出现的第一个位置
# 要么mid是数组首位,要么mid前一个元素不等于target
# 如果mid不是target首次出现的位置,我们需要缩小右边界继续寻找
if mid == 0 or nums[mid-1] != target:
return mid
else:
right = mid - 1
return -1
def findRight(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
if mid == len(nums) - 1 or nums[mid+1] != target:
return mid
else:
left = mid + 1
return -1
return [findFirst(nums, target), findRight(nums, target)]
# 时间复杂度:
# O(log n) + O(log n) = O(log n)