騰訊面試題-求滑動視窗的最大值

演算法推薦管發表於2021-11-10

大家好,我是程式設計師學長~

今天給大家分享一道騰訊面試真題,如果喜歡,記得點個關注喲~

問題描述

給你一個整數陣列 nums,有一個大小為 k 的滑動視窗從陣列的最左側移動到陣列的最右側。你只可以看到在滑動視窗內的 k 個數字。滑動視窗每次只向右移動一位。返回滑動視窗中的最大值。

示例:

輸入:[2,3,4,2,6,2,5,1],3

輸出:[4,4,6,6,6,5]

分析問題

這道題的關鍵點在於求滑動視窗中的最大值。大小為k的滑動視窗,我們可以通過遍歷的方式來求出其中的最大值,需要O(k)的時間複雜度。對於大小為n的陣列nums,一共有n-k+1個視窗,因此該演算法的時間複雜度是O(nk)。

通過觀察,我們可以知道,對於兩個相鄰的滑動視窗,有k-1個元素是共用的,只有一個元素是變化的,因此我們可以利用此性質來優化我們的演算法。

對於求最大值問題,我們可以使用優先順序佇列(大頂推)來求解。首先,我們將陣列的前k個元素放入優先順序佇列中。每當我們向右移動視窗時,我們就可以把一個新的元素放入佇列中,此時堆頂元素就是堆中所有元素的最大值,然而這個最大值有可能不屬於當前的滑動視窗中,我們需要將該元素進行移除處理(如果最大值不在當前滑動視窗中,它只能在滑動視窗的左邊界的左側,所以滑動視窗向右移動的過程中,該元素再也不會出現在滑動視窗中了,所以我們可以對其進行移除處理)。我們不斷地移除堆頂的元素,直到其確實出現在滑動視窗中。此時,堆頂元素就是滑動視窗中的最大值。

為了方便判斷堆頂元素與滑動視窗的位置關係,我們可以在優先佇列中儲存二元組 (num,index),表示元素num在陣列中的下標為index。

小trick:因為python中只提供了小頂堆,所以我們需要對元素進行取反處理,例如對於列表[1, -3],我們對元素進行取反,然後插入小頂堆中,此時堆中是這樣的[-1,3],我們取出堆頂元素-1,然後取反為1,正好可以得到列表中的最大值1。

我們nums=[2,3,4,2,6,2,5,1],k=3為例,來看一下具體的過程。

  1. 首先,我們將nums的前3個元素放入優先順序佇列中,隊首元素下標值index=2>0,在視窗中,所以加入結果中,此時res=[4]。

  2. 下一個元素2入隊,此時隊首元素下標index=2>1,在視窗中,所以加入結果中,此時res=[4,4]。

  3. 下一個元素6入隊,此時隊首元素下標index=4>2,在視窗中,所以加入結果中,此時res=[4,4,6]。

  4. 下一個元素2入隊,此時隊首元素下標index=4>3,在視窗中,所以加入結果中,此時res=[4,4,6,6]。

  5. 下一個元素5入隊,此時隊首元素下標index=4=4,在視窗中,所以加入結果中,此時res=[4,4,6,6,6]。

  6. 下一個元素1佇列,此時隊首元素下標index=4<5,不在視窗中,所以我們將其彈出,此時隊首元素的下標變為6,在視窗中,所以加入結果中,此時res=[4,4,6,6,6,5]。

進階

這道題我們也可以使用雙端佇列來求解。我們在遍歷陣列的過程中,不斷的對元素對應的下標進行出隊入隊操作,在出入隊的過程中,我們需要保證佇列中儲存的下標對應的元素是從大到小排序的。具體來說,當有一個新的元素對應的下標需要入隊時,如果該元素比隊尾對應的元素的值大,我們需要彈出隊尾,然後迴圈往復,直到佇列為空或者新的元素小於隊尾對應的元素。

