一、数组双指针、滑动窗口
1.练习题目(第12天)
此题要求O(1)的空间复杂度,我们可以先尝试我们最可能先想到的方法,一层for循环简单遍历结合二分来解答此题:
class Solution:
def reverseString(self, s: List[str]) -> None:
"""
Do not return anything, modify s in-place instead.
"""
mid=len(s)//2 #求出s字符串的中心点
for i in range(0,mid):
s[i],s[len(s)-i-1]=s[len(s)-i-1],s[i]
可以看到一次循环遍历所需要的空间复杂度很低,再让我们尝试用双指针来解决此问题:
class Solution:
def reverseString(self, s: List[str]) -> None:
"""
Do not return anything, modify s in-place instead.
"""
left = 0 # 初始化左边界为数组的第一个索引
right = len(s) - 1 # 初始化右边界为数组的最后一个索引
while left < right: # 在左边界小于右边界的条件下进行迭代
# 交换左右边界对应的元素
s[left], s[right] = s[right], s[left]
# 左边界向右移动一步
left += 1
# 右边界向左移动一步
right -= 1
结果我们可以看到在空间复杂度上两者相差不大,但在时间复杂度上,双指针的方法是明显优于我们能想到的简单直接的方法的。
这道题的题意表达的不是很清楚,很容易引起误解,经过代码测试,这里的反转元音字母是指反转左、右遍历按次序遇到的元音,左侧第一个元音与右侧第一个元音交换,左侧第二个元音与右侧第二个元音交换...明白了题目,让我们用二指针遍历来实现此功能:
class Solution:
def reverseVowels(self, s: str) -> str:
st1 = list(s) # 将输入的字符串转换为列表形式
left = 0 # 左指针初始位置
right = len(s) - 1 # 右指针初始位置
st = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'] # 元音字母列表
index1, index2 = 0, 0 # 初始化元音字母的索引位置
st2 = '' # 初始化结果字符串
flag1 = False # 标记左边是否找到元音字母
flag2 = False # 标记右边是否找到元音字母
while left < right:
if st1[left] in st: # 判断左边是否为元音字母
index1 = left # 记录左边元音字母的索引位置
flag1 = True # 设置左边元音字母标记为True
else:
left += 1 # 若左边不是元音字母则左指针右移
if st1[right] in st: # 判断右边是否为元音字母
index2 = right # 记录右边元音字母的索引位置
flag2 = True # 设置右边元音字母标记为True
else:
right -= 1 # 若右边不是元音字母则右指针左移
if flag1 and flag2: # 如果左右两边都找到了元音字母
st1[index1], st1[index2] = st1[index2], st1[index1] # 交换左右两边元音字母的位置
index1, index2 = 0, 0 # 重置元音字母的索引位置
flag1, flag2 = False, False # 重置元音字母的标记
left += 1 # 左指针右移
right -= 1 # 右指针左移
st2 = "".join(st1) # 将列表转换为字符串
return st2 # 返回结果字符串
有没有更快捷的方法呢:
我们可以使用正则表达式:re.findall(),它可以在字符串中查找匹配要求字符的所有非重叠子串,并将它们以列表的形式返回。例如 s
的取值为 "Hello World",那么 re.findall(r"[aeiouAEIOU]", s)
将返回 ['e', 'o', 'o']
,这是字符串中的所有元音字母,而pop()
是 Python 列表的一个方法,用于移除并返回列表的最后一个元素
代码如下:
class Solution:
def reverseVowels(self, s: str) -> str:
import re
a = re.findall(r"[aeiouAEIOU]", s) # 使用正则表达式找到字符串 s 中的所有元音字母,并保存到列表 a 中
ans = "" # 初始化一个空字符串 ans,用于保存结果
for i in s: # 遍历字符串 s 中的每个字符
if i not in "aeiouAEIOU": # 如果当前字符不是元音字母
ans += i # 直接将当前字符添加到结果字符串 ans 中
else:
ans += a.pop() # 如果当前字符是元音字母,从列表 a 中取出一个元音字母,并将其添加到结果字符串 ans 中
return ans # 返回结果字符串 ans
可以看到结果优化了很多,这也是python的强大之处。
这道题的结果判定很简单,但难的就是它特别备注的事项:答案中不可包含重复的三元组,既然是查找有特定相加结果元素的题,我们还可以用双指针来做,先对数组进行排序以便初步筛选,然后我们可以先固定首个元素nums[i],再把左指针定为i+1,右指针定为len(nums)-1,然后从左右两边遍历以避免重复,即便如此,元素也还是会出现重复,我们在选择过第一个i元素后,如果第二个i元素与第一个相同,我们完全可以跳过第二个i元素,然后如果第一个left元素与下一个left元素相同,我们可以直接判定下一个left元素,同理,若right元素与right-1的元素相同,我们可以直接判定right-1元素来避免重复,具体代码如下:
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
result = []
# 如果数组长度小于3,无法构成三个数的组合
if n < 3:
return []
else:
# 对数组进行排序,方便后面的双指针操作
nums.sort()
for i in range(n):
# 如果当前值大于0,后面的数都会大于0,无法满足和为0的条件
if nums[i] > 0:
return result
# 跳过重复的值,避免出现重复的结果
if i >= 1 and nums[i] == nums[i-1]:
continue
left = i + 1
right = n - 1
while left < right:
if nums[i] + nums[left] + nums[right] == 0:
# 如果找到满足和为0的三个数,加入结果列表
result.append([nums[i], nums[left], nums[right]])
# 跳过重复的值,避免出现重复的结果
while left < right and nums[left] == nums[left+1]:
left += 1
while left < right and nums[right] == nums[right-1]:
right -= 1
# 移动指针继续搜索下一组解
left += 1
right -= 1
elif nums[i] + nums[left] + nums[right] > 0:
# 当前和大于0,右指针左移减小和的值
right -= 1
else:
# 当前和小于0,左指针右移增大和的值
left += 1
return result
2.练习题目(第13天)
这题可以用简单直接地方法解决,直接遍历寻找并用pop函数删除指定元素即可:
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# 从列表末尾开始遍历
for i in range(len(nums)-1, -1, -1):
if nums[i] == val: # 如果当前元素等于给定值
nums.pop(i) # 使用pop()方法删除当前元素
return len(nums) # 返回删除后剩余元素的个数
当然,我们学习了双指针,这题也可以用双指针来解决,代码使用两个指针 first和 second进行操作。指针 first 用于遍历整个列表,指针second用于指向下一个待赋值的位置。遍历过程中,当指针 first 指向的元素不等于给定值 val
时,将该元素赋值给指针second指向的位置,然后将指针 second向后移动一位。最后返回指针 second的值,即删除目标元素后的新长度。在时间和空间复杂度上均有较大提升:
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
first = 0 # 第一个指针,用于遍历整个列表
second = 0 # 第二个指针,指向下一个待赋值的位置
while first < len(nums):
if nums[first] != val: # 当前元素不等于给定值时
nums[second] = nums[first] # 将当前元素赋值给第二个指针指向的位置
second+= 1 # 第二个指针向后移动一位
first += 1 # 第一个指针向后移动一位
return second # 返回第二个指针的值,即删除目标元素后的新长度
此题与上一题的类型很相似,我们也可以用双指针的方法来解决,使用两个指针
first
和 second
进行操作。指针 first
用于遍历整个列表,指针 second
用于指向下一个不重复元素的位置,当指针 second
前面有两个元素或者当前元素与指针 second
前面的第二个元素不相等时,将当前元素赋值给指针 second
指向的位置,并将指针 second
向后移动一位。如果当前元素与指针 second
前面的第二个元素相等,则跳过当前元素。最后返回指针 second
的值,即去重后的新长度:
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
second = 0 # 第二个指针初始位置为0,用于指向下一个不重复元素的位置
for first in range(len(nums)): # 第一个指针,遍历整个列表
# 如果第二个指针前面有两个元素或者当前元素与第二个指针前面的第二个元素不相等
if second < 2 or nums[second - 2] != nums[first]:
nums[second] = nums[first] # 将当前元素赋值给第二个指针指向的位置
second += 1 # 第二个指针向后移动一位
elif second >= 2 and nums[second - 2] == nums[first]:
continue # 当前元素与第二个指针前面的第二个元素相等,跳过当前元素
return second # 返回第二个指针的值,即去重后的新长度
这一题我们依旧可以用双指针的办法来解决,但有个难点是如何在保持字母出现顺序不变的情况下,统计name和typed中的相应字母出现次数并比较是否name中该字母出现次数小于等于typed中该字母出现次数,我们可以再循环开始前用name_0和typed_0来储存起始位置,最后用name和typed分别减去起始位置来统计两字符串中的对应字母出现次数:
class Solution:
def isLongPressedName(self, name: str, typed: str) -> bool:
left, right = 0, 0 # 初始化左右指针分别指向name和typed的开头
while left < len(name) and right < len(typed): # 当左指针未越界且右指针未越界时,进行循环
temp = name[left] # 记录当前name中的字符
left_0 = left # 记录当前位置为初始位置
right_0 = right # 记录当前位置为初始位置
while left < len(name) and name[left] == temp: # 在name中找到连续相同的字符,更新左指针位置
left += 1
if typed[right] != temp: # 如果当前位置的typed字符与temp不相同,说明不符合长按键入条件
return False
while right < len(typed) and typed[right] == temp: # 在typed中找到连续相同的字符,更新右指针位置
right += 1
if left - left_0 > right - right_0: # 如果name中的字符个数大于typed中的字符个数,说明不符合长按键入条件
return False
if right == len(typed) and left == len(name): # 当左指针和右指针都走到字符串末尾时,说明符合长按键入条件
return True
else:
return False
3.练习题目(第14天)
我们很容易看出来这是一个滑动窗口的题目,让我们用简单的滑动窗口思路来尝试一下,创建一个起始窗口,再创建一个滑动窗口,在遍历元素的过程中,将滑动窗口的总和与原窗口的和比较,若滑动窗口的和更大则更新起始窗口:
class Solution:
def findMaxAverage(self, nums: List[int], k: int) -> float:
left_0 = 0 # 初始窗口的左边界
right_0 = k - 1 # 初始窗口的右边界
left = left_0 + 1 # 移动窗口的左边界
right = right_0 + 1 # 移动窗口的右边界
num_0 = sum(nums[left_0:right_0 + 1]) # 计算初始窗口内元素的和
while right < len(nums): # 当窗口右边界未越界时进行循环
num = sum(nums[left:right + 1]) # 计算移动窗口内元素的和
if num > num_0: # 如果移动窗口内元素的和大于初始窗口内元素的和
left_0 = left # 更新初始窗口的左边界为移动窗口的左边界
right_0 = right # 更新初始窗口的右边界为移动窗口的右边界
num_0 = num # 更新初始窗口内元素的和为移动窗口内元素的和
else:
left += 1 # 否则向右移动窗口,更新左边界和右边界
right += 1
return num_0 / k # 返回最大平均值
但结果却因时间复杂度过高而无法处理大量数据导致超时,让我们对代码做一些优化,在优化后的代码中,我们只用一个滑动窗口,并在此基础上通过对元素的添加和删除来改变窗口元素的总和,保留总和更大的值:
class Solution:
def findMaxAverage(self, nums: List[int], k: int) -> float:
left = 0 # 滑动窗口的左边界
num = sum(nums[:k]) # 初始化窗口内元素的和为前k个数的和
result = num / k # 初始化结果为窗口内元素的平均值
for right in range(k, len(nums)): # 遍历数组,right为窗口右边界(包含)
num += nums[right] # 向右移动窗口,加上新的右侧元素
num -= nums[left] # 左边界元素移出窗口,减去左侧元素
result = max(result, num / k) # 更新结果为当前窗口内元素的平均值和之前的结果的较大值
left += 1 # 左边界向右移动一个位置
return result # 返回最大平均值
该题仍是滑动窗口的典型题,难点在于对特殊情况的处理, 对只有一个元素的数组、从头到尾全递增的数组的处理需要特别注意:
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
count, l, r = 0, 0, 1 # 初始化计数器count和左右指针l、r
flag = False # 标志变量,用于判断是否进入if语句块
if len(nums) == 1: # 特殊情况处理:数组只有一个元素,直接返回1
return 1
while r < len(nums): # 循环进行遍历,直到右指针越界为止
if nums[r] > nums[r-1]: # 如果当前元素大于前一个元素,表示递增
r += 1 # 右指针向右移动一位
if r == len(nums): # 如果右指针已经到达数组末尾
count = max(count, r - l) # 更新计数器count为当前窗口长度和之前的最大值
else: # 如果当前元素不大于前一个元素,表示出现了非递增情况
count = max(count, r - l) # 更新计数器count为当前窗口长度和之前的最大值
l = r # 左指针跳到右指针位置
r = r + 1 # 右指针右移一位
flag = True # 设置标志变量为True,表示进入了if语句块
return count # 返回最长递增子序列的长度
这道题的难点在于如何处理反转的k个0对于窗口中1的个数的影响,我们在统计中可以直接统计窗口的长度,然后保证窗口中的0的个数等于k,就能选出在翻转0后最长的1字符串:
class Solution(object):
def longestOnes(self, nums, k):
left = 0 # 左指针,表示滑动窗口的起始位置
count = 0 # 计数器,统计窗口中0的个数
ans = 0 # 最长连续1的长度
for index, data in enumerate(nums): # 使用enumerate同时获取元素和索引
if not data: # 如果当前元素为0
count += 1 # 计数器count加1
while left <= index and count > k: # 当窗口中0的个数大于k时,移动左指针缩小窗口
if not nums[left]: # 如果左指针指向的元素为0
count -= 1 # 计数器count减1
left += 1 # 左指针右移一位
ans = max(ans, index - left + 1) # 更新最长连续1的长度
return ans # 返回最长连续1的长度