滑动窗口思想(数组)-python


前言

滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n2 )的暴力解法降为O(n)。主要要理解滑动窗口如何移动窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合题目条件的长度。


一、思想

那么滑动窗口是如何用一个for循环来完成整个操作的呢?
首先思考用一个for循环,那么应该表示滑动窗口的起始位置,还是终止位置。
如果只用一个for循环来控制滑动窗口的起始位置,那么剩下的终止位置如何遍历?这就又回到了暴力解法。
所以只用一个for循环的话,那么这个循环的索引,一定是表示滑动窗口的终止位置。那么滑动窗口的起始位置如何操作移动呢?
实现滑动窗口,主要确定如下三点:
(1) 窗口内是什么?
(2) 如何移动窗口的起始位置?
(3) 如何移动窗口的结束位置?
窗口就是满足长度最小的符合题目条件的连续子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
在这里插入图片描述
通过这个图例可以体会到滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n2 )暴力解法降为O(n)。

二、相关题目讲解

1.长度最小的子数组(leetcode 209.)

代码如下(示例):

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        l=len(nums)
        left=0
        right=0
        min_len=float('inf')#正无穷
        cur_sum=0      
        for right in range(0,l):
            cur_sum += nums[right]            
            while cur_sum >= target: # 当前累加值大于目标值
                min_len = min(min_len, right - left + 1)
                cur_sum -= nums[left]
                left += 1   #移动滑动窗口起始位置         
            right += 1  #移动滑动窗口结束位置       
        return min_len if min_len != float('inf') else 0

时间复杂度是O(n),有同学可能会问为什么不是O(n2 )呢,因为一个for循环下,虽然里面还放一个while循环,但是主要看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也依旧是O(n)。

2.水果成篮(leetcode 904.)

代码如下(示例):

class Solution:
    def totalFruit(self, fruits: List[int]) -> int:
        l=len(fruits)
        left=0
        res=0
        classMap=defaultdict(int)#
        classCnt=0
        for right in range(0,l):
            if classMap[fruits[right]]==0:
                classCnt+=1
            classMap[fruits[right]]+=1
            while classCnt>2:
                if classMap[fruits[left]]==1:
                    classCnt-=1
                classMap[fruits[left]]-=1
                left+=1
                #一旦满足条件,更新结果
            res = max(res, right - left + 1)
        return res

本题区别于76题,这道题求的是最大滑窗,最大滑窗模版:给定数组 nums,定义滑窗的左右边界 left, right,求满足某个条件的滑窗的最大长度。关键的区别在于,最大滑窗是在迭代右移右边界的过程中更新结果,而最小滑窗是在迭代右移左边界的过程中更新结果。因此虽然都是滑窗,但是两者的模板和对应的贪心思路并不一样。

while right < len(nums):#滑动窗口结束位置的遍历
    判断[left, right]是否满足条件
    while 不满足条件:
        left += 1 (最保守的压缩i,一旦满足条件了就退出压缩i的过程,使得滑窗尽可能的大)
    不断更新结果(注意在while外更新!)
    right+= 1

3.最小覆盖子串(leetcode 76.)

class Solution:
    def minWindow(self, s: str, t: str) -> str:
    	from collections import Counter#快速计数
    	template_dict=Counter(t)#统计目标字符串里各字符的个数
    	window_dict={}#定义了一个滑动窗口字典
    	for each_key in template_dict:#对于目标字符串的每个字符
    		if each_key not in window_dict:#如果不在滑动窗口里
    			window_dict[each_key]=0#就赋值为零
    	def isContains(cur_dict,tmp_dict):
    		for each_key in tmp_dict:
    			if cur_dict[each_key]<tmp_dict[each_key]:#如果滑动窗口
    				return False
    		return True
    	start=0#定义滑动窗口起始位置
    	min_len=float('inf')#定义滑动窗口长度
    	res=''
    	for end in range(len(s)):#滑动窗口结束位置的遍历
    		if s[end] in template_dict:#如果字符串的字符出现在目标字符串里
    			window_dict[s[end]]+=1#滑动窗口对应字符附值+1
    		while isContains(window_dict,template_dict):#滑动窗口满足目标要求
    			if min_len>end-start+1
    				min_len=end-start+1
    				res=s[start:end+1]
    			if s[start] in window_dict:
    				window_dict[s[start]]-=1
    			start+=1
    	return res

本题是最小滑窗思路,最小滑窗模板:给定数组 nums,定义滑窗的左右边界 left, right,求满足某个条件的滑窗的最小长度。滑动窗口简单说就是右指针先出发,左指针视情况追赶右指针。因此,右指针最多遍历一遍数组,左指针也最多遍历一次数组,时间复杂度不超过O(2N)。接下来,如何判断滑动窗口内是否满足题设条件,有两种选择:(1) 要么你遍历这个滑窗,通过遍历来断滑窗是否满足需要O(N), 那么总的时间就退化为O(N2), (2) 要么你选择字典,用空间换时间,那么判断划窗是否满足条件则需要 O(1),总时间为O(N).

