Förberedelse inför kodningsintervjuer · Lektion

Trapping Rain Water: stack och två pekare

Lös trapping-rain-water med både metoden med monoton stack, som beräknar horisontella lager, och metoden med två pekare, som beräknar vertikala kolumner.

Lektion 4 av 413 steg

Trapping Rain Water: stack och två pekare är en gratis lektion i Förberedelse inför kodningsintervjuer på CoddyKit. Detta är lektion 4 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Förberedelse inför kodningsintervjuer, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Förberedelse inför kodningsintervjuer innehåller totalt 4 lektioner.

Problem: Fånga regnvatten

Fånga regnvatten (LeetCode 42) är ett av de mest ikoniska intervjuproblemen. Givet n icke-negativa heltal som representerar en höjdkarta där varje stapel har bredden 1 ska du beräkna hur mycket vatten som kan samlas mellan staplarna efter att det har regnat. Vatten samlas i varje sänka mellan högre staplar på båda sidor.

För varje position i är vattennivån min(max_left[i], max_right[i]) - height[i]. Om detta är negativt samlas inget vatten (stapeln är högre än minst en av gränserna). Det finns tre angreppssätt: förberäknade arrayer O(n)/O(n), två pekare O(n)/O(1) och monoton stack O(n)/O(n).

height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
# Water trapped at each position:
# pos 2: min(1,3)-0=1
# pos 4: min(2,3)-1=1
# pos 5: min(2,3)-0=2
# pos 6: min(2,3)-1=1
# pos 9: min(3,2)-1=1
# Total = 6
print('height:', height)
print('Expected trapped water: 6')

# Visualise
max_h = max(height)
for row in range(max_h, 0, -1):
    line = ''
    for h in height:
        line += '#' if h >= row else ' '
    print(line)

Angreppssätt 1: Förberäknade maximumarrayer

Den raka lösningen med tidskomplexiteten O(n) och utrymmeskomplexiteten O(n) förberäknar två arrayer: max_left[i] = den största höjden från index 0 till i och max_right[i] = den största höjden från index i till n-1. Vattenmängden vid position i är max(0, min(max_left[i], max_right[i]) - height[i]).

Att bygga max_left kräver en enda genomgång från vänster till höger; max_right kräver en genomgång från höger till vänster. En sista genomgång summerar vattnet. Detta angreppssätt är rent och enkelt att förklara, men använder O(n) extra utrymme.

def trap_prefix(height):
    n = len(height)
    if n < 3:
        return 0

    max_left = [0] * n
    max_right = [0] * n

    max_left[0] = height[0]
    for i in range(1, n):
        max_left[i] = max(max_left[i-1], height[i])

    max_right[-1] = height[-1]
    for i in range(n-2, -1, -1):
        max_right[i] = max(max_right[i+1], height[i])

    water = 0
    for i in range(n):
        water += max(0, min(max_left[i], max_right[i]) - height[i])
    return water

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

Angreppssätt 2: Två pekare (O(1) utrymme)

Tvåpekaralgoritmen uppnår tidskomplexiteten O(n) och utrymmeskomplexiteten O(1). Använd vänster- och högerpekare som börjar i varsin ände. Upprätthåll max_left och max_right som löpande maximumvärden från respektive sida.

Bearbeta i varje steg den sida som har det mindre löpande maximumvärdet — eftersom den sidan är den begränsande faktorn. Om max_left < max_right är vattnet vid vänsterpekaren max_left - height[left] (den högra sidan är tillräckligt hög). Flytta vänsterpekaren inåt. Bearbeta annars högerpekaren på motsvarande sätt. Inga förberäknade arrayer behövs.

def trap_two_pointer(height):
    left, right = 0, len(height) - 1
    max_left = max_right = 0
    water = 0

    while left < right:
        if height[left] < height[right]:
            if height[left] >= max_left:
                max_left = height[left]    # new max on the left
            else:
                water += max_left - height[left]  # trapped by max_left
            left += 1
        else:
            if height[right] >= max_right:
                max_right = height[right]
            else:
                water += max_right - height[right]
            right -= 1
    return water

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

Varför två pekare fungerar: invarianten

Den viktiga insikten är följande: när vi bearbetar vänsterpekaren eftersom height[left] < height[right] vet vi att max_right >= height[right] > height[left]. Därför är den effektiva vattenbarriären till höger minst height[right], som redan är större än max_left. Alltså är min(max_left, effective_max_right) = max_left, och vattenformeln förenklas till max_left - height[left].

Vi behöver inte känna till det exakta max_right — det räcker att veta att det är minst height[right] > height[left] för att kunna använda max_left som vattennivå. Detta är den eleganta invariant som gör O(1) utrymme möjligt.

