劍指Offer59-滑動窗口的最大值

劍指Offer典型題整理 - 爭取做最好的題解

整理時間:2020年02月19日

本題和 LeetCode-239 相同

1 題目描述

給定一個數組 nums 和滑動窗口的大小 k,請找出所有滑動窗口裏的最大值。

示例

輸入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
輸出: [3,3,5,5,6,7] 
解釋: 

  滑動窗口的位置                最大值
------------------------------------
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

2 題解

看似簡單的一道題,內有大乾坤!

2.1 暴力法 O(n·k)

最容易想到的也肯定能想到的就是暴力法了,時間複雜度O(kn)O(k·n),寫法如下:

class Solution(object):
    def maxSlidingWindow(self, nums, k):
        if len(nums) == 0:
            return []
        ans = []
        for i in range(len(nums) - k + 1):
            cur_min = nums[i]
            for j in range(i, i + k):
                cur_min = max(cur_min, nums[j])
            ans.append(cur_min)
        return ans

2.2 暴力解法優化

暴力解法中包含了許多重複的不必要的判斷,如果能夠避免這些額外的判斷也能大大的降低其複雜度,如下圖中第1和第2個窗口的最大值均爲同一個值3,在這時候加上一些判斷就能避免重新在窗口中遍歷一遍。其算法思路如下:

  1. 使用雙指針left, right確定窗口左右邊界首先得到最左邊的窗口,並求得該窗口中的最大值cur_max,將當前的cur_max加入到ans數組中,將窗口右移一格(left += 1, right += 1);
  2. 如果right等於len(nums), 結束;否則,判斷最右邊的新的數字是否大於cur_max:
    • 如果nums[right] >= cur_max,修改cur_max = nums[right]
    • 如果nums[right] < cur_max,判斷nums[left - 1]是否等於cur_max
      • 如果nums[left] - 1 == cur_max,說明之前的最大值失效了,需要在[left, right]中重新尋找cur_max
      • 如果nums[left - 1] != cur_max,說明之前的最大值仍然在[left, right]中,cur_max不變
  3. 將cur_max添加到ans中,並將窗口右移一格,回到第2步。

在這裏插入圖片描述
C++代碼:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if (nums.size() == 0 || k == 1) {
            return nums;
        }
        int cur_max = nums[0];
        for (int i = 1; i < k; i++) {
            if (nums[i] > cur_max) cur_max = nums[i];
        }
        vector<int> ans;
        ans.push_back(cur_max);
        int left = 1;
        int right = k;
        while (right < nums.size()) {
            if (nums[right] >= cur_max) {
                cur_max = nums[right];
            }
            else if (nums[left - 1] == cur_max) {
                cur_max = nums[left];
                for (int i = left; i <= right; i++) {
                    if (nums[i] > cur_max) cur_max = nums[i];
                }
            }
            ans.push_back(cur_max);
            left += 1;
            right += 1;
        }
        return ans;
    }
};

python代碼

