劍指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)
最容易想到的也肯定能想到的就是暴力法了,時間複雜度,寫法如下:
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
,在這時候加上一些判斷就能避免重新在窗口中遍歷一遍。其算法思路如下:
- 使用雙指針left, right確定窗口左右邊界首先得到最左邊的窗口,並求得該窗口中的最大值cur_max,將當前的cur_max加入到ans數組中,將窗口右移一格(left += 1, right += 1);
- 如果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不變
- 將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的堆中插入一個元素的時間複雜度爲,算法整體的時間複雜度爲。
不過這道題使用堆並不是一個明智的選擇,因爲需要頻繁的插入堆刪除堆,而且時間複雜度也並不低,因此不寫代碼實現這個笨方法了。在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
(完)