0Pricing
Coding Interview Prep · Lezione

Schema dello stack monotono

Applichi lo stack monotono per risolvere daily-temperatures, largest-rectangle-in-histogram e next-greater-element in O(n)

Schema dello stack monotono è una lezione Coding Interview Prep gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Coding Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Coding Interview Prep include 4 lezioni in totale.

Che cos'è una pila monotona?

Una pila monotona è una pila che mantiene un invariante di ordinamento tra i propri elementi. Una pila monotona crescente contiene elementi crescenti dal fondo verso la cima; una pila monotona decrescente contiene elementi decrescenti dal fondo verso la cima. Quando un nuovo elemento viola l'invariante, gli elementi vengono estratti finché l'invariante non è ripristinato, quindi il nuovo elemento viene inserito.

Questo semplice meccanismo consente di rispondere in O(n) alle query sull'elemento maggiore più vicino e sull'elemento minore più vicino, che con un approccio ingenuo richiederebbero cicli annidati in O(n²).

# Build a monotonically increasing stack from [3,1,2,5,4]
nums  = [3, 1, 2, 5, 4]
stack = []
for n in nums:
    while stack and stack[-1] > n:
        stack.pop()   # remove elements that violate increasing order
    stack.append(n)
    print('stack:', stack)

Elemento maggiore successivo (LeetCode 496)

Per ogni elemento, trovi il primo elemento strettamente maggiore alla sua destra. Un approccio brute force in O(n²) esegue una scansione verso destra a partire da ogni posizione. L'approccio con pila monotona mantiene una pila decrescente di indici. Quando viene incontrato un elemento maggiore, rimuove tutti gli indici degli elementi più piccoli: l'elemento maggiore successivo per ciascuno di essi è l'elemento corrente. Gli indici rimanenti non hanno alcun elemento maggiore successivo (la risposta è -1).

def nextGreaterElement(nums):
    n      = len(nums)
    result = [-1] * n
    stack  = []   # indices, decreasing values
    for i, val in enumerate(nums):
        while stack and nums[stack[-1]] < val:
            j = stack.pop()
            result[j] = val
        stack.append(i)
    return result

print(nextGreaterElement([2, 1, 2, 4, 3]))   # [4, 2, 4, -1, -1]
print(nextGreaterElement([1, 3, 2, 4]))       # [3, 4, 4, -1]

Elemento maggiore successivo in un array circolare

LeetCode 503 «Next Greater Element II»: lo stesso problema, ma l'array viene considerato circolare. Dopo aver raggiunto la fine, si torna all'inizio e si continua la ricerca. Il trucco consiste nell'iterare due volte sull'array (dagli indici 0 a 2n-1) e usare i % n per accedere all'array originale. Inserisca nella pila solo gli indici nell'intervallo [0, n-1] per evitare di elaborare duplicati.

def nextGreaterElements(nums):
    n      = len(nums)
    result = [-1] * n
    stack  = []
    for i in range(2 * n):
        while stack and nums[stack[-1]] < nums[i % n]:
            j = stack.pop()
            result[j] = nums[i % n]
        if i < n:
            stack.append(i)
    return result

print(nextGreaterElements([1, 2, 1]))   # [2, -1, 2]
print(nextGreaterElements([5, 4, 3, 2, 1]))  # [-1, 5, 5, 5, 5]

Temperature giornaliere: soluzione completa

Riprendiamo LeetCode 739: per ogni giorno, quanti giorni devono trascorrere prima di trovare una temperatura più alta? La pila monotona contiene gli indici dei giorni con temperature in ordine decrescente. Quando viene trovato un giorno più caldo i, rimuova dalla pila tutti gli indici j dei giorni più freddi e registri result[j] = i - j. I giorni rimasti nella pila non hanno mai trovato un giorno più caldo, quindi il loro risultato resta 0.

def dailyTemperatures(temperatures):
    n      = len(temperatures)
    result = [0] * n
    stack  = []  # indices, decreasing temperatures
    for i, t in enumerate(temperatures):
        while stack and temperatures[stack[-1]] < t:
            j         = stack.pop()
            result[j] = i - j
        stack.append(i)
    return result

