算法通關(guān) - 優(yōu)先隊(duì)列

優(yōu)先隊(duì)列(PriorityQueue)

優(yōu)先隊(duì)列也是隊(duì)列的一種账嚎,它的特點(diǎn):

  • 不像隊(duì)列按照先進(jìn)先出來(lái)的。優(yōu)先隊(duì)列是正常進(jìn)入察皇,按照優(yōu)先級(jí)出(優(yōu)先級(jí)是優(yōu)先隊(duì)列本身你可以設(shè)置的一個(gè)屬性蕉堰,優(yōu)先級(jí)可以是最大值先出,或者是元素的隊(duì)列里出現(xiàn)的次數(shù)最多的先出)

優(yōu)先隊(duì)列的實(shí)現(xiàn)機(jī)制(了解即可搁骑,不需要自己去實(shí)現(xiàn))

  • 堆(heap)實(shí)現(xiàn)(可以有很多種堆斧吐,比如:二叉堆、多項(xiàng)式堆仲器、斐波那契堆)


    常見(jiàn)堆.png

小頂堆(二叉堆的一種煤率,特點(diǎn)是父節(jié)點(diǎn)比左右孩子節(jié)點(diǎn)都要小,最小的元素永遠(yuǎn)在堆頂)


小頂堆.png

大頂堆(二叉堆的一種乏冀,特點(diǎn)是父節(jié)點(diǎn)比左右孩子節(jié)點(diǎn)都要大蝶糯,最大的元素永遠(yuǎn)在堆頂)


大頂堆.png
  • 二叉搜索樹(shù)

push : 向棧中添加元素
peek : 返回棧頂元素
pop : 返回并刪除棧頂元素的操作

1.數(shù)據(jù)流中第K大元素(LeetCode - 703)

設(shè)計(jì)一個(gè)找到數(shù)據(jù)流中第K大元素的類(lèi)(class)。注意是排序后的第K大元素辆沦,不是第K個(gè)不同的元素昼捍。你的 KthLargest 類(lèi)需要一個(gè)同時(shí)接收整數(shù) k 和整數(shù)數(shù)組nums 的構(gòu)造器识虚,它包含數(shù)據(jù)流中的初始元素。每次調(diào)用 KthLargest.add妒茬,返回當(dāng)前數(shù)據(jù)流中第K大的元素担锤。你可以假設(shè) nums 的長(zhǎng)度≥ k-1k ≥ 1。

如下所示:
鏈接:https://leetcode-cn.com/problems/kth-largest-element-in-a-stream

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

方法一:第一次的時(shí)候從接受到的數(shù)組nums中取出前K大的數(shù)保存起來(lái)乍钻,以后每次add加入新的數(shù)據(jù)肛循,就將新數(shù)據(jù)和保存的前k大的數(shù)進(jìn)行排序丛塌,每次排序后都只保留K個(gè)數(shù)捏浊,將最小的淘汰掉菇存。用例子中K=3舉例就是第一次的時(shí)候保存前三大的數(shù)忆绰,也就是[4,5,8]与帆,添加進(jìn)來(lái)3之后將3和[4,5,8]排序疆瑰,3最小淘汰掉糕簿,保存的還是[4,5,8]屈扎,如果第二次進(jìn)來(lái)的10怀挠,將10和[4,5,8]排序析蝴,淘汰掉最小的4,保存下來(lái)的是[5,8,10]绿淋,然后返回保存下來(lái)的數(shù)據(jù)中的最小值就可以了闷畸。

這種方法的缺點(diǎn)是時(shí)間復(fù)雜度比較高:如果是N個(gè)數(shù)的話(huà),時(shí)間復(fù)雜度是N*K*logK ,每次排序采用快排吞滞,時(shí)間復(fù)雜度是K*logK 佑菩,加上數(shù)據(jù)量是k,所以總的復(fù)雜度就是N*K*logK

方法二:采用優(yōu)先隊(duì)列裁赠,維護(hù)一個(gè)小頂堆(Min Heap),因?yàn)槲覀円业贙大的元素殿漠,所以要保證小頂堆中元素個(gè)數(shù)始終是K個(gè)。在小頂堆中佩捞,堆頂元素是最小元素绞幌,那么每次有新元素進(jìn)來(lái)只需要和堆頂元素比較大小,如果新元素比堆頂元素小直接舍棄一忱,維持原來(lái)的堆不變莲蜘。新元素比堆頂元素大的話(huà),舍棄堆頂元素帘营,將新元素加入小頂堆中票渠,小頂堆會(huì)重新更新,維持堆頂元素最小的特點(diǎn)芬迄。我們要找第K大元素的話(huà)问顷,只要返回小頂堆的堆頂元素就可以了。

