文章目录
题目特征:
- 数据是有序或者相对有序的
- 每次操作可以排除掉一半的可能性
有序数据
代表场景:
- 二分查找
- 普通二分查找
- 二分下界查找
- 二分上界查找
搜索一个元素时,搜索区间两端闭
while条件带等号,否则需要打补丁
if相等就返回,其他事情甭操心
mid必须加减一,因为区间两端闭
while结束就凉了,凄凄惨惨返-1
搜索左右区间时,搜索区间要阐明
左闭右开最常见,其余逻辑便自明
while要用小于号,这样才能不漏掉
if相等别返回,利用mid锁边界
普通二分查找:左闭右闭
上下界查找: 左闭右开
while <
nums = [1, 1, 3, 6, 6, 6, 7, 8, 8, 9]
def binary_search(nums, target):
'''找一个数'''
l = 0
r = len(nums) - 1
while l <= r:
mid = (l + r) // 2
# 如果是Java Cpp
# mid = l + (r - l) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
l = mid + 1
elif nums[mid] > target:
r = mid - 1
return -1
print("binary_search", binary_search(nums, 6))
def lower_bound(nums, target):
'''找左边界'''
l = 0
r = len(nums)
while l < r:
mid = (l + r) // 2
if nums[mid] == target:
r = mid
elif nums[mid] < target:
l = mid + 1
elif nums[mid] > target:
r = mid
# 对未命中情况进行后处理
if l == len(nums):
return -1
return l if nums[l] == target else -1
print("lower_bound", lower_bound(nums, 6))
print("lower_bound", lower_bound(nums, 2))
def upper_bound(nums, target):
'''找右边界'''
l = 0
r = len(nums)
while l < r:
mid = (l + r) // 2
if nums[mid] == target:
l = mid + 1
elif nums[mid] < target:
l = mid + 1
elif nums[mid] > target:
r = mid
if l == 0:
return -1
return l - 1 if nums[l - 1] == target else -1
print("upper_bound", upper_bound(nums, 100))
print("upper_bound", upper_bound(nums, -1))
print("upper_bound", upper_bound(nums, 2))
print("upper_bound", upper_bound(nums, 6))
相对有序数据
数据是相对有序的,可以通过一些tricky做到二分查找,这里列举3道题
咱们讨论的第一题相对最简单:
class Solution:
def minArray(self, numbers: List[int]) -> int:
l = 0
r = len(numbers) - 1
while l < r:
mid = (l + r) // 2
if numbers[l] >= numbers[r]:
if numbers[mid] == numbers[r]:
l += 1
elif numbers[mid] < numbers[r]:
r = mid
else:
l = mid + 1
else:
return numbers[l]
return numbers[r]
第二题相对难一些:
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
l = 0
r = n - 1
while l <= r:
mid = (l + r) // 2
if nums[mid] == target:
return mid
if nums[l] < nums[mid] or l==mid:
if nums[l] <= target < nums[mid]:
r = mid - 1
else:
l = mid + 1
else: # 写成了 <
if nums[mid] < target <= nums[r]:
l = mid + 1
else:
r = mid - 1
return -1
第三题在第二题的基础上去掉了【互不相同】这个限定条件,这是常见的套路(反正我是第3次见到了),遇到这种场景直接加个线性搜索就好了:
注意:这题的时间复杂度分上下界,如果所有元素相同的情况下,时间复杂度为 O ( N ) \mathcal{O}(N) O(N),如果所有元素各不相同,变为上题的情形,为 O ( log N ) \mathcal{O}(\log N) O(logN)
时间复杂度分情况讨论的场景,还有很多,举几个例子,快速排序
相对有序
时,时间复杂度为 O ( N 2 ) \mathcal{O}(N^2) O(N2)(可以理解为二叉树退化为链表),其他为 O ( N log N ) \mathcal{O}(N\log N) O(NlogN)。插入排序在元素升序相对有序
时,时间复杂度为 O ( N ) \mathcal{O}(N) O(N),平均时间复杂度为 O ( N 2 ) \mathcal{O}(N^2) O(N2)
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
l = 0
r = n - 1
while l <= r:
mid = (l + r) // 2
if nums[mid] == target:
return True
# 相比于原版就加了这个if判断
if nums[l] == nums[mid]:
l += 1
continue
if nums[l] < nums[mid] or l==mid:
if nums[l] <= target < nums[mid]:
r = mid - 1
else:
l = mid + 1
else: # 写成了 <
if nums[mid] < target <= nums[r]:
l = mid + 1
else:
r = mid - 1
return False
每次操作排除一半可能性
- 二叉搜索树
- 二叉搜索树的插入
- 二叉搜索树的查找
- 二叉搜索树的删除
- 二叉树的相关算法
- 求完全二叉树的高度
- 可以转为为二分查找的问题
等等,需要自己识别
二叉搜索树
判断二叉树是否合法
我们通过使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点,这也是二叉树算法的一个小技巧吧。
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
return self._isValidBST(root, None, None)
def _isValidBST(self, root: TreeNode, min_: TreeNode, max_: TreeNode):
if root is None:
return True
if min_ is not None and root.val <= min_.val:
return False
if max_ is not None and root.val >= max_.val:
return False
return self._isValidBST(root.left, min_, root) and \
self._isValidBST(root.right, root, max_)
BST的查找
class Solution:
def searchBST(self, root: TreeNode, val: int) -> TreeNode:
if root is None or root.val == val:
return root
if root.val < val:
return self.searchBST(root.right, val)
else:
return self.searchBST(root.left, val)
BST的插入
class Solution:
def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
if root is None:
return TreeNode(val)
if root.val < val:
root.right = self.insertIntoBST(root.right, val)
else:
root.left = self.insertIntoBST(root.left, val)
return root
BST的删除
- 叶子结点(0个孩子) → 当场去世
- 1个孩子 → 让孩子接替自己位置
-
两个孩子 → 找到
左子树中最大的结点
或右子树最小结点
接替自己
class Solution:
def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
if root is None:
return None
if root.val == key:
# 一举解决了情况1和情况2
if root.left is None:
return root.right
if root.right is None:
return root.left
# 情况3
min_node = self.find_min(root.right) # root.right 写错
root.val = min_node.val
root.right = self.deleteNode(root.right, min_node.val)
elif root.val > key: # 左右顺序写错
root.left = self.deleteNode(root.left, key)
elif root.val < key:
root.right = self.deleteNode(root.right, key)
return root
def find_min(self, root: TreeNode) -> TreeNode:
while root.left:
root = root.left
return root
可以转为为二分查找的问题
本质是二分查找找左边界
需要注意的是时间复杂度的确定
O ( N log W ) O(N\log W) O(NlogW),其中N是香蕉堆的数量,W是最大香蕉堆的大小
其中
log
W
\log W
logW表示了二分查找的复杂度,
O
(
N
)
O(N)
O(N) 表示了每次查找进行cur_H
计算的时间复杂度。
class Solution:
def minEatingSpeed(self, piles: List[int], H: int) -> int:
def get_H(k: int):
return sum([ceil(pile / k) for pile in piles])
l = 1
r = max(piles)
while l < r:
mid = (l + r) // 2
cur_H = get_H(mid)
if cur_H == H:
r = mid # 往左边逼近
elif cur_H < H:
r = mid
elif cur_H > H:
l = mid + 1
return l
引人深思的一题,告诉我们当无法通过case的时候,需要仔细再看看题设条件
算法题,首先考的是语文/英语,然后才是算法
def get_D(weights, w):
D = 0
cur_weight = 0
for weight in weights:
if cur_weight + weight > w:
cur_weight = 0
D += 1
cur_weight += weight
return D + 1
class Solution:
def shipWithinDays(self, weights: List[int], D: int) -> int:
l = max(weights) # fixme 错在这
r = sum(weights)
while l < r:
mid = (l + r) // 2
cur_D = get_D(weights, mid)
if cur_D == D:
r = mid
elif cur_D < D:
# 天数过少,减少最大载重,天数增加
r = mid
elif cur_D > D:
l = mid + 1
return l
O
(
log
N
⋅
log
N
)
\mathcal{O}(\log N \cdot \log N)
O(logN⋅logN)
O
(
N
)
\mathcal{O}(N)
O(N)
O
(
N
log
N
)
\mathcal{O}(N\log N)
O(NlogN)
O
(
N
2
)
\mathcal{O}(N^2)
O(N2)
O
(
N
k
)
\mathcal{O}(N^k)
O(Nk)
O
(
N
!
)
\mathcal{O}(N!)
O(N!)