temps = [73, 74, 75, 71, 69, 72, 76, 73]
print(dailyTemperatures(temps))
# [1, 1, 4, 2, 1, 1, 0, 0]

Elemento minore precedente

La query sull'elemento minore precedente chiede: per ogni elemento, qual è il valore minore più vicino alla sua sinistra? Utilizzi una pila monotona crescente, elaborando gli elementi da sinistra a destra. Prima di inserire l'indice i, la cima della pila contiene l'elemento minore precedente, perché tutti gli elementi maggiori di nums[i] erano già stati rimossi durante gli inserimenti precedenti, quando elementi maggiori li avevano fatti rimuovere.

def previousSmallerElement(nums):
    n      = len(nums)
    result = [-1] * n
    stack  = []   # indices, increasing values
    for i, val in enumerate(nums):
        while stack and nums[stack[-1]] >= val:
            stack.pop()
        if stack:
            result[i] = nums[stack[-1]]
        stack.append(i)
    return result

print(previousSmallerElement([4, 5, 2, 10, 8]))  # [-1, 4, -1, 2, 2]
print(previousSmallerElement([3, 1, 2]))           # [-1, -1, 1]

Rettangolo più grande nell'istogramma

LeetCode 84 «Largest Rectangle in Histogram»: una pila monotona crescente di indici. Per ogni barra, rimuova tutte le barre più alte di quella corrente. Per ogni barra estratta h, il suo limite destro è l'indice corrente i e il suo limite sinistro è la nuova cima della pila + 1 (oppure 0 se la pila è vuota). Area = h × (right - left). Aggiunga una sentinella di altezza 0 per forzare l'estrazione di tutte le barre rimanenti alla fine.

def largestRectangleArea(heights):
    heights = heights + [0]  # sentinel
    stack   = []  # indices, increasing heights
    result  = 0
    for i, h in enumerate(heights):
        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            left   = stack[-1] + 1 if stack else 0
            width  = i - left
            result = max(result, height * width)
        stack.append(i)
    return result

print(largestRectangleArea([2, 1, 5, 6, 2, 3]))  # 10
print(largestRectangleArea([2, 4]))                # 4
print(largestRectangleArea([1]))                   # 1

Rettangolo massimo (LeetCode 85)

LeetCode 85 «Maximal Rectangle» estende il problema dell'istogramma a una matrice binaria 2D. Per ogni riga, calcoli le altezze accumulate delle barre: se matrix[row][col] == '1', l'altezza è il numero di 1 consecutivi sopra questa cella e includendo questa cella. Applichi quindi l'algoritmo del «rettangolo più grande nell'istogramma» all'array delle altezze di ogni riga. Complessità temporale: O(m × n) per una matrice m×n.

def maximalRectangle(matrix):
    if not matrix or not matrix[0]:
        return 0
    n       = len(matrix[0])
    heights = [0] * n
    result  = 0

    def largest_in_hist(h):
        h = h + [0]
        stack, best = [], 0
        for i, val in enumerate(h):
            while stack and h[stack[-1]] > val:
                height = h[stack.pop()]
                left   = stack[-1] + 1 if stack else 0
                best   = max(best, height * (i - left))
            stack.append(i)
        return best

    for row in matrix:
        for j, cell in enumerate(row):
            heights[j] = heights[j] + 1 if cell == '1' else 0
        result = max(result, largest_in_hist(heights[:]))
    return result

m = [['1','0','1','0','0'],['1','0','1','1','1'],
     ['1','1','1','1','1'],['1','0','0','1','0']]
print(maximalRectangle(m))  # 6

Trapping Rain Water: approccio con pila

LeetCode 42 «Trapping Rain Water» con una pila: mantenga una pila decrescente di indici. Quando viene incontrata una barra più alta, si forma una conca. Rimuova il fondo della conca; calcoli la larghezza dell'acqua come (current_index - stack_top - 1) e l'altezza come (min(current_bar, new_stack_top_bar) - valley_height). Sommi tutti i contributi. Complessità temporale: O(n), spazio: O(n).

