演算法面試(三) 優先佇列

天澄發表於2019-02-24

系列文章導圖:

演算法面試(三)  優先佇列

檢視所有系列文章: xiaozhuanlan.com/leetcode_tc

演算法面試(一) 連結串列 演算法面試(七) 廣度和深度優先演算法

1. 概念

優先佇列,對比佇列而已,顧名思義,就是正常入,按優先順序出。可以按小到大,也可以按大到小,或者自定義一個屬性,按屬性的特徵進行出佇列。

2. 實現機制

2.1 Heap 堆

Heap常見的有小頂堆和大頂堆。

演算法面試(三)  優先佇列
小頂堆圖上演示的是用二叉堆實現的,優先順序越小的越排在前面,父親節點的值比左孩子和右孩子都要小,有興趣的可以自己實現一個小頂堆。

演算法面試(三)  優先佇列
同理大頂堆類似,父親節點的值比左右孩子的值都要大。

堆的實現其實還有很多種,給大家分享一個連結,Google Heap就能查詢的到。 en.wikipedia.org/wiki/Heap_(…

這個內容裡面有一張圖給大家分享一下

演算法面試(三)  優先佇列
各種堆的實現方式和時間複雜度,有興趣的可以深入瞭解一下,剛才上面圖片演示的就是Binary實現方式。

2.2 Binary Search Tree二叉搜尋樹

二叉搜尋樹,也叫二叉查詢樹,後面章節會詳細介紹。簡單介紹一個特性:

  1. 若任意節點的左⼦樹不空,則左⼦樹上所有結點的值均⼩於它的 根結點的值;
  2. 若任意節點的右⼦樹不空,則右⼦樹上所有結點的值均⼤於它的 根結點的值;
  3. 任意節點的左、右⼦樹也分別為⼆叉搜尋樹。

演算法面試(三)  優先佇列
簡單的說二叉搜尋樹的值有序的,左孩子的值<父親節點<右孩子的值,按照樹的中序遍歷,則是一個按小到大的順序。

3. 面試題

3.1 703. Kth Largest Element in a Stream 返回資料流中K大的元素

leetcode 第703題

題目要求

  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 返回視窗的最大值

leetcode 第239題

題目要求 給定一個陣列 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)

演算法面試(三)  優先佇列

相關文章