# Trace two-pointer on [4, 2, 0, 3, 2, 5]
height = [4, 2, 0, 3, 2, 5]
left, right = 0, len(height) - 1
max_l = max_r = water = 0
print('height:', height)
print(f'{'Step':5} {'L':3} {'R':3} {'maxL':5} {'maxR':5} {'water':6} {'total':6}')
step = 0
while left < right:
    side = 'L' if height[left] < height[right] else 'R'
    if side == 'L':
        if height[left] >= max_l: max_l = height[left]
        else:
            w = max_l - height[left]; water += w
        left += 1
    else:
        if height[right] >= max_r: max_r = height[right]
        else:
            w = max_r - height[right]; water += w
        right -= 1
    step += 1
    print(f'{step:5} {left:3} {right:3} {max_l:5} {max_r:5} {water:6}')
print('Total trapped:', water)

Metod 3: Monoton stack (horisontella lager)

Metoden med en monoton stack beräknar vatten i horisontella lager mellan intilliggande staplar. Håll en monotont avtagande stack med index. När stapel i är högre än stackens topp j uppstår en sänka: botten är height[j], den vänstra väggen är height[stack[-1]] efter att j tagits bort och den högra väggen är height[i]. Vattnet fyller sänkan upp till min(left_wall, right_wall) - floor, med bredden i - stack[-1] - 1.

Varje ”sänka” beräknas när en högre stapel påträffas. Då bearbetas vattnet i avgränsade rektangulära segment, vilket är användbart när du även behöver hålla reda på vilka staplar som bidrar till vattennivån.

def trap_stack(height):
    stack = []   # monotonic decreasing indices
    water = 0

    for i in range(len(height)):
        while stack and height[stack[-1]] < height[i]:
            bottom_idx = stack.pop()        # the floor of the valley
            if not stack:
                break                       # no left wall, no water
            left_idx = stack[-1]
            floor = height[bottom_idx]
            water_height = min(height[left_idx], height[i]) - floor
            width = i - left_idx - 1
            water += water_height * width
        stack.append(i)
    return water

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

Följ den monotona stacken

Låt oss följa [0,1,0,2,1,0,1,3,...] med stackmetoden. När vi når stapel 3 (h=2) vid i=3 är stackens topp i=2 (h=0), så den tas bort. Den vänstra väggen är i=1 (h=1), och den högra väggen är h=2. Vattenhöjd = min(1,2)-0=1, bredd=3-1-1=1, area=1. Fortsätt: stackens topp i=1 (h=1) är inte mindre än 2, så vi stoppar. Lägg 3 på stacken.

Stackmetoden är mer komplex att implementera än tvåpekarmetoden, men visar vilka specifika staplar som bildar varje vattencell. Denna insikt är användbar i följdfrågor om att återskapa vattnets utbredning eller räkna olika sänkor.

def trap_stack_trace(height):
    stack = []
    water = 0
    for i in range(len(height)):
        print(f'i={i} h={height[i]}: stack={[height[s] for s in stack]}')
        while stack and height[stack[-1]] < height[i]:
            bot = stack.pop()
            if not stack:
                print(f'  Pop {height[bot]}: no left wall, skip')
                break
            left = stack[-1]
            h = min(height[left], height[i]) - height[bot]
            w = i - left - 1
            water += h * w
            print(f'  Pop {height[bot]}: floor={height[bot]}, left_wall={height[left]}, right_wall={height[i]}, h={h}, w={w}, +{h*w}')
        stack.append(i)
    return water

result = trap_stack_trace([0,1,0,2,1,0,1,3,2,1,2,1])
print('Total:', result)

Jämförelse av alla tre metoder

Sammanfattning av de tre metoderna för att samla regnvatten:

  • Prefixarrayer: O(n) tid, O(n) minnesutrymme. Enklast att förstå och verifiera. Bäst i intervjuer där tydlighet värderas högre än minneseffektivitet.
  • Två pekare: O(n) tid, O(1) minnesutrymme. Optimalt vad gäller både tid och minnesutrymme. Bäst för följdfrågor i stil med ”kan du använda O(1) minnesutrymme?”
  • Monoton stack: O(n) tid, O(n) minnesutrymme. Bearbetar vatten i horisontella lager. Bäst när du behöver veta vilka staplar som bidrar eller när problemet ingår som delproblem i en större stackbaserad algoritm.
height = [0,1,0,2,1,0,1,3,2,1,2,1]

# All three methods — verify they agree
def trap_prefix(h):
    n = len(h)
    ml = [0]*n; mr = [0]*n; ml[0]=h[0]; mr[-1]=h[-1]
    for i in range(1,n): ml[i]=max(ml[i-1],h[i])
    for i in range(n-2,-1,-1): mr[i]=max(mr[i+1],h[i])
    return sum(max(0,min(ml[i],mr[i])-h[i]) for i in range(n))