def trap(height):
    stack  = []
    water  = 0
    for i, h in enumerate(height):
        while stack and height[stack[-1]] < h:
            bottom     = stack.pop()
            if not stack:
                break
            left       = stack[-1]
            width      = i - left - 1
            bounded_h  = min(h, height[left]) - height[bottom]
            water     += width * bounded_h
        stack.append(i)
    return water

print(trap([0,1,0,2,1,0,1,3,2,1,2,1]))  # 6
print(trap([4,2,0,3,2,5]))               # 9

Riconoscere i problemi adatti a una pila monotona

Segnali che indicano che una pila monotona è lo strumento giusto: il problema chiede l'elemento maggiore o minore successivo o precedente, la risposta per ogni elemento dipende dagli elementi in una direzione specifica oppure una soluzione ingenua in O(n²) richiede una scansione a sinistra o a destra per ogni elemento. La pila conserva i candidati che potrebbero essere le risposte per gli elementi futuri e li scarta non appena arriva un candidato migliore.

Decida sempre in anticipo: crescente (per l'elemento minore successivo o precedente) oppure decrescente (per l'elemento maggiore successivo o precedente), oltre alla direzione di elaborazione.

Analisi ammortizzata O(n)

Gli algoritmi con pila monotona possono sembrare inizialmente O(n log n) o O(n²) a causa del ciclo while annidato nel ciclo for. Tuttavia, ogni elemento viene inserito al massimo una volta ed estratto al massimo una volta. Il numero totale di operazioni di inserimento è n e anche il numero totale di operazioni di estrazione è al massimo n. Pertanto, considerando tutte le iterazioni, il lavoro totale è di 2n operazioni: O(n) ammortizzato, non O(n²).

# Count total pushes and pops for n=1000
n     = 1000
nums  = list(range(n, 0, -1))  # worst case for decreasing stack
stack = []
pushes = pops = 0
for val in nums:
    while stack and stack[-1] < val:
        stack.pop()
        pops += 1
    stack.append(val)
    pushes += 1

print(f'n={n}, pushes={pushes}, pops={pops}, total={pushes+pops}')
# Total <= 2*n

Riepilogo: scelta dell'invariante della pila monotona

Scelga la direzione della pila in base alla query. Per l'elemento maggiore successivo, utilizzi una pila decrescente: estragga gli elementi quando quello corrente è maggiore. Per l'elemento minore successivo, utilizzi una pila crescente: estragga gli elementi quando quello corrente è minore. Per il rettangolo più grande, utilizzi una pila crescente ed estragga gli elementi quando compare una barra più bassa. Per il massimo in una finestra scorrevole, utilizzi una deque decrescente e rimuova gli elementi da entrambe le estremità.

Scrivere l'invariante in un commento prima di iniziare a programmare chiarisce la logica e velocizza il debug.

Verifica rapida

Verifichi la sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.

Riepilogo della lezione

In questa lezione ha appreso che: una pila monotona mantiene un invariante ordinato rimuovendo gli elementi che lo violano prima di inserire il nuovo elemento, le pile decrescenti rispondono alle query sull'elemento maggiore successivo, mentre le pile crescenti rispondono alle query sull'elemento minore successivo e il tempo totale è O(n) ammortizzato perché ogni elemento viene inserito ed estratto al massimo una volta. Nella prossima lezione implementeremo code usando pile e pile usando code.

Domande Frequenti

La lezione «Schema dello stack monotono» è gratuita?

Sì — il testo completo di «Schema dello stack monotono» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Coding Interview Prep, passa a CoddyKit PRO. Il corso Coding Interview Prep include 4 lezioni in totale.

Cosa imparerò in «Schema dello stack monotono»?

Applichi lo stack monotono per risolvere daily-temperatures, largest-rectangle-in-histogram e next-greater-element in O(n) Eserciti Coding Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare Coding Interview Prep?

Non è richiesta alcuna esperienza precedente. Coding Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.

Quanto tempo richiede la lezione «Schema dello stack monotono»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione Coding Interview Prep?

Sì. Ogni lezione Coding Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Implementazione e applicazioni dello stack
  2. Implementazione della coda e deque
  3. Schema dello stack monotono
  4. Simulazione reciproca di stack e coda
← Torna a Coding Interview Prep