class Solution(object):
    def maxSlidingWindow(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: List[int]
        """
        if len(nums) == 0 or k == 1:
            return nums
        cur_max = max(nums[0:k])
        ans = [cur_max]
        left, right = 1, k
        while right < len(nums):
            if nums[right] >= cur_max:
                cur_max = nums[right]
            elif nums[left - 1] == cur_max:
                cur_max = max(nums[left:right + 1])
            ans.append(cur_max)
            left += 1
            right += 1
        return ans

2.3 使用優先級隊列/堆

有沒有數據結構能夠一直返回最值呢?答案是堆。在最大堆中堆頂元素永遠是最大的元素,而且在大小爲k的堆中插入一個元素的時間複雜度爲O(logk)O(logk),算法整體的時間複雜度爲O(nlogk)O(nlogk)

不過這道題使用堆並不是一個明智的選擇,因爲需要頻繁的插入堆刪除堆,而且時間複雜度也並不低,因此不寫代碼實現這個笨方法了。在python中可以通過import heapq使用堆,python中heapq的部分函數使用方法:

import heapq
# 需要說明:heapq中只有小頂堆,沒有大頂堆,如果要使用大頂堆,可以取負數

# 創建heap方法1:使用heapq.heappush(heap_name, val)逐個添加
nums = [2, 3, 5, 1, 54, 23, 132]
heap = []
for num in nums:
    heapq.heappush(heap, num)
    
# 創建heap方法2:使用heapq.heapify(arr)直接添加整個數組,nums就會變爲一個堆
heapq.heapify(nums)

# 彈出最小值
heapq.heappop(heap)

# 打印最小值
heapq[0]

# 堆排序結果
print([heapq.heappop(heap) for _ in range(len(nums))])

# 刪除堆中最小元素並加入一個元素
heapq.heapreplace(nums, 23)

# 獲取堆中的k個最大值或最小值
heapq.nlargest(k, nums)
heapq.nsmallest(k, nums)

2.4 雙端隊列deque

方法2.2已經將暴力解法優化到了極限,然而在最差的情況下(整個數組是降序的)還是需要O(n·k)的時間複雜度。那麼有沒有更好的解法呢?在LeetCode題解上我看到了大佬們的解法-通過藉助雙端隊列deque可以在O(1)的時間複雜度內計算出每個窗口的最大值。

主要的思路是維護一個單調遞減的隊列,因此隊列最左邊的元素就是最大的。由於在窗口右移的過程中,需要不斷地把窗口最左邊的數字去除,隊列中存儲的是數字在nums數組中的索引。下圖中有顏色的方框表示在隊列中的數。

在這裏插入圖片描述

下面的代碼是使用數組模擬雙端隊列的簡化版本代碼:

python代碼

from collections import deque
class Solution(object):
    def maxSlidingWindow(self, nums, k):
        ans, window = [], []
        for i, num in enumerate(nums):
            if i >= k and window[0] <= i - k: window.pop(0)
            while window and nums[window[-1]] <= num:
                window.pop()
            window.append(i)
            if i >= k - 1:
                ans.append(nums[window[0]])
        return ans

另外,在python的collections模塊內置了deque,其次該模塊還有如下容器:

collections模塊實現了特定目標的容器,以提供Python標準內建容器 dict、list、set、tuple 的替代選擇。

  • Counter:字典的子類,提供了可哈希對象的計數功能
  • defaultdict:字典的子類,提供了一個工廠函數,爲字典查詢提供了默認值
  • OrderedDict:字典的子類,保留了他們被添加的順序
  • namedtuple:創建命名元組子類的工廠函數
  • deque:類似列表容器,實現了在兩端快速添加(append)和彈出(pop)
  • ChainMap:類似字典的容器類,將多個映射集合到一個視圖裏面

下面的代碼來自LeetCode官方題解, 使用deque實現:

python代碼

from collections import deque
class Solution(object):
    def maxSlidingWindow(self, nums, k):
        # base cases
        n = len(nums)
        if n * k == 0:
            return []
        if k == 1:
            return nums
        
        def clean_deque(i):
            # remove indexes of elements not from sliding window
            if deq and deq[0] == i - k:
                deq.popleft()
                
            # remove from deq indexes of all elements 
            # which are smaller than current element nums[i]
            while deq and nums[i] > nums[deq[-1]]:
                deq.pop()
        
        # init deque and output
        deq = deque()
        max_idx = 0
        for i in range(k):
            clean_deque(i)
            deq.append(i)
            # compute max in nums[:k]
            if nums[i] > nums[max_idx]:
                max_idx = i
        output = [nums[max_idx]]
        
        # build output
        for i in range(k, n):
            clean_deque(i)          
            deq.append(i)
            output.append(nums[deq[0]])
        return output

(完)

發佈了129 篇原創文章 · 獲贊 118 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章