【面试必刷TOP101】系列包含:
- 面试必刷TOP101:链表(01-05,Python实现)
- 面试必刷TOP101:链表(06-10,Python实现)
- 面试必刷TOP101:链表(11-16,Python实现)
- 面试必刷TOP101:二分查找/排序(17-22,Python实现)
- 面试必刷TOP101:二叉树系列(23-30,Python实现)
- 面试必刷TOP101:二叉树系列(31-36,Python实现)
- 面试必刷TOP101:二叉树系列(37-41,Python实现)
- 面试必刷TOP101:堆、栈、队列(42-49,Python实现)
- 面试必刷TOP101:哈希表(50-54,Python实现)
- 面试必刷TOP101:递归 / 回溯(55-61,Python实现)
- 面试必刷TOP101:动态规划(入门)(62-66,Python实现)
- 面试必刷TOP101:动态规划(67-71,Python实现)
- 面试必刷TOP101:动态规划(72-77,Python实现)
- 面试必刷TOP101:动态规划(78-82,Python实现)
- 面试必刷TOP101:字符串(83-86,Python实现)
- 面试必刷TOP101:双指针(87-94,Python实现)
- 面试必刷TOP101:贪心算法(95-96,Python实现)
- 面试必刷TOP101:模拟(97-99,Python实现)
面试必刷TOP101:双指针(87-94,Python实现)
87.合并两个有序数组
87.1 合并后排序
class Solution:
def merge(self, A, m, B, n):
for i in range(m,m+n):
A[i] = B[i-m]
A.sort()
return A
87.2 双指针
step 1:使用三个指针,i 指向数组A的最大元素,j 指向数组B的最大元素,k 指向数组A空间的结尾处。
step 2:从两个数组最大的元素开始遍历,直到某一个结束,每次取出较大的一个值放入数组A空间的最后,然后指针一次往前。
step 3:如果数组B先遍历结束,数组A前半部分已经存在了,不用管;但是如果数组A先遍历结束,则需要把数组B剩余的前半部分依次逆序加入数组A前半部分,类似归并排序最后的步骤。
class Solution:
def merge(self, A, m, B, n):
# 指向数组A的结尾
i = m - 1
# 指向数组B的结尾
j = n - 1
# 指向数组A扩容后的结尾
k = m + n - 1
# 从两个数组最大的元素开始,直到一个数组遍历完
while i >= 0 and j >= 0:
if A[i] > B[j]:
A[k] = A[i]
k = k - 1
i = i - 1
else:
A[k] = B[j]
k = k - 1
j = j - 1
# 若A遍历完了,数组B还有,则需要把B加到数组A前面
if i < 0:
while j >= 0:
A[k] = B[j]
k = k - 1
j = j - 1
# 若A遍历完了,数组A前面正好有,不用添加
时间复杂度:
O
(
n
+
m
)
O(n+m)
O(n+m),其中 m、n 分别为两个数组的长度,最坏情况遍历整个数组 A 和数组 B。
空间复杂度:
O
(
1
)
O(1)
O(1),常数级变量,无额外辅助空间。
88.判断是否为回文字符串
88.1 双指针
class Solution:
def judge(self , str: str) -> bool:
a = list(str)
i = 0
j = len(a) - 1
while i < j:
if a[i] == a[j]:
i = i + 1
j = j - 1
else:
return False
return True
时间复杂度:O(n),其中 n 为字符串长度,最多遍历半个字符串。
空间复杂度:O(1),除了常数个临时变量,无额外辅助空间。
不使用双指针其实也可以,主要是前后一起遍历的思想,但双指针节省一半时间。
class Solution:
def judge(self , str: str) -> bool:
for i in range(len(str)):
if str[i] != str[len(str)-1-i]:
return False
return True
88.2 反转字符串比较法
class Solution:
def judge(self , str: str) -> bool:
a = str
str = str[::-1]
if a == str:
return True
return False
时间复杂度:O(n),反转字符串和比较字符串都是 O(n)。
空间复杂度:O(n),辅助字符串 a 记录原来的字符串。
89.合并区间
89.1 排序+贪心
step 1:既然要求重叠后的区间按照起点位置升序排列,我们就将所有区间按照起点位置先进行排序。使用 sort 函数进行排序,重载比较方式为比较 interval 结构的 start 变量。
step 2:排序后的第一个区间一定是起点值最小的区间,我们将其计入返回数组 res,然后遍历后续区间。
step 3:后续遍历过程中,如果遇到起点值小于 res 中最后一个区间的末尾值的情况,那一定是重叠,取二者最大末尾值更新 res 中最后一个区间即可。
step 4:如果遇到起点值大于 res 中最后一个区间的末尾值的情况,那一定没有重叠,后续也不会有这个末尾的重叠区间了,因为后面的起点只会更大,因此可以将它加入 res。
cmp_to_key 是 functools 库里的一个函数,它可以配合 sort 与 sorted 完成自定义排序的功能。
# class Interval:
# def __init__(self, a=0, b=0):
# self.start = a
# self.end = b
from functools import cmp_to_key
class Solution:
def merge(self , intervals: List[Interval]) -> List[Interval]:
res = []
if len(intervals) == 0:
return res
# 按照区间首排序
intervals.sort(key=cmp_to_key(lambda a,b: a.start - b.start))
# 放入第一个区间
res.append(intervals[0])
# 遍历后续区间,查看是否与末尾有重叠
for i in range(len(intervals)):
# 区间有重叠,更新结尾
if intervals[i].start <= res[-1].end:
res[-1].end = max(res[-1].end, intervals[i].end)
# 区间没有重叠,直接加入
else:
res.append(intervals[i])
return res
时间复杂度:
O
(
n
l
o
g
2
n
)
O(nlog2n)
O(nlog2n),排序的复杂度为
O
(
n
l
o
g
2
n
)
O(nlog2n)
O(nlog2n),后续遍历所有区间的复杂度为
O
(
n
)
O(n)
O(n),属于低次幂,忽略。
空间复杂度:
O
(
1
)
O(1)
O(1),res 为返回必要空间,没有使用额外辅助空间。
90.最小覆盖子串
90.1 滑动窗口+哈希表
step 1:建立哈希表,遍历字符串 T,统计各个字符出现的频率,频率计为负数。
step 2:依次遍历字符串 S,如果匹配则将哈希表中的相应的字符加 1。
step 3:在遍历过程中维护一个窗口,如果哈希表中所有元素都大于 0,意味着已经找全了,则窗口收缩向左移动,找最小的窗口,如果不满足这个条件则窗口右移继续匹配。窗口移动的时候需要更新最小窗口,以取得最短子串。
step 4:如果匹配到最后,窗口 left(初始为-1)也没有右移,说明没有找到,返回空串即可。
step 5:最后使用字符串截取函数,截取刚刚记录下的窗口即可得到符合条件的最短子串。
class Solution:
# 检查是否有小于0的
def check(self, dic:dict()):
for key,value in dic.items():
if value < 0:
return False
return True
def minWindow(self , S: str, T: str) -> str:
# cnt 其实就是最小窗口的大小
cnt = len(S) + 1
dic = dict()
# 初始化哈希表都为负数
for i in range(len(T)):
if T[i] in dic:
dic[T[i]] -= 1
else:
dic[T[i]] = -1
# slow、fast 用作移动的双指针
slow = 0
fast = 0
# left、right 用于记录最小窗口的左右边界
left = -1
right = -1
while fast < len(S):
c = S[fast]
if c in dic:
dic[c] += 1
# 没有小于 0 的说明都覆盖了
while self.check(dic):
# 维护最小窗口
if fast - slow + 1 < cnt:
cnt = fast - slow + 1
left = slow
right = fast
# 缩小窗口
c = S[slow]
if c in dic:
dic[c] -= 1
slow += 1
fast += 1
if left == -1:
return ''
return S[left:right+1]
时间复杂度:
O
(
C
∗
n
S
+
n
T
)
O(C*n_S+n_T)
O(C∗nS+nT),其中
C
C
C 为
T
T
T 字符串的字符集大小,本题中为 52 个字母,
n
S
n_S
nS 为字符串
S
S
S 的长度,
n
T
n_T
nT 为字符串
T
T
T 的长度。
空间复杂度:
O
(
C
)
O(C)
O(C),哈希表长度不会超过字符串
T
T
T 的字符集大小。
91.反转字符串
91.1 内置方法
class Solution:
def solve(self , str: str) -> str:
return str[::-1]
91.2 从后遍历
class Solution:
def solve(self , str: str) -> str:
res = ''
for i in range(len(str)-1,-1,-1):
res = res + str[i]
return res
92.最长无重复子数组
92.1 滑动窗口
step 1:构建一个哈希表,用于统计数组元素出现的次数。
step 2:窗口左右界都从数组首部开始,每次窗口优先右移右界,并统计进入窗口的元素的出现频率。
step 3:一旦右界元素出现频率大于1,就需要右移左界直到窗口内不再重复,将左边的元素移除窗口的时候同时需要将它在哈希表中的频率减1,保证哈希表中的频率都是窗口内的频率。
step 4:每轮循环,维护窗口长度最大值。
class Solution:
def maxLength(self , arr: List[int]) -> int:
num = 0
dic = dict() # 哈希表用于记录窗口内非重复的数字
i = 0
for j in range(len(arr)):
# 窗口右移,哈希表统计次数
if arr[j] in dic:
dic[arr[j]] += 1
else:
dic[arr[j]] = 1
while dic[arr[j]] > 1:
# 窗口左移,同时减去该数字出现的次数
dic[arr[i]] -= 1
i = i + 1
num = max(num, j-i+1)
return num
时间复杂度:O(n),外循环窗口右界从数组首右移到数组尾,内循环窗口左界同样如此,因此复杂度
为O(n+n)=O(n)。
空间复杂度:O(n),最坏情况下整个数组都是不重复的,哈希表长度就为数组长度 n。
92.2 模拟队列
class Solution:
def maxLength(self , arr: List[int]) -> int:
num = 0
a = []
for item in arr:
while item in a:
a = a[1:]
a.append(item)
num = max(num,len(a))
return num
93.盛水最多的容器
93.1 贪心思想
我们都知道容积与最短边长和底边长有关,最长的底边一定以首尾为边,但是首尾不一定够高,中间可能会出现更高但是底边更短的情况,因此我们可以使用对撞双指针向中间靠,这样底边长会缩短,因此还想要有更大容积只能是增加最短边长,此时我们每次指针移动就移动较短的一边,因为贪心思想下较长的一边比较短的一边更可能出现更大容积。
step 1:优先排除不能形成容器的特殊情况。
step 2:初始化双指针指向数组首尾,每次利用上述公式计算当前的容积,维护一个最大容积作为返回值。
step 3:对撞双指针向中间靠,但是依据贪心思想,每次指向较短边的指针向中间靠,另一指针不变。
class Solution:
def maxArea(self , height: List[int]) -> int:
if len(height) < 2:
return 0
res = 0
# 双指针
left = 0
right = len(height) - 1
while left < right:
capacity = min(height[left],height[right]) * (right-left)
res = max(res,capacity)
# 优先舍弃较短的边
if height[left] < height[right]:
left = left + 1
else:
right = right - 1
return res
时间复杂度:
O
(
n
)
O(n)
O(n),双指针共同遍历一次数组。
空间复杂度:
O
(
1
)
O(1)
O(1),常数级变量,没有额外辅助空间。
94.接雨水问题
94.1 双指针
我们都知道水桶的短板问题,控制水桶水量的是最短的一条板子。这道题也是类似,我们可以将整个图看成一个水桶,两边就是水桶的板,中间比较低的部分就是水桶的底,由较短的边控制水桶的最高水量。但是水桶中可能出现更高的边,比如上图第四列,它比水桶边还要高,那这种情况下它是不是将一个水桶分割成了两个水桶,而中间的那条边就是两个水桶的边。
有了这个思想,解决这道题就容易了,因为我们这里的水桶有两个边,因此可以考虑使用对撞双指针往中间靠。
step 1:检查数组是否为空的特殊情况
step 2:准备双指针,分别指向数组首尾元素,代表最初的两个边界
step 3:指针往中间遍历,遇到更低柱子就是底,用较短的边界减去底就是这一列的接水量,遇到更高的柱子就是新的边界,更新边界大小。
class Solution:
def maxWater(self , arr: List[int]) -> int:
if len(arr) == 0:
return 0
res = 0
# 左右双指针
left = 0
right = len(arr) - 1
# 中间区域的边界高度
maxL = 0
maxR = 0
while left < right:
# 每次维护往中间的最大边界
maxL = max(maxL,arr[left])
maxR = max(maxR,arr[right])
# 较短的边界确定该格子的水量
if maxL <= maxR:
res = res + (maxL - arr[left])
left = left + 1
else:
res = res + (maxR - arr[right])
right = right - 1
return res
时间复杂度:
O
(
n
)
O(n)
O(n),两个指针最多共同遍历整个数组。
空间复杂度:
O
(
1
)
O(1)
O(1),常数个变量,没有额外的辅助空间。