這種方法的時(shí)間復(fù)雜度是N*log?K

 class KthLargest {

    PriorityQueue<Integer> q;
    int k;
    public KthLargest(int k, int[] nums) {
        this.k = k;
        q = new PriorityQueue<>(k);
        for(int n : nums){
            add(n);
        }
    }
    
    public int add(int val) {
        if(q.size() < k){
            q.offer(val);
        }else if(q.peek() < val){
            q.poll();
            q.offer(val);
        }
        return q.peek();
    }
} 
滑動(dòng)窗口最大值(LeetCode - 239)

給定一個(gè)數(shù)組 nums,有一個(gè)大小為 k 的滑動(dòng)窗口從數(shù)組的最左側(cè)移動(dòng)到數(shù)組的最右側(cè)杜窄。你只可以看到在滑動(dòng)窗口內(nèi)的 k 個(gè)數(shù)字肠骆。滑動(dòng)窗口每次只向右移動(dòng)一位羞芍。返回滑動(dòng)窗口中的最大值哗戈。你可以假設(shè)k 總是有效的,在輸入數(shù)組不為空的情況下荷科,1 ≤ k ≤ 輸入數(shù)組的大小唯咬。

例如:

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

  滑動(dòng)窗口的位置                最大值
---------------               -----
[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

方法一:最簡(jiǎn)單直接的方法是遍歷每個(gè)滑動(dòng)窗口,找到每個(gè)窗口的最大值畏浆。一共有 N - k + 1 個(gè)滑動(dòng)窗口胆胰,每個(gè)有 k 個(gè)元素,于是算法的時(shí)間復(fù)雜度為O(N*k)刻获,性能較差蜀涨。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        if (n * k == 0) return new int[0];
        
        int [] output = new int[n - k + 1];
        for (int i = 0; i < n - k + 1; i++) {
            int max = Integer.MIN_VALUE;
            for(int j = i; j < i + k; j++) 
                max = Math.max(max, nums[j]);
            output[i] = max;
        }
        return output;
    }
}
  • 時(shí)間復(fù)雜度:O(N*k)。其中 N 為數(shù)組中元素個(gè)數(shù)蝎毡。
  • 空間復(fù)雜度:O(N?k+1)厚柳,用于輸出數(shù)組。

方法二:因?yàn)槊看味家页龃翱谥械淖畲笤劂灞晕覀兛梢允褂么箜敹眩∕ax Heap)的優(yōu)先隊(duì)列别垮,堆中元素個(gè)數(shù)是K,要找的最大元素就是堆頂元素扎谎。在這個(gè)大頂堆中碳想,我們還要記錄元素的索引,每次窗口移動(dòng)的操作就是將堆中前面的元素移出去毁靶,后面新的元素加入進(jìn)來(lái)胧奔,然后返回堆頂元素。這種方法的時(shí)間復(fù)雜度是N*logK

public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length==0) return new int[0];
        
        int[] res = new int[nums.length - k + 1];
        Deque<Integer> deque = new ArrayDeque<>();
        
        for(int i=0;i<nums.length;i++) {
            // 刪除隊(duì)列中小于窗口左邊下標(biāo)的元素
            if(i >= k && i - k + 1 > deque.peek()) deque.remove();
            
            // 從隊(duì)列右側(cè)開(kāi)始, 刪除小于nums[i] 的元素
            while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i])
                deque.removeLast();
            
            deque.add(i);
            
            // 隊(duì)列左側(cè)是最大值,加入結(jié)果
            if(i - k + 1 >= 0)
                res[i - k + 1] = nums[deque.peek()];
        }
        return res;
}

方法三:我們可以使用雙向隊(duì)列预吆,該數(shù)據(jù)結(jié)構(gòu)可以從兩端以常數(shù)時(shí)間壓入/彈出元素龙填。存儲(chǔ)雙向隊(duì)列的索引比存儲(chǔ)元素更方便,因?yàn)閮烧叨寄茉跀?shù)組解析中使用拐叉。步驟如下:

1.處理前 k 個(gè)元素觅够,初始化雙向隊(duì)列。

2.遍歷整個(gè)數(shù)組巷嚣。在每一步 ,清理雙向隊(duì)列 :

  • 只保留當(dāng)前滑動(dòng)窗口中有的元素的索引钳吟。
  • 移除比當(dāng)前元素小的所有元素廷粒,它們不可能是最大的。

3.將當(dāng)前元素添加到雙向隊(duì)列中。

4.將 deque[0] 添加到輸出中坝茎。

5.返回輸出數(shù)組

class Solution {
  ArrayDeque<Integer> deq = new ArrayDeque<Integer>();
  int [] nums;

