Specimen 09 ยท O(V + E) traversal

Depth-First Search

Commit to one path and follow it as far as it goes. Only when you hit a dead end do you back up to the last fork and try the next branch. A stack remembers where to return โ€” the exact opposite discipline to BFS's queue.

โ† Back to the Field Guide
09-namuna ยท O(V + E) traversal

Depth-First Search

Bitta yo'lni tanlab, uni oxirigacha kuzating. Faqat boshi berk ko'chaga duch kelganingizda oxirgi ayrilishga qaytib, keyingi shoxni sinaysiz. Stack qayerga qaytishni eslab qoladi โ€” bu BFS'ning queue'siga tamoman qarama-qarshi tartib.

โ† Qo'llanmaga qaytish

Intuition

Watch the graph below: each dive commits to one neighbor and follows it as deep as it goes before backing up โ€” a completely different order from BFS's rings, using the exact same graph.

Tushuncha (Intuition)

Quyidagi grafga qarang: har bir sho'ng'ish bitta qo'shniga bog'lanadi va orqaga qaytishdan oldin uni iloji boricha chuqur kuzatadi โ€” bir xil grafda, lekin BFS'ning halqalaridan mutlaqo boshqa tartibda.

How It Works

  1. Create an empty stack and push the start node onto it. Create an empty visited set โ€” unlike BFS, nothing gets marked visited yet.
  2. While the stack isn't empty, pop the node at the top of the stack (not the front of a line โ€” that's what makes this depth-first instead of breadth-first) and call it node.
  3. If node is already in visited, skip it and go back to step 2 โ€” it was pushed more than once before ever being explored, and there's no reason to explore it twice.
  4. Otherwise, mark node visited right now, at the moment it's popped (not when it was pushed). This is the "commit" moment: node is officially explored.
  5. Look at every neighbor of node and push every one that is not yet in visited onto the top of the stack.
  6. Because a stack is last-in-first-out, the neighbor pushed most recently is the one explored next, before any neighbor pushed earlier โ€” that's what drives DFS to commit to one path and follow it as deep as it goes, instead of spreading out evenly.
  7. When every neighbor of the current node is already visited (a dead end), the loop simply pops whatever is next on the stack โ€” the most recently pushed still-unvisited node from a few steps back. That's "backing up to the last fork," and it falls straight out of the stack with no separate backtracking logic required.
  8. Stop when the stack is empty. Every node reachable from start has now been visited.