def trap_two_ptr(h):
    l,r,ml,mr,w = 0,len(h)-1,0,0,0
    while l<r:
        if h[l]<h[r]:
            ml=max(ml,h[l]); w+=ml-h[l]; l+=1
        else:
            mr=max(mr,h[r]); w+=mr-h[r]; r-=1
    return w

def trap_stk(h):
    stk,w = [],[]
    for i in range(len(h)):
        while stk and h[stk[-1]]<h[i]:
            b=stk.pop()
            if not stk: break
            w.append(max(0,min(h[stk[-1]],h[i])-h[b])*(i-stk[-1]-1))
        stk.append(i)
    return sum(w)

for h in [height, [4,2,0,3,2,5], [3,0,3], [1,0,1]]:
    p=trap_prefix(h); t=trap_two_ptr(h); s=trap_stk(h)
    print(f'{h}: prefix={p}, two-ptr={t}, stack={s}, match={p==t==s}')

Behållaren med mest vatten

Container With Most Water (LeetCode 11) förväxlas ofta med problemet att samla regnvatten. Här väljer du exakt två staplar, och vattnet begränsas enbart av dessa två staplar (interna staplar spelar ingen roll). Maximera arean min(height[l], height[r]) × (r - l).

Två pekare löser detta girigt: börja i båda ändarna (maximal bredd). Flytta den kortare pekaren inåt — att flytta den högre kan bara minska arean. Detta tar O(n) tid och O(1) minnesutrymme och är enklare än tvåpekarlösningen för att samla regnvatten, eftersom inget löpande maxvärde behövs.

def max_water_container(height):
    left, right = 0, len(height) - 1
    max_area = 0

    while left < right:
        area = min(height[left], height[right]) * (right - left)
        max_area = max(max_area, area)
        # Move the shorter bar: moving taller bar can only reduce min
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1
    return max_area

print(max_water_container([1,8,6,2,5,4,8,3,7]))  # 49: bars 8 and 7
print(max_water_container([1,1]))                  # 1
print(max_water_container([4,3,2,1,4]))            # 16

# Key difference from trapping rain water:
# Container: choose 2 bars, water fills freely between them (no internal barriers)
# Trapping:  water fills ALL valleys in the full elevation map

Avancerat: Trapping Rain Water II (3D)

Trapping Rain Water II (LeetCode 407) utvidgar problemet till en 2D-höjdmatris. Vatten kan rinna i alla fyra riktningar och måste rinna ut över kanten. Lösningen använder en min-heap: initiera heapen med alla celler på kanten och utför sedan en BFS-liknande expansion. Bearbeta cellen med lägst höjd — varje lägre granne måste innehålla vatten åtminstone upp till den aktuella cellens nivå.

Detta är en grundläggande annorlunda algoritm än i 1D-fallet och testar både heapoperationer och BFS-traversering. Tvåpekarttricket från 1D generaliseras inte till 2D, men heapmetoden gör det.

import heapq

def trap_rain_water_2d(heightMap):
    if not heightMap or not heightMap[0]:
        return 0
    m, n = len(heightMap), len(heightMap[0])
    visited = [[False]*n for _ in range(m)]
    heap = []  # (height, row, col)

    # Add all border cells to the heap
    for i in range(m):
        for j in [0, n-1]:
            heapq.heappush(heap, (heightMap[i][j], i, j))
            visited[i][j] = True
    for j in range(n):
        for i in [0, m-1]:
            if not visited[i][j]:
                heapq.heappush(heap, (heightMap[i][j], i, j))
                visited[i][j] = True

    total = 0
    max_h = 0
    while heap:
        h, r, c = heapq.heappop(heap)
        max_h = max(max_h, h)
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r+dr, c+dc
            if 0<=nr<m and 0<=nc<n and not visited[nr][nc]:
                visited[nr][nc] = True
                total += max(0, max_h - heightMap[nr][nc])
                heapq.heappush(heap, (max(max_h, heightMap[nr][nc]), nr, nc))
    return total

map2d = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]
print(trap_rain_water_2d(map2d))  # 4

När de olika metoderna används i intervjuer

Beslutsvägledning för intervjun om att samla regnvatten:

  • Börja med: prefixarrayer — lätta att förklara, visuellt intuitiva och uppenbart korrekta
  • Följdfrågan ”O(1) minnesutrymme?”: två pekare — förklara invarianten att den mindre sidan är flaskhalsen
  • Om intervjuaren frågar ”finns det någon annan metod?”: monoton stack — förklara beräkningen av horisontella lager

