系列文章導圖:
檢視所有系列文章: xiaozhuanlan.com/leetcode_tc
演算法面試(一) 連結串列 演算法面試(七) 廣度和深度優先演算法
1. 概念
優先佇列,對比佇列而已,顧名思義,就是正常入,按優先順序出。可以按小到大,也可以按大到小,或者自定義一個屬性,按屬性的特徵進行出佇列。
2. 實現機制
2.1 Heap 堆
Heap常見的有小頂堆和大頂堆。
小頂堆圖上演示的是用二叉堆實現的,優先順序越小的越排在前面,父親節點的值比左孩子和右孩子都要小,有興趣的可以自己實現一個小頂堆。 同理大頂堆類似,父親節點的值比左右孩子的值都要大。堆的實現其實還有很多種,給大家分享一個連結,Google Heap就能查詢的到。 en.wikipedia.org/wiki/Heap_(…
這個內容裡面有一張圖給大家分享一下
各種堆的實現方式和時間複雜度,有興趣的可以深入瞭解一下,剛才上面圖片演示的就是Binary實現方式。2.2 Binary Search Tree二叉搜尋樹
二叉搜尋樹,也叫二叉查詢樹,後面章節會詳細介紹。簡單介紹一個特性:
- 若任意節點的左⼦樹不空,則左⼦樹上所有結點的值均⼩於它的 根結點的值;
- 若任意節點的右⼦樹不空,則右⼦樹上所有結點的值均⼤於它的 根結點的值;
- 任意節點的左、右⼦樹也分別為⼆叉搜尋樹。
3. 面試題
3.1 703. Kth Largest Element in a Stream 返回資料流中K大的元素
題目要求
Design a class to find the kth largest element in a stream.
設計一個找到資料流中第K大元素的類(class)
複製程式碼
Example:
int k = 3;
int[] arr = [4,5,8,2];
KthLargest kthLargest = new KthLargest(3, arr);
kthLargest.add(3); // returns 4
kthLargest.add(5); // returns 5
kthLargest.add(10); // returns 5
kthLargest.add(9); // returns 8
kthLargest.add(4); // returns 8
複製程式碼
解題思路
假設K=1的話,其實就是從資料流中的最大值,問題就比較簡單了,即每次記錄最大值即可,比如例子中的[4,5,8,2],那隻要每次和記錄的Max比較即可。 同理,如果是求第K個最大的元素,有2種解法:
- 第一種: 用一個array保留前K個最大的元素,並將其排序。即每次來一個元素,則與這個array中最小的值比較,如果元素的值比最小的值大,則將array中最小的元素pop掉,將當前元素插入array,並再次排序。 時間複雜度是 N * klogk,klogk是排序的時間複雜度(算最快的快排)。
- 第二種: 第一種的話還是有點慢,第二種則採用今天的主題優先佇列來實現,每次維護一個小頂堆MinHeap即可。MinHeap的size是k,每次來元素與堆頂的元素比較,比堆頂元素小,則繼續往下走,比堆頂元素大,則剔除堆頂元素,將當前元素插入MinHeap,並重新調整堆的順序。時間複雜度,N * (1 or log2K),最差是每次都需要調整堆,調整堆的時間複雜度是Log2K。如果不需要調整,則是O(1)。
程式碼實現
程式碼實現以第二種方式。
import heapq
class KthLargest(object):
def __init__(self, k, nums):
"""
:type k: int
:type nums: List[int]
"""
self.k = k
self.min_heap = []
for i in nums:
self.add(i)
def add(self, val):
"""
:type val: int
:rtype: int
"""
if len(self.min_heap) < self.k:
heapq.heappush(self.min_heap, val)
else:
if val > self.min_heap[0]:
heapq.heappop(self.min_heap)
heapq.heappush(self.min_heap, val)
return self.min_heap[0]
複製程式碼
這裡用了python內建實現小頂堆的模組heapq,如果是別的語言,應該也有對應的內部實現堆的函式。heappush就是往堆裡面插入元素,函式會再次調整好順序,heappop就是講堆頂元素pop。程式碼實現思路與上面文字描述一致。
3.2 239. Sliding Window Maximum 返回視窗的最大值
題目要求 給定一個陣列 nums,有一個大小為 k 的滑動視窗從陣列的最左側移動到陣列的最右側。 返回滑動視窗最大值。
Example:
Input: nums = [1,3,-1,-3,5,3,6,7], and k = 3
Output: [3,3,5,5,6,7]
Explanation:
Window position Max
--------------- -----
[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
複製程式碼
解題思路 此題也有兩種解法。
- 第一種解法: 採用大頂端實現,維護一個size=k的MaxHeap,同時再維護一個Count的Map,記錄每一個值得位置。比如開始[1,3,-1],則堆頂是3,返回最大值是3,如果再來一個-3,比3小,堆頂依然是3,但是最初的1需要從堆中去掉,將-3加入進來。所以需要維護這個堆,刪除舊元素,加入新元素,同時結果就是堆頂。程式碼會待會給大家演示。時間複雜度是 NlogK 。
- 第二種: 第一種還是比較複雜的,還可以簡化,題目的特點是每次需要維護的視窗是一定的,即size=k,那麼可以使用上一篇講的Queue實現,但是有點區別是這裡需要使用雙端佇列deque,即兩邊都可以進和出。怎麼實現了? 將k個元素依次加入到deque裡,同時每次一個新元素進來的話進行佇列的維護。 重點就在維護的邏輯,首先保證deque的長度不能大於k, 同時最左邊的元素永遠是最大的元素。 比如[1,3,-1],那麼deque維護之後就變成[3, -1],因為1比3小,還比3老,那肯定不會是我們要找的元素,結果就是3。再加一個-3,deque就變成[3,-1,-3],因為-3比3小,所以加到最新的位置即可,結果還是3。再來一個5,首先將舊元素-3剔除,變成[-1,-3,5],如果比較這三個元素,發現-1,-3都比5小,則全部再次剔除變成[5],所以結果是5。 文字描述舉例輸出結果就是[3,3,5]
程式碼實現
- 第一種解法: MaxHeap實現
import collections
import heapq
class Solution(object):
def maxSlidingWindow(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
count_map, max_heap, result = collections.Counter(), [], []
for i, num in enumerate(nums):
heapq.heappush(max_heap, -num)
count_map[num] += 1 # 記錄每個元素的count
while not count_map[-max_heap[0]]: # 清除堆頂的舊元素
heapq.heappop(max_heap)
if i >= k - 1:
result.append(-max_heap[0])
count_map[nums[i - k + 1]] -= 1 # 將位置在k之前的元素count變為0
return result
複製程式碼
大頂端還是使用heapq實現,CountMap使用python內部實現的Counter(),裡面就是一個Map,key是num,value是count。需要說明的是,heapq預設是小頂堆,如果需要實現大頂端,需要小技巧,就是將num變負數,比如[1,2,3],預設情況下是堆頂是1,但是變負數-num,[-1,-2,-3],則就變成堆頂是-3,從而實現了MaxHeap。 還有一個注意的是,這裡沒有每次去不是堆頂的且count=0的舊元素,而是等它變成堆頂的情況,如果發現是少於size=k之前的元素,則清除。這樣比較好實現一點,可能理解上稍微變難了點。
- 第二種解法: deque雙端佇列 (推薦解法)
class Solution(object):
"""
deque實現
"""
def maxSlidingWindow(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
if not nums:
return []
result, 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:
result.append(nums[window[0]])
return result
複製程式碼
第二種解法採用雙端佇列,迴圈列舉nums,window是記錄nums元素的下標,而不是值,如果發現window最左邊的元素下標超過i-k,則元素過時,需要從佇列的最左邊將元素剔除。如果發現window最右邊的元素比當前元素小,則也全部剔除,當前元素肯定比這些元素小的要更新,將當前元素插入window即可,同時取window最左邊的值為結果,因為最左邊的值永遠最大。
完整程式碼已上傳github github.com/CrystalSkyZ…
更多精彩文章請關注公眾號 天澄的技術筆記 (ChengTian_Tech)