Qanday Ishlaydi

  1. Bo'sh stack yarating va start node'ni unga qo'shing. Bo'sh visited to'plamini yarating โ€” BFS'dan farqli o'laroq, hali hech narsa visited deb belgilanmagan.
  2. Stack bo'sh emas ekan, stack'ning tepasidan node'ni oling (navbat oldidan emas โ€” aynan shu narsa breadth-first emas, depth-first bo'lishini ta'minlaydi) va uni node deb ataymiz.
  3. Agar node allaqachon visitedda bo'lsa, uni o'tkazib yuboring va 2-qadamga qayting โ€” u hali hech qachon ko'rilmasdan turib bir necha marta stack'ga qo'shilgan, uni ikki marta ko'rishning hojati yo'q.
  4. Aks holda, nodeni aynan hozir, stack'dan olingan paytda (qo'shilgan paytda emas) visited deb belgilang. Bu โ€” "bog'lanish" lahzasi: node rasman ko'rilgan hisoblanadi.
  5. nodening har bir qo'shnisiga qarang va hali visitedda bo'lmagan har birini stack'ning tepasiga qo'shing.
  6. Stack last-in-first-out bo'lgani uchun, eng so'nggi qo'shilgan qo'shni keyin ko'riladi โ€” undan oldin qo'shilgan har qanday qo'shnidan oldin. Aynan shu narsa DFS'ni bitta yo'lga bog'lanib, uni tekis tarqalish o'rniga iloji boricha chuqurroq kuzatishga undaydi.
  7. Joriy node'ning barcha qo'shnilari allaqachon ko'rilgan bo'lsa (boshi berk ko'cha), loop shunchaki stack'dagi keyingi narsani oladi โ€” bu bir necha qadam oldin qo'shilgan, hali ko'rilmagan eng so'nggi node. Bu โ€” "oxirgi ayrilishga qaytish", va u qo'shimcha backtracking logikasisiz, to'g'ridan-to'g'ri stack'dan kelib chiqadi.
  8. Stack bo'shaganda to'xtang. startdan yetib boriladigan har bir node endi ko'rilgan.

Complexity

Time: O(V + E), where V is the number of nodes and E is the number of edges. Step 3's visited check guarantees each node is explored โ€” marked visited and has its neighbors scanned โ€” exactly once, which is O(V) of exploration work. A node can still be pushed onto the stack more than once before that happens (once for every edge into it from an already-explored neighbor โ€” exactly what step 3 exists to filter out), but since each push only ever happens while scanning one specific edge during a node's single exploration step, the total number of pushes (and therefore pops) across the whole run is bounded by the number of edges, O(E). Add the exploration work and the stack bookkeeping together and the whole traversal still comes out to O(V + E). On a grid of rows rows and cols columns, every cell is a node and each has at most four edges to its neighbors, so this becomes O(rows ยท cols) โ€” every cell is visited a constant number of times, no matter how the land is shaped.

Space: O(V) in general โ€” the stack (or, written recursively, the call stack) and the visited tracking can, in the worst case, hold nearly every node at once, for example if the graph is one long unbranching path. On a grid, that worst case is a single island shaped like a snake winding through every cell: since the recursive solution below has no separate visited set and instead marks a cell visited by writing directly into the grid, its only real memory cost is the recursion depth โ€” a snake-shaped island forces the recursive calls to stack up O(rows ยท cols) deep before the last cell is reached and the calls finally start unwinding.

Murakkablik

Vaqt: O(V + E), bu yerda V โ€” node'lar soni, E โ€” qirralar soni. 3-qadamdagi visited tekshiruvi har bir node aynan bir marta ko'rilishini (visited deb belgilanib, qo'shnilari skanerlanishini) kafolatlaydi โ€” bu O(V) ko'rish ishi. Node bu sodir bo'lishidan oldin baribir bir necha marta stack'ga qo'shilishi mumkin (allaqachon ko'rilgan har bir qo'shnisidan keladigan har bir qirra uchun bittadan โ€” bu aynan 3-qadam filtrlab tashlaydigan holat), lekin har bir qo'shish bitta node'ning yagona ko'rish qadami paytida faqat bitta qirrani skanerlash chog'ida sodir bo'lgani uchun, butun ish davomidagi jami qo'shishlar (demak, olishlar ham) soni qirralar soniga โ€” O(E)ga โ€” chegaralangan. Ko'rish ishi va stack hisob-kitobini qo'shsangiz, butun traversal baribir O(V + E) bo'lib chiqadi. rows qator va cols ustunli grid'da har bir katak โ€” node, va har birining qo'shnilariga ko'pi bilan to'rtta qirrasi bor, shuning uchun bu O(rows ยท cols)ga aylanadi โ€” quruqlik qanday shaklda bo'lishidan qat'i nazar, har bir katak doimiy sondagi marta ko'riladi.

Xotira: umuman O(V) โ€” stack (yoki rekursiv yozilganda, call stack) va visited kuzatuvi, eng yomon holatda, deyarli barcha node'larni bir vaqtda saqlashi mumkin, masalan graf bitta uzun, shoxlanmagan yo'l bo'lsa. Grid'da bu eng yomon holat โ€” butun grid bo'ylab ilon kabi buralib ketgan yagona orol: quyidagi rekursiv yechim alohida visited to'plamiga ega emasligi va buning o'rniga katakni grid'ning o'ziga yozib visited deb belgilagani uchun, uning yagona haqiqiy xotira xarajati โ€” rekursiya chuqurligi โ€” ilon shaklidagi orol rekursiv chaqiruvlarni oxirgi katakka yetib, chaqiruvlar nihoyat qaytishni boshlagunga qadar O(rows ยท cols) chuqurlikda uyib boradi.

Common Mistakes

  • Marking a cell visited (sinking it) only after making the four recursive calls, instead of before. Fix: flip grid[r][c] to '0' as the very first thing inside the recursive step, before recursing into any neighbor โ€” otherwise a neighbor's own call still sees the original cell as land and calls straight back into it, and the two calls bounce back and forth until Python's recursion limit is hit.
  • Skipping the bounds check, or checking it in the wrong order. Fix: the recursive function's very first check must confirm r and c are still inside [0, rows) and [0, cols) before grid[r][c] is read at all โ€” a negative index like grid[-1][c] doesn't crash in Python, it silently wraps around and reads the last row instead, corrupting the island count with no error message at all.
  • Running the sinking DFS only once, from a single starting cell, and returning early. Fix: the count only comes out right if a fresh scan over every row and column keeps launching a new DFS (and incrementing the count) each time it lands on a cell that is still '1' โ€” that's what finds every separate island, not just the first one.
  • Trusting a purely recursive DFS on a very large grid. Fix: one island can, in the worst case, cover every cell, and a recursive solution then recurses rows ยท cols calls deep โ€” comfortably past Python's default recursion limit (~1000) long before a 300ร—300 grid finishes sinking. Swap in the explicit stack from "How It Works" instead of recursion if the grid can get that large.

Ko'p Uchraydigan Xatolar

  • Katakni visited deb belgilashni (cho'ktirishni) to'rtta rekursiv chaqiruvdan keyin, oldin emas, bajarish. Yechim: grid[r][c]ni '0'ga rekursiv qadam ichida eng birinchi ish sifatida, har qanday qo'shniga rekursiya qilishdan oldin o'zgartiring โ€” aks holda qo'shnining o'z chaqiruvi hali ham asl katakni quruqlik deb ko'radi va to'g'ridan-to'g'ri unga qaytadan chaqiradi, va ikkala chaqiruv Python'ning rekursiya chegarasiga yetguncha orqa-oldinga sakrab yuradi.
  • Chegara tekshiruvini o'tkazib yuborish, yoki uni noto'g'ri tartibda tekshirish. Yechim: rekursiv funksiyaning eng birinchi tekshiruvi r va c hali ham [0, rows) va [0, cols) ichida ekanligini grid[r][c] umuman o'qilishidan oldin tasdiqlashi kerak โ€” Python'da grid[-1][c] kabi manfiy indeks xato bermaydi, u sezmasdan chetlanib oxirgi qatorni o'qiydi, hech qanday xato xabarisiz orollar sonini buzadi.
  • Cho'ktiruvchi DFS'ni faqat bitta boshlang'ich katakdan bir marta ishga tushirib, erta qaytish. Yechim: son faqat har bir qator va ustun bo'ylab yangi skanerlash hali '1' bo'lgan katakka duch kelgan har safar yangi DFS ishga tushirib (va sonni oshirib) borsagina to'g'ri chiqadi โ€” aynan shu narsa faqat birinchisini emas, har bir alohida orolni topadi.
  • Juda katta grid'da faqat rekursiv DFS'ga ishonish. Yechim: eng yomon holatda bitta orol har bir katakni qamrab olishi mumkin, va rekursiv yechim shunda rows ยท cols chaqiruv chuqurlikda rekursiya qiladi โ€” bu 300ร—300 grid cho'kib bo'lishidan ancha oldin Python'ning standart rekursiya chegarasidan (~1000) oshib ketadi. Grid shunchalik katta bo'lishi mumkin bo'lsa, rekursiya o'rniga "Qanday Ishlaydi"dagi explicit stack'ni ishlating.

When to Use It

  • The problem asks you to explore every possible path, arrangement, or combination exhaustively โ€” permutations, combinations, and puzzles like N-Queens or Sudoku all follow DFS's exact "commit, then backtrack on dead end" discipline; this is the shape underneath backtracking.
  • The problem is really about connected components โ€” grouping cells or nodes into clusters that are reachable from each other (islands, regions, friend circles). The order cells are touched within one component doesn't matter, only which component they end up in, so DFS's simple plunge-and-consume style is enough.
  • The problem needs to detect a cycle, or produce an ordering that respects dependencies (topological sort, course prerequisites). These need to know which nodes are still "on the current path" โ€” exactly what a stack (or the call stack, written recursively) naturally tracks.
  • Skip DFS when the problem specifically needs the shortest path or fewest steps in an unweighted graph โ€” that guarantee belongs to breadth-first search's ring-by-ring order, not to a single-path plunge that might wander far before finding a short route.

Qachon Ishlatish Kerak

  • Masala sizdan har qanday mumkin bo'lgan yo'l, joylashuv yoki kombinatsiyani to'liq sanab chiqishni so'raydi โ€” permutatsiyalar, kombinatsiyalar, va N-Queens yoki Sudoku kabi bulmacalar hammasi DFS'ning aynan "bog'lan, keyin boshi berk ko'chada orqaga qayt" tartibiga ergashadi; bu backtracking'ning tagida yotgan shakl.
  • Masala aslida bog'langan komponentlar haqida โ€” kataklar yoki node'larni bir-biridan yetib borish mumkin bo'lgan klasterlarga guruhlash (orollar, hududlar, do'stlar doiralari). Bitta komponent ichida kataklarga qaysi tartibda tegilishi muhim emas, faqat qaysi komponentga tegishli ekanligi muhim โ€” shuning uchun DFS'ning oddiy sho'ng'ish-va-yutish uslubi yetarli.
  • Masala cycle'ni aniqlashi, yoki bog'liqliklarga rioya qiladigan tartib (topological sort, kurs prerequisite'lari) hosil qilishi kerak. Bular qaysi node'lar hali "joriy yo'lda" ekanligini bilishni talab qiladi โ€” bu aynan stack (yoki rekursiv yozilganda, call stack) tabiiy ravishda kuzatadigan narsa.
  • Masala og'irliksiz grafda aynan eng qisqa yo'l yoki eng kam qadamni talab qilsa, DFS'dan saqlaning โ€” bu kafolat breadth-first search'ning halqa-halqa tartibiga tegishli, uzoqqa adashib ketishi mumkin bo'lgan yagona-yo'l sho'ng'ishiga emas.

LeetCode Practice

200. Number of Islands โ†—

Restated: given an m x n grid where each cell is '1' (land) or '0' (water), an island is a group of land cells connected 4-directionally (up, down, left, right). Return the number of islands.

  1. See the grid as a graph. This doesn't look like the node-and-edge diagrams from earlier, but the shape underneath is the same: every cell is a node, and each has up to four edges โ€” to its up, down, left, and right neighbors. An island is nothing more than a connected component of land nodes.
  2. Translate "count the islands" into graph terms. Counting islands means counting connected components of land: scan for a land cell that hasn't been claimed by any island yet, and everything reachable from it by 4-directional land steps belongs to that same island.
  3. Reach for DFS, not BFS, because order doesn't matter here. There's no "shortest" or "closest" to find โ€” the problem only needs the full extent of each connected group, not any particular visiting order. DFS's plunge-then-backtrack style is a natural fit: dive from one land cell in every direction and don't stop until every reachable land cell in that island has been touched. (BFS would touch the exact same set of cells โ€” see the quiz below.)
  4. Design the recursive step to never recount a cell. The moment a land cell is visited, mark it immediately โ€” the cleanest way, with no separate visited set, is to flip it from '1' to '0' right there in the grid, "sinking" it. A single combined guard clause โ€” out of bounds, or not land โ€” stops the recursion at both the edges of the grid and the edges of the island, with no extra bookkeeping.
  5. Wrap it in a full scan of the grid. Walk every cell in row-major order. Whenever a still-unsunk '1' turns up, that's a brand-new island: increment the count once, then let the sinking DFS flush the rest of that island down to '0's before the scan moves on, so none of its cells get counted a second time later.
def numIslands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    def sink(r, c):
        # bounds check comes first -- "or" short-circuits, so grid[r][c]
        # is never read for an out-of-bounds (r, c)
        if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
            return
        grid[r][c] = '0'   # mark visited by sinking this cell BEFORE recursing
        sink(r - 1, c)
        sink(r + 1, c)
        sink(r, c - 1)
        sink(r, c + 1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':   # unvisited land found -- a brand-new island
                count += 1
                sink(r, c)          # sink the whole island so it's never recounted

    return count

LeetCode Amaliyoti

200. Number of Islands โ†—

Qayta bayon: har bir katagi '1' (quruqlik) yoki '0' (suv) bo'lgan m x n grid berilgan, orol esa 4 yo'nalishda (yuqori, past, chap, o'ng) bog'langan quruqlik kataklari guruhi. Orollar sonini qaytaring.

  1. Grid'ni graf sifatida ko'ring. Bu avvalgi node-va-qirra diagrammalariga o'xshamaydi, lekin ostidagi shakl bir xil: har bir katak โ€” node, va har birining to'rttagacha qirrasi bor โ€” yuqori, past, chap va o'ng qo'shnilariga. Orol โ€” quruqlik node'larining bog'langan komponentidan boshqa narsa emas.
  2. "Orollarni sanash"ni graf tiliga tarjima qiling. Orollarni sanash โ€” quruqlik bog'langan komponentlarini sanash degani: hali hech qanday orolga tegishli qilinmagan quruqlik katakni qidiring, va undan 4 yo'nalishli quruqlik qadamlar bilan yetib borish mumkin bo'lgan hamma narsa aynan shu orolga tegishli.
  3. BFS emas, DFS'ni tanlang, chunki bu yerda tartib muhim emas. Topish kerak bo'lgan "eng qisqa" yoki "eng yaqin" narsa yo'q โ€” masalaga faqat har bir bog'langan guruhning to'liq hajmi kerak, biror aniq ko'rish tartibi emas. DFS'ning sho'ng'i-keyin-orqaga-qayt uslubi tabiiy ravishda mos keladi: bitta quruqlik katakdan har tomonga sho'ng'ing va o'sha oroldagi har bir yetib boriladigan quruqlik katak tegilmaguncha to'xtamang. (BFS ham aynan bir xil kataklar to'plamiga tegardi โ€” pastdagi quiz bo'limiga qarang.)
  4. Rekursiv qadamni bironta katakni hech qachon qayta sanamaydigan qilib loyihalang. Quruqlik katak ko'rilgan zahoti, uni darhol belgilang โ€” alohida visited to'plamisiz eng toza yo'li, uni aynan o'sha yerda grid ichida '1'dan '0'ga o'zgartirish, ya'ni "cho'ktirish". Bitta birlashtirilgan guard clause โ€” chegaradan tashqarida, yoki quruqlik emas โ€” rekursiyani ham grid chegaralarida, ham orol chegaralarida to'xtatadi, qo'shimcha hisob-kitobsiz.
  5. Uni grid'ning to'liq skanerlashiga o'rang. Har bir katakni qator bo'yicha tartibda yuring. Hali cho'kmagan '1' duch kelgan har safar, bu โ€” yangi orol: sonni bir marta oshiring, keyin cho'ktiruvchi DFS'ga skanerlash davom etishdan oldin o'sha orolning qolgan qismini '0'larga aylantirishga ruxsat bering, shunda uning hech bir katagi keyinroq ikkinchi marta sanalmaydi.
def numIslands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    def sink(r, c):
        # bounds check comes first -- "or" short-circuits, so grid[r][c]
        # is never read for an out-of-bounds (r, c)
        if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
            return
        grid[r][c] = '0'   # mark visited by sinking this cell BEFORE recursing
        sink(r - 1, c)
        sink(r + 1, c)
        sink(r, c - 1)
        sink(r, c + 1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':   # unvisited land found -- a brand-new island
                count += 1
                sink(r, c)          # sink the whole island so it's never recounted

    return count

Check Yourself

O'zingizni Sinang

ยฉ 2026 Davronbek