相信许多正在为算法面试做着准备刷着题的程序员都会有类似的焦虑:我刷够题了吗?还要再来点吗?到底刷到什么程度才够呢?
刷题究竟应该怎么刷?
这边对常见的解题模式(套路),分析了 相关问题如何进行识别,给出了 具体的模板,同时每个模式都列出了 若干经典题和高频题,在实战中加深理解。
P.S. 本文为个人刷题心得总结,如有问题,欢迎交流探讨
二分法基本思路(基本原理)
二分法,也就是二分查找,用二分的方式去查找。
简单来讲,二分查找的 核心思路 就是 每次都取中间项进行判断,然后利用列表的有序性在 O(1) 的时间内将问题的规模缩小至一半(砍掉一半的项)【从 n 到 n/2 再到 n/4……,最终到 1,即查找完毕】,具体步骤如下:
mid = (start + end) / 2
,但这种方式可能会出现 整数越界,因此一般使用这种写法:mid = start + (end — start) / 2
【Python没有这种顾虑】nums[mid]
是否满足 特定条件,利用列表的有序性砍掉剩余项的一半换句话说,如果我们想用二分法的框架来解决一个问题,那么我们就需要 找到一个判断条件,我们可以通过这个判断条件,来确定目标项是位于左半段还是右半段
二分法细节
二分法思路虽然简单,但是细节很魔鬼,稍不注意就可能导致死循环。
① start 和 end 指针如何变化(要不要,能不能把 mid 给剔除掉?什么时候可以什么时候不行?)
② 循环结束条件(两指针相邻?重合?交叉?)
这边给出一个二分法的通用模板。
所谓“通用”,就是指 在各种情形下都不会出问题(比如陷入死循环等),可以 放心大胆地,闭着眼睛去用,从而可以减少思考复杂度,让你可以把注意力集中在算法实现上。
有三个关键点,
nums[start]
和nums[end]
分别进行判断(判断的先后顺序因问题而异)。以在排序数组中寻找target为例
class Solution:
def search(self, nums: List[int], target: int) -> int:
if nums == []:
return -1
start = 0
end = len(nums) - 1
while start + 1 < end:
mid = start + end // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
start = mid
else: # target < nums[mid]
end = mid
if nums[start] == target:
return start
elif nums[end] == target:
return end
else: # 目标不存在
return -1
这边简单列举了几种查找情况下,nums[mid] == target
时应该如何分析和处理
可以看到,第一个等于和第一个大于等于的处理方式是相同的,和第一个大于是相反的。
class Solution:
"""
@param nums: An integer array sorted in ascending order
@param target: An integer
@return: An integer
"""
def lastPosition(self, nums, target):
# write your code here
if nums == []:
return -1
start = 0
end = len(nums) - 1
while start + 1 < end:
mid = (start + end) // 2
if nums[mid] <= target:
start = mid
else:
end = mid
# 因为是查找last position,所以先判断nums[end]
if nums[end] == target:
return end
elif nums[start] == target:
return start
else: # 目标不存在
return -1
# 用两次二分法,分别找到 first position 和 last position
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
if nums == []:
return [-1, -1]
# 查找目标的第一个位置
start ,end = 0, len(nums) - 1
while start + 1 < end:
mid = (start + end) // 2
if nums[mid] < target:
start = mid
else:
end = mid
if nums[start] == target:
left_bound = start
elif nums[end] == target:
left_bound = end
else: # 目标不存在
return [-1, -1]
# 查找目标的最后一个位置
start ,end = left_bound, len(nums) - 1
while start + 1 < end:
mid = (start + end) // 2
if nums[mid] <= target:
start = mid
else:
end = mid
if nums[end] == target:
right_bound = end
elif nums[start] == target:
right_bound = start
return [left_bound, right_bound]
# 可以转换成:
# - 查找最后一个小于 target 的数字的位置,+ 1 即为答案
# - 反过来,也可以是查找第一个大于等于 target 的数字的位置。
# - 两者其实等效的(就只是返回值需不需要 +1 的区别),个人感觉 “带等于” 的更好分析一些
class Solution:
"""
@param A: A list of integer
@return: The number of element in the array that
are smaller that the given integer
"""
def countOfSmallerNumber(self, A, queries):
A = sorted(A)
res = []
for query in queries:
res.append(self.count_smaller(A, query))
return res
def count_smaller(self, A, query):
'''核心函数'''
start = 0
end = len(A) - 1
while start + 1 < end:
mid = (start + end) // 2
if A[mid] < query:
start = mid
else:
end = mid
if query <= A[start]:
return start
elif query <= A[end]:
return end
else:
return end + 1
# 简单来讲就是寻找第一个 isBadVersion 为 True 的版本。
# 典型的给定一个 XXOO 的序列,寻找第一个 O 的位置的问题。
class Solution:
def firstBadVersion(self, n):
"""
:type n: int
:rtype: int
"""
start = 1
end = n
while start + 1 < end:
mid = start + (end - start) // 2
if isBadVersion(mid):
end = mid
else:
start = mid
if isBadVersion(start):
return start
elif isBadVersion(end):
return end
根据 山脉数组的性质,nums[mid]只可能有三种情况,如下图所示
# 查找nums[i],满足 nums[i-1] < nums[i] and nums[i] > nums[i+1]
class Solution:
def peakIndexInMountainArray(self, nums: List[int]) -> int:
start = 0
end = len(nums) - 1
while start + 1 < end:
mid = (start + end) // 2
if nums[mid] < nums[mid - 1]:
end = mid
elif nums[mid] < nums[mid + 1]: # nums[mid - 1] < nums[mid] < nums[nums + 1]
start = mid
else: # nums[mid - 1] < nums[mid] and nums[mid] > nums[nums + 1]
return mid
if nums[start] < nums[end]:
return end
else:
return start
遍历数组,寻找最小值
对于旋转数组的题,数组中不存在重复元素这一条件非常重要,否则无法砍掉一半项(举个极端点的例子,111011111)
解决这个问题,需要 利用到 旋转数组 (Rotated Sorted Array) 的性质。
旋转数组大概是这样的形状
我们可以把最小点转换为 第一个小于等于 nums[-1] 的点
为什么是第一个小于等于 num[-1] 的点,能不能是第一个小于 nums[0] 的点?
列举一下所有的特殊情况 就可以总结出来。
这样一来其实就变成XXOO的形式了。
class Solution:
# @param nums: a rotated sorted array
# @return: the minimum number in the array
def findMin(self, nums):
if nums == []:
return -1
start = 0
end = len(nums) - 1
target = nums[-1]
while start + 1 < end:
mid = (start + end) // 2
if nums[mid] <= target:
end = mid
else:
start = mid
if nums[start] <= target:
return nums[start]
else:
return nums[end]
一次二分,先确定mid在前半段还是后半段,然后进一步判断 target 是否在剩余项的有序半边上
效率要更高一些
class Solution:
def search(self, nums: List[int], target: int) -> int:
if len(nums) == 0:
return -1
start, end = 0, len(nums) - 1
while start + 1 < end:
mid = start + (end - start) // 2
if nums[mid] == target:
return mid
# nums[mid] != target
if nums[start] < nums[mid]: # mid落在前半段
if nums[start] <= target and target < nums[mid]: # target可能在有序的前半段
end = mid
else:
start = mid
else: # mid 落在后半段上
if nums[mid] < target <= nums[end]: # target可能在有序的后半段
start = mid
else:
end = mid
if nums[start] == target:
return start
if nums[end] == target:
return end
return -1
两次二分,先用二分法找到分割点 (最小值点) ,再在有序一侧上用二分法查找目标值
容易思考和实现(思考复杂度比较低)
时间复杂度为 O(logn) + O(logn)
class Solution:
def search(self, nums: List[int], target: int) -> int:
if nums == []:
return -1
min_index = self.find_min_index(nums)
if nums[-1] < target: # 判断target可能在左半段还是右半段
return self.binary_search(nums, 0, min_index - 1, target)
return self.binary_search(nums, min_index, len(nums) - 1, target)
def find_min_index(self, nums):
start = 0
end = len(nums) - 1
target = nums[-1]
while start + 1 < end:
mid = (start + end) // 2
if nums[mid] <= target:
end = mid
else:
start = mid
if nums[start] <= target:
return start
return end
def binary_search(self, nums, start, end, target):
if start > end: # 输入为空数组
return -1
while start + 1 < end:
mid = (start + end) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
start = mid
else:
end = mid
if nums[start] == target:
return start
if nums[end] == target:
return end
return -1