  public void clean_deque(int i, int k) {
    
    if (!deq.isEmpty() && deq.getFirst() == i - k)
      deq.removeFirst();
      
    // 移除掉雙向隊(duì)列中比當(dāng)前元素小的所有元素
    while (!deq.isEmpty() && nums[i] > nums[deq.getLast()])                           deq.removeLast();
  }

  public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    if (n * k == 0) return new int[0];
    if (k == 1) return nums;

    //初始化雙向隊(duì)列和輸出數(shù)組
    this.nums = nums;
    int max_idx = 0;
    for (int i = 0; i < k; i++) {
      clean_deque(i, k);
      deq.addLast(i);
      //計(jì)算前K個(gè)數(shù)中最大元素的索引
      if (nums[i] > nums[max_idx]) max_idx = i;
    }
    int [] output = new int[n - k + 1];
    //得到第一次K個(gè)數(shù)中的最大值涤姊,保存在output[0]中
    output[0] = nums[max_idx];

    //遍歷數(shù)組中從k到n-1個(gè)元素,得到每次滑動(dòng)窗口的最大值
    for (int i  = k; i < n; i++) {
      clean_deque(i, k);
      deq.addLast(i);
      output[i - k + 1] = nums[deq.getFirst()];
    }
    return output;
  }
}
  • 時(shí)間復(fù)雜度:O(N)嗤放,每個(gè)元素被處理兩次 -- 分別是其索引被添加到雙向隊(duì)列中和被雙向隊(duì)列刪除思喊。
  • 空間復(fù)雜度:O(N),輸出數(shù)組使用了 O(N?k+1) 空間次酌,雙向隊(duì)列使用了O(k)恨课。

方法四: 動(dòng)態(tài)規(guī)劃,本算法的優(yōu)點(diǎn)是不需要使用 數(shù)組 / 列表 之外的任何數(shù)據(jù)結(jié)構(gòu)岳服。

class Solution {
  public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    if (n * k == 0) return new int[0];
    if (k == 1) return nums;

    int [] left = new int[n];
    left[0] = nums[0];
    int [] right = new int[n];
    right[n - 1] = nums[n - 1];
    for (int i = 1; i < n; i++) {
      // from left to right
      if (i % k == 0) left[i] = nums[i];  // block_start
      else left[i] = Math.max(left[i - 1], nums[i]);

      // from right to left
      int j = n - i - 1;
      if ((j + 1) % k == 0) right[j] = nums[j];  // block_end
      else right[j] = Math.max(right[j + 1], nums[j]);
    }

    int [] output = new int[n - k + 1];
    for (int i = 0; i < n - k + 1; i++)
      output[i] = Math.max(left[i + k - 1], right[i]);

    return output;
  }
}
  • 時(shí)間復(fù)雜度:O(N)剂公,我們對(duì)長(zhǎng)度為 N 的數(shù)組處理了 3次。
  • 空間復(fù)雜度:O(N)吊宋,用于存儲(chǔ)長(zhǎng)度為 N 的 left 和 right 數(shù)組纲辽,以及長(zhǎng)度為 N - k + 1的輸出數(shù)組。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末璃搜,一起剝皮案震驚了整個(gè)濱河市拖吼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌这吻,老刑警劉巖吊档,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異橘原,居然都是意外死亡籍铁,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)趾断,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)拒名,“玉大人,你說(shuō)我怎么就攤上這事芋酌≡鱿裕” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵脐帝,是天一觀的道長(zhǎng)同云。 經(jīng)常有香客問(wèn)我,道長(zhǎng)堵腹,這世上最難降的妖魔是什么炸站? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮疚顷,結(jié)果婚禮上旱易,老公的妹妹穿的比我還像新娘禁偎。我一直安慰自己,他們只是感情好阀坏,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布如暖。 她就那樣靜靜地躺著,像睡著了一般忌堂。 火紅的嫁衣襯著肌膚如雪盒至。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,328評(píng)論 1 310
  • 那天士修,我揣著相機(jī)與錄音枷遂,去河邊找鬼。 笑死李命,一個(gè)胖子當(dāng)著我的面吹牛登淘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播封字,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼黔州,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了阔籽?” 一聲冷哼從身側(cè)響起流妻,我...
    開(kāi)封第一講書(shū)人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎笆制,沒(méi)想到半個(gè)月后绅这,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡在辆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年证薇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匆篓。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡浑度,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鸦概,到底是詐尸還是另有隱情箩张,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布窗市,位于F島的核電站先慷,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏咨察。R本人自食惡果不足惜论熙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望摄狱。 院中可真熱鬧赴肚,春花似錦素跺、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)刊愚。三九已至踊跟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸥诽,已是汗流浹背商玫。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牡借,地道東北人拳昌。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像钠龙,于是被迫代替她去往敵國(guó)和親炬藤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容