while right < len(nums):
    判断[eft, right]是否满足条件
    while 满足条件:
        不断更新结果(注意在while内更新!)
        left += 1 (最大程度的压缩i,使得滑窗尽可能的小)
    right += 1

三、 模拟行为

螺旋矩阵II(leetcode.59)

这道题在面试中出现频率较高,不涉及算法,就是模拟过程,考察候选人的代码能力
模拟顺时针画矩阵的过程:
上行从左到右
右列从上到下
下行从右到左
左列从下到上
由外向内一圈一圈这么画下去。

发现边界条件非常多,在一个循环中,如此多的边界条件,如果不按固定规则来遍历,就容易陷入循环陷阱。
思路:

startx=0,starty=0
offset=1
count=1
while(n/2){
	for(j=starty;j<n-offset;j++) #从左到右
		nums[startx][j]=count++;
	for(i=startx;i<n-offset;i++)#从上到下
		nums[i][j]=count++;
	for(j=n-offset;j>s;j--)#从右到左
		nums[i][j]=count++;
	for( ;i>startx;i--)
		nums[i][j]=count++;

具体实现代码:
在这里插入图片描述

class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        nums = [[0] * n for _ in range(n)]
        startx, starty = 0, 0               # 起始点
        loop, mid = n // 2, n // 2          # 迭代次数、n为奇数时,矩阵的中心点
        count = 1                           # 计数
        for offset in range(1, loop + 1) :      # 每循环一层偏移量加1,偏移量从1开始
            for i in range(starty, n - offset) :    # 从左至右,左闭右开
                nums[startx][i] = count
                count += 1
            for i in range(startx, n - offset) :    # 从上至下
                nums[i][n - offset] = count
                count += 1
            for i in range(n - offset, starty, -1) : # 从右至左
                nums[n - offset][i] = count
                count += 1
            for i in range(n - offset, startx, -1) : # 从下至上
                nums[i][starty] = count
                count += 1                
            startx += 1         # 更新起始点
            starty += 1

        if n % 2 != 0 :			# n为奇数时,填充中心点
            nums[mid][mid] = count 
        return nums

复杂度分析:时间复杂度:O(n2),其中 n是给定的正整数。矩阵的大小是 nxn,需要填入矩阵中的每个元素。
空间复杂度:O(1)。除了返回的矩阵以外,空间复杂度是常数。


leetcode 54.螺旋矩阵

class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        if not matrix or not matrix[0]:
            return list()        
        rows, columns = len(matrix), len(matrix[0])
        order = list()
        left, right, top, bottom = 0, columns - 1, 0, rows - 1
        while left <= right and top <= bottom:
            for column in range(left, right + 1):
                order.append(matrix[top][column])
            for row in range(top + 1, bottom + 1):
                order.append(matrix[row][right])
            if left < right and top < bottom:
                for column in range(right - 1, left, -1):
                    order.append(matrix[bottom][column])
                for row in range(bottom, top, -1):
                    order.append(matrix[row][left])
            left, right, top, bottom = left + 1, right - 1, top + 1, bottom - 1
        return order

时间复杂度:O(mn),其中 m 和 n分别是输入矩阵的行数和列数。矩阵中的每个元素都要被访问一次。
空间复杂度:O(1)。除了输出数组以外,空间复杂度是常数。


剑指Offer 29. 顺时针打印矩阵

class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        if not matrix or not matrix[0]:
            return list()
        
        rows, columns = len(matrix), len(matrix[0])
        order = list()
        left, right, top, bottom = 0, columns - 1, 0, rows - 1
        while left <= right and top <= bottom:
            for column in range(left, right + 1):
                order.append(matrix[top][column])
            for row in range(top + 1, bottom + 1):
                order.append(matrix[row][right])
            if left < right and top < bottom:
                for column in range(right - 1, left, -1):
                    order.append(matrix[bottom][column])
                for row in range(bottom, top, -1):
                    order.append(matrix[row][left])
            left, right, top, bottom = left + 1, right - 1, top + 1, bottom - 1
        return order

同样的解题思路,时间复杂度:O(mn),其中 m 和 n分别是输入矩阵的行数和列数。矩阵中的每个元素都要被访问一次。
空间复杂度:O(1)。除了输出数组以外,空间复杂度是常数。


总结

滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。最容易想到的暴力解法,步骤是一个for循环控制滑动窗口的起始位置,另一个for循环控制滑动窗口的终止位置,用两个for循环完成了一个不断搜索区间的过程,时间复杂度是O(n2 )。而滑动窗口的精妙之处在于,用一个for循环完成区间的搜索,时间复杂度是O(n)。

### 回答1: 滑动窗口的最大值指的是一个固定长度的数字序列中的最大值。它通常用于数据流处理,并且可以使用一个队列来维护滑动窗口内的数字。 以下是一个用 Python 实现滑动窗口的最大值的示例代码: ``` from collections import deque def max_in_sliding_window(nums, k): if not nums: return [] q = deque() res = [] for i in range(len(nums)): while q and nums[q[-1]] <= nums[i]: q.pop() q.append(i) if q[0] == i - k: q.popleft() if i >= k - 1: res.append(nums[q[0]]) return res ``` 其中 `nums` 是输入数字序列,`k` 是滑动窗口的长度。该代码返回每个滑动窗口的最大值组成的列表。 ### 回答2: 滑动窗口的最大值是一种常用的算法思想,可以用于解决一些与连续子数组相关的问题。在Python中,我们可以使用双端队列实现滑动窗口的最大值查找。 具体实现过程如下: 1. 首先,我们需要创建一个双端队列(deque)来存储滑动窗口中的元素索引。我们将队列中的索引按照元素大小降序排列,以便可以快速找到最大值。 2. 然后,我们需要遍历整个数组,并在每次迭代中执行以下步骤: a. 检查双端队列的头部元素(即索引)是否已经超出当前窗口的范围。如果超出了范围,我们需要将其从队列中弹出。 b. 将当前元素添加到队列中。在添加之前,我们需要将队列中所有小于当前元素的索引都弹出,以保持队列中的索引按照元素大小降序排列。 c. 检查当前索引与窗口的起始位置的差值是否大于等于窗口的大小K。如果是,说明当前窗口已经满了,我们可以将队列的头部元素作为当前窗口的最大值。 3. 最后,我们可以将所有窗口的最大值存储在一个数组中,并返回该数组作为最终结果。 以下是一个示例代码: ``` from collections import deque def maxSlidingWindow(nums, k): result = [] queue = deque() for i in range(len(nums)): # 检查队列的头部元素是否超出窗口范围 if queue and queue[0] <= i - k: queue.popleft() # 弹出所有小于当前元素的索引 while queue and nums[queue[-1]] < nums[i]: queue.pop() # 将当前元素添加到队列中 queue.append(i) # 检查窗口是否已经满了 if i >= k - 1: result.append(nums[queue[0]]) return result # 测试 nums = [1,3,-1,-3,5,3,6,7] k = 3 print(maxSlidingWindow(nums, k)) ``` 以上代码的输出为:[3, 3, 5, 5, 6, 7]。 这样,我们就成功实现了滑动窗口的最大值查找的功能。 ### 回答3: 滑动窗口的最大值是指在一个给定数组中,长度为k的窗口从左至右滑动,每次滑动窗口都需要找到窗口中的最大值。下面是一个用Python实现滑动窗口最大值的示例代码: ``` def find_max_in_window(arr, k): if not arr or k <= 0 or k > len(arr): return [] max_nums = [] deque = [] for i in range(len(arr)): # 检查队首元素是否还在窗口范围,如果不在则删除 if deque and deque[0] <= i - k: deque.pop(0) # 如果当前遍历元素大于队尾元素,删除队尾元素直到当前元素小于队尾元素或队列为空 while deque and arr[i] >= arr[deque[-1]]: deque.pop() deque.append(i) # 当前窗口已形成 if i >= k - 1: max_nums.append(arr[deque[0]]) return max_nums ``` 在这个代码中,我们使用一个双端队列(deque)来保存当前窗口中的元素索引。其中deque的特点是队首元素是当前窗口中的最大值,队列中较小的元素按照递减顺序排列。在遍历数组时,我们首先检查队首元素是否还在窗口范围内,如果不在窗口范围内,删除队首元素。然后,我们将当前元素与队列中的元素进行比较,如果当前元素比队尾元素大,删除队尾元素,直到当前元素小于队尾元素或队列为空。最后,将当前元素的索引加入队列中。 当遍历到第k个元素时,当前窗口已经形成,此时队首元素就是当前窗口中的最大值,将其添加到结果集中。接着,继续遍历数组,每次遍历窗口滑动一次,重复上述步骤找到最大值,直到遍历完整个数组。 调用该函数,可以输出滑动窗口的最大值。如下所示: ``` arr = [1, 3, -1, -3, 5, 3, 6, 7] k = 3 max_nums = find_max_in_window(arr, k) print(max_nums) ``` 输出结果为:[3, 3, 5, 5, 6, 7],表示滑动窗口的最大值为3, 3, 5, 5, 6, 7。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值