由於佇列中下標對應的元素是嚴格單調遞減的,因此隊首下標對應的元素就是滑動視窗中的最大值。但是此時的最大值可能在滑動視窗左邊界的左側,並且隨著視窗向右移動,它永遠不可能出現在滑動視窗中了。因此我們還需要不斷從隊首彈出元素,直到隊首元素在視窗中為止。

我們還是以nums=[2,3,4,2,6,2,5,1],k=3為例,來看一下具體的過程。我們首先初始化一個空佇列que。

  1. 此時佇列為que空,元素2對應的下標0入隊。並且此時未形成視窗,不取值。

  2. 此時佇列que=[0],隊尾元素為0,它對應陣列中的元素是nums[0] < nums[1]的,所以我們把隊尾0彈出,此時佇列為空,我們將1入隊。並且此時未形成視窗,不取值。

  3. 此時佇列que=[1],隊尾元素為1,它對應的陣列中的元素是nums[1] < nums[2]的,所以我們把隊尾1彈出,此時佇列為空,我們將2入隊。並且此時隊首元素1在視窗[0,2]中,所以取出隊首元素。

  4. 此時佇列que=[2],隊尾元素為2,它對應的陣列中的元素是nums[2] > nums[3]的,所以我們將3入隊。並且此時隊首元素1在視窗[1,3]中,所以取出隊首元素。

  5. 此時佇列que=[2,3],隊尾元素為3,它對應的陣列中的元素是nums[3] < nums[4]的,所以我們把隊尾3彈出,並且此時隊尾元素對應的陣列中的元素是nums[2] < nums[4],所以我們把隊尾2彈出,此時佇列為空,我們將4入隊。並且此時隊首元素4在視窗[2,4]中,所以取出隊首元素。

  6. 此時佇列que=[4],隊尾元素為4,它對應的陣列中的元素是nums[4] > nums[5]的,所以我們將5入隊。並且此時隊首元素4在視窗[3,5]中,所以我們取出隊首元素。

  7. 此時佇列que=[4,5],隊尾元素為5,它對應的陣列中的元素是nums[5] < nums[6]的,所以我們把隊尾5彈出,此時隊尾元素對應的陣列中的元素時nums[4] > nums[6] ,所以我們將6入隊。並且此時隊首元素4在視窗[4,6]中,所以我們取出隊首元素。

  8. 此時佇列que=[4,6],隊尾元素為6,它對應的陣列中的元素是nums[6] > nums[7]的,所以我們將7入隊。而此時隊首元素4不在視窗[5,7]中,所以我們將其移除佇列,此時隊首元素6在視窗[5,7]中,所以我們將其取出。

下面我們來看一下程式碼實現。

import collections
class Solution:
    def maxSlidingWindow(self, nums, k):
        n = len(nums)
        #申請一個雙端佇列
        q = collections.deque()

        #初始化第一個視窗
        for i in range(k):
            #如果佇列不為空且比隊尾元素大,將隊尾出隊
            while q and nums[i] >= nums[q[-1]]:
                q.pop()
            #直到佇列為空,或者比隊尾元素小,入隊
            q.append(i)

        #將隊首元素加入結果中
        ans = [nums[q[0]]]

        #視窗逐步向右移動
        for i in range(k, n):
            #如果佇列不為空且比隊尾元素大,將隊尾出隊
            while q and nums[i] >= nums[q[-1]]:
                q.pop()
            #直到佇列為空,或者比隊尾元素小,入隊
            q.append(i)
            #如果隊首元素不在該視窗內,出隊操作
            while q[0] <= i - k:
                q.popleft()
            #將隊首元素加入結果中
            ans.append(nums[q[0]])

        return ans


s=Solution()
print(s.maxSlidingWindow([2,3,4,2,6,2,5,1],3))

最後

原創不易!各位小夥伴覺得文章不錯的話,不妨點贊(在看)、留言、轉發三連走起!

你知道的越多,你的思維越開闊。我們下期再見。

相關文章