Börja alltid med att tydligt definiera vad som avgör vattennivån vid varje position (det minsta av den högsta stapeln på vardera sidan) innan du går över till koden. Detta visar att du har förstått problemet och gör lösningen enklare att förklara.

# Quick summary of all three approaches
approaches = [
    {
        'name': 'Prefix max arrays',
        'time': 'O(n)', 'space': 'O(n)',
        'description': '3 passes: build max_left, max_right, sum water column-by-column',
    },
    {
        'name': 'Two pointers',
        'time': 'O(n)', 'space': 'O(1)',
        'description': 'Process smaller side: its max is the limiting wall, no array needed',
    },
    {
        'name': 'Monotonic stack',
        'time': 'O(n)', 'space': 'O(n)',
        'description': 'Compute water in horizontal layers when a taller bar is encountered',
    },
]
for a in approaches:
    print(f'{a["name"]} [{a["time"]} / {a["space"]}]')
    print(f'  {a["description"]}')
    print()

Randfall och vanliga misstag

Vanliga misstag när man löser problemet att samla regnvatten:

  • Glömma min: vattennivån är min(max_left, max_right), inte bara en av dem. En stapel behöver höga väggar på båda sidor.
  • Negativt vatten: använd max(0, ...) för att begränsa negativa värden till 0 när en positions höjd överstiger vattennivån.
  • Positioner vid kanterna: staplarna längst till vänster och höger kan aldrig hålla vatten (ingen vägg på ena sidan). Prefixarraymetoden hanterar detta naturligt eftersom max_left[0] = height[0] gör att vattnet alltid är 0 vid index 0.
  • Tomma eller mycket små arrayer: returnera 0 för arrayer med färre än 3 element.
def trap(height):
    n = len(height)
    if n < 3:
        return 0   # need at least 3 bars to trap anything

    left, right = 0, n - 1
    max_l = max_r = water = 0
    while left < right:
        if height[left] <= height[right]:
            if height[left] >= max_l:
                max_l = height[left]
            else:
                water += max_l - height[left]  # never negative: max_l > height[left]
            left += 1
        else:
            if height[right] >= max_r:
                max_r = height[right]
            else:
                water += max_r - height[right]
            right -= 1
    return water

# Edge cases
print(trap([]))          # 0: empty
print(trap([1]))         # 0: single bar
print(trap([1,2]))       # 0: two bars
print(trap([3,0,3]))     # 3: simple valley
print(trap([3,3,3]))     # 0: flat top, no water

Snabbtest

Testa din förståelse av begreppen Data Structures & Algorithms — Coding Interview Prep från den här lektionen.

Sammanfattning av lektionen

I den här lektionen har du lärt dig: problemet att samla regnvatten löses genom att hitta det minsta av de högsta vänstra och högra väggarna vid varje position, metoden med två pekare och O(1) minnesutrymme fungerar eftersom det löpande maxvärdet på den sida som är lägre alltid är den begränsande faktorn, och metoden med en monoton stack beräknar vatten i horisontella lager, vilket är användbart när den kombineras med annan stackbaserad logik. Härnäst går vi över till systemdesignkoncept, med RADIO-ramverket för strukturerade intervjusvar som utgångspunkt.

Gratis att börja

Lär dig Förberedelse inför kodningsintervjuer med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
90
Lektioner
360

Vanliga frågor

Är lektionen ”Trapping Rain Water: stack och två pekare” gratis?

Ja – hela texten till ”Trapping Rain Water: stack och två pekare” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Förberedelse inför kodningsintervjuer, kan Ni uppgradera till CoddyKit PRO. Kursen i Förberedelse inför kodningsintervjuer innehåller totalt 4 lektioner.

Vad lär jag mig i ”Trapping Rain Water: stack och två pekare”?

Lös trapping-rain-water med både metoden med monoton stack, som beräknar horisontella lager, och metoden med två pekare, som beräknar vertikala kolumner. Ni övar på Förberedelse inför kodningsintervjuer med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Förberedelse inför kodningsintervjuer?

Du behöver inga förkunskaper. Utbildningen i Förberedelse inför kodningsintervjuer på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 4 av 4.

Hur lång tid tar lektionen ”Trapping Rain Water: stack och två pekare”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Förberedelse inför kodningsintervjuer-lektionen?

Ja. Varje Förberedelse inför kodningsintervjuer-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Monoton stack: ökande eller minskande
  2. Största rektangeln i ett histogram
  3. Maximalt värde i ett glidande fönster med monoton deque
  4. Trapping Rain Water: stack och två pekare
← Tillbaka till Förberedelse inför kodningsintervjuer