文章目录
239. 滑动窗口最大值
暴力的解法自然是取每个区间,分别求最大值,然后得到结果,复杂度为 O ( n k ) O(nk) O(nk),但会超时。
尝试用队列来解决问题,会发现无法获得足够的单调性。普通的队列可以保持 index 的单调,便于窗口的滑动,但无法便捷地获取最大值;有序队列,例如 priority queue (heap)可以保持值的单调,但是无法便捷地删除要 pop 的元素(不知道对应的 index)。本题中似乎同时需要 index 的单调性以及值的单调性,但目前没有现成的结构能有效地同时实现这两大单调性,从概念上来说两种单调性就是冲突的。
实际上,我们并不需要完全维持窗口中的值单调性,而只需要维持最大值,或者说有可能成为最大值的元素,并保持队列中的元素是从大到小的,我们称之为单调队列(需要自己实现)。
单调队列最重要的思想即是维持有可能成为(窗口内)最大值的元素的有序性,而不是整个窗口的有序性。举例来说,对于窗口[2, 3, 5, 1, 4]
,只有[5, 4]
具有被维护的价值,因为当遇到5的时候,即知道前面的2、3都不会成为当前以及之后、包括它们的窗口中的最大值。同理,1也不会被维持。然而,4却会被维持,因为假如4的后面全部是小于4的元素,则当窗口继续滑动,可能会出现4成为某一窗口最大值的情况。
同时,这也使得窗口内是单调递减的。
维护有可能成为(窗口内)最大值的元素,也是对于窗口的一种剪枝。
完整的演算过程如下
![](https://img-blog.csdnimg.cn/e04ae411e03c4a88a1aa7f59a9eb0d85.gif#pic_center)
- push:当添加一个新元素时,与队列入口(back)的元素比较,若新元素更大,则 pop 入口元素,直到新的元素小于入口元素。此时添加新元素。
- pop:如果需要 pop 的元素并非当前队列的最大元素(即出口元素,因为队列从大到小排列),则该元素已经在之前某一次 push 中被 pop 了。仅当需要 pop 的元素就是当前队列中的最大元素时,才需要 pop 该元素。
- getMax:由于维护的队列从大到小,队列的出口元素即为最大的元素。
定义并维持了一个新的队列,同时每个元素至多被 push 和 pop 各一次。
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( k ) O(k) O(k)
因为需要在队列的两端进行操作,pop 开头的元素的时候 list 的复杂度太高了,所以使用 deque 操作。
from collections import deque
class MyMaxQueue:
def __init__(self):
self.data = deque() # low complexity using deque, two-sided list
def pop(self, item: int) -> None:
"""
Compare the item to pop and the front (max) item in the list. Only pop when they are the same.
This means that we will pop the max item in the deque. Otherwise, the item to pop should have already been removed from the deque previously.
"""
if len(self.data) > 0 and item == self.data[0]: # always check whether empty first
self.data.popleft() # O(1)
def push(self, item: int) -> None:
"""
Keep the deque containing elements in a non-decreasing order.
If the pushed item is greater than the back of deque, pop the back item until the pushed item is smaller than or equal to the back.
"""
while (len(self.data) > 0 and item > self.data[-1]):
self.data.pop()
self.data.append(item)
def getMax(self) -> int:
return self.data[0] # non-decreasing order
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
queue = MyMaxQueue()
for i in range(k):
queue.push(nums[i])
results = [queue.getMax()]
for i in range(len(nums) - k):
queue.pop(nums[i])
queue.push(nums[i + k])
results.append(queue.getMax())
return results
347. 前 K 个高频元素
最朴实暴力的思想就是将数组储存成 mapping(key 是数值,value 是出现次数),然后根据出现次数排序,选取前 k 项的 key。非常直接的想法,时间复杂度为
O
(
n
log
n
)
O(n\log{n})
O(nlogn),主要取决于排序。
但题目要求复杂度优于
O
(
n
log
n
)
O(n\log{n})
O(nlogn),相当于直接否定了需要整体排序的解法。注意到,对于题目要求中的“前 k 个”元素,实际上并不需要对于整个 mapping 进行排序,而是仅需维持前 k 个的排序即可。
于是我们想到了 heap 的应用。
在本题中,是要维持前 k 个高频元素,似乎要记录的是最大的一些值,自然会想到 max heap。但是限定 heap size 之后,决定是否将新的元素加入 max heap 需要将该元素与 max heap 的最小值比较,彻底违反了 max heap 的初衷。所以,min heap 才是此题的最优解,因为我们添加进 heap 的基准是与 heap 中最小的值进行比较。
具体实现可以采用 priority queue。
Heap
Heap 是一个数组形式的 complete binary tree。以 max heap 为例,其中每一个 parent node 的值都大于等于其 child nodes 的值。显然,heap 的内部是有序的,并且以 binary tree 的形式来维持这个排序。
优先级队列 priority queue
优先级队列一般是由 heap 来实现的,其中每个元素都有自己相对应的 priority,在 python 中,priority 越小代表优先级越高(越早被取出)。优先级队列维护 priority 从小到大的排序。
优先级队列的应用之一是任务处理,保证优先处理高优先级的任务。
from queue import PriorityQueue
queue = PriorityQueue()
q.put((2, 'g'))
q.put((3, 'e'))
q.get() # 'g', remove and return the item with lowest priority
q.qsize() # 1
暴力解法
对整个 mapping 根据出现次数排序,取前 k 个。
from collections import Counter
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
records = Counter(nums)
records_lst = []
for key in records:
records_lst.append([key, records[key]])
records_lst.sort(key=lambda x:x[1], reverse=True)
results = []
for item in records_lst[:k]:
results.append(item[0])
return results
Min Heap - queue.PriorityQueue
时间复杂度:
O
(
n
log
k
)
O(n\log{k})
O(nlogk)
对于每个(key,value)的插入,维护 heap 的复杂度为
O
(
log
k
)
O(\log{k})
O(logk)。
from collections import Counter
from queue import PriorityQueue
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# turn nums into a mapping: (num, count)
records = Counter(nums)
# set up a priority queue
queue = PriorityQueue()
for key in records:
queue.put((records[key], key))
if queue.qsize() > k:
queue.get()
# load the priority queue's elements
results = []
while (not queue.empty()):
results.append(queue.get()[1])
return results
Min Heap - heapq
heapq
是 python 中实现 heap 的一个辅助库。不像 queue.PriorityQueue
是一个独立的子类,heapq
中的函数可以直接作用于 list,只是将 list 维持在 min heap 的规则下。
常用的 API:
heapq.heappush(heap, item)
: Push the value item onto the heap, maintaining the heap invariant.
heapq.heappop(heap)
: Pop and return the smallest item from the heap, maintaining the heap invariant. If the heap is empty, IndexError is raised. To access the smallest item without popping it, use heap[0]
.
heapq.heappushpop(heap, item)
: Push item on the heap, then pop and return the smallest item from the heap. The combined action runs more efficiently than heappush()
followed by a separate call to heappop()
.
heapq.heapify(x)
: Transform list x into a heap, in-place, in linear time.
值得注意的是,heapq 在添加元素时也要遵循 (priority, value)
的格式。
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# another way to implement Counter
records = {}
for i in nums:
records[i] = records.get(i, 0) + 1
# initialize a list - min_heap
min_heap = []
for key in records:
heapq.heappush(min_heap, (records[key], key))
if len(min_heap) > k:
heapq.heappop(min_heap)
results = []
for item in min_heap:
results.append(item[1])
return results