Tarkibga o'tish

14 β€” Stack, Queue va Deque

⬅️ Oldingi: 13 β€” Bog'langan ro'yxatlar (linked list) Β· 🏠 README Β· Keyingi: 15 β€” Hash jadval (hash table) ➑️


Bu bobda: Uchta eng ko'p ishlatiladigan chiziqli ma'lumot strukturasi β€” stack (stek), queue (navbat) va deque (ikki uchli navbat) bilan tanishamiz. Avval abstrakt ma'lumot turi (ADT) tushunchasini β€” "qanday amallar" ni "qanday qurilgan" dan ajratishni β€” o'rganamiz. Keyin har birining ish tartibini (LIFO va FIFO), implementatsiyasini (massiv, bog'langan ro'yxat, doiraviy bufer), murakkabligini va real qo'llanmalarini ko'rib chiqamiz. Oxirida priority queue ni qisqacha eslatib o'tamiz.

Halollik / Eslatma: Bu yerda keltirilgan barcha murakkablik chegaralari (push/pop/enqueue/dequeue uchun O(1), sodda massivli dequeue uchun O(n)) aniq va isbotlangan. "O(1)" deganda biz Python ro'yxatining amortizatsiyalangan O(1) append/pop xulqiga tayanamiz β€” bu 11-bobda isbotlangan. Barcha Python namunalari haqiqatan ishga tushirib tekshirildi; ko'rsatilgan chiqishlar kod chiqargan haqiqiy natijalar.


Abstrakt ma'lumot turi (ADT) nima

Tasavvur qiling, do'kondan televizor sotib oldingiz. Sizga pultning tugmalari muhim: "ovozni baland qil", "kanalni almashtir", "o'chir". Bu tugmalar β€” televizor nima qila olishi. Ichida qanday tranzistorlar borligi, signal qanday qayta ishlanishini bilishingiz shart emas. Pult β€” bu interfeys, ichkarisi β€” bu implementatsiya.

Abstrakt ma'lumot turi (ADT) β€” aynan shu g'oya ma'lumot strukturalariga tatbiq etilgani. ADT ikki narsani belgilaydi:

  1. Qanday qiymatlarni saqlaydi (masalan, butun sonlar to'plami).
  2. Qanday amallarni qo'llab-quvvatlaydi va har bir amal nima qiladi (masalan, push(x) β€” yuqoriga element qo'yadi).

ADT qanday qurilganini aytmaydi. Stack ADT'sini massiv bilan ham, bog'langan ro'yxat bilan ham qurish mumkin β€” foydalanuvchi uchun ikkalasi ham bir xil ko'rinadi, chunki amallar to'plami bir xil.

Intuitsiya: ADT β€” bu "shartnoma" (kontrakt). Shartnomada "push, pop, peek bo'ladi va ular shunday ishlaydi" deyiladi. Implementatsiya β€” shartnomani bajaruvchi konkret kod. Bitta shartnomani ko'p xil usulda bajarish mumkin.

Nega bu muhim? Chunki dasturni yozayotganda biz amal nomlari bilan fikrlaymiz ("stekka qo'shaman, stekdan olaman"), implementatsiya tafsilotlari bilan emas. Bu kodimizni soddalashtiradi va keyinchalik implementatsiyani (masalan, tezligi uchun) almashtirsak ham, qolgan kod o'zgarmaydi.

Bu bobdagi uchala struktura β€” stack, queue, deque β€” bular ADT'lar. Quyida har birining shartnomasini, so'ng konkret implementatsiyalarini ko'ramiz.


Stack (stek) β€” LIFO

Stack β€” bu shunday to'plamki, unga element faqat bir uchidan (yuqoridan, "top") qo'shiladi va faqat o'sha uchidan olib tashlanadi. Eng oxirgi qo'shilgan element eng birinchi chiqadi. Bu tartibni LIFO deb atashadi: Last In, First Out β€” "oxirgi kirgan birinchi chiqadi".

Analogiya: Oshxonadagi tarelkalar uyumi. Yangi tarelkani faqat eng ustiga qo'yasiz, va olganingizda ham eng ustidagisini olasiz. O'rtadagi tarelkaga teginib bo'lmaydi.

Stack: push va pop yuqoridan, LIFO tartibi

Amallar va murakkablik

Amal Ma'nosi Murakkablik
push(x) x ni yuqoriga qo'shadi O(1)
pop() yuqoridagi elementni olib, qaytaradi O(1)
peek() / top() yuqoridagini ko'radi, lekin olmaydi O(1)
is_empty() stek bo'shmi? O(1)

Diqqat qiling: hamma amal O(1). Sababi oddiy β€” biz hech qachon hamma elementlarni surishimiz, joyini almashtirishimiz shart emas. Faqat bitta uchni (top) o'zgartiramiz.

Implementatsiya β€” massiv (Python ro'yxati) bilan

Eng tabiiy yo'l: dinamik massivning oxirini stekning "top" i deb belgilaymiz. append β€” push, pop β€” pop. Python ro'yxatida ikkalasi ham amortizatsiyalangan O(1) (11-bob).

class Stack:
    def __init__(self):
        self._items = []
    def push(self, x):
        self._items.append(x)
    def pop(self):
        if self.is_empty():
            raise IndexError("bo'sh stekdan pop")
        return self._items.pop()
    def peek(self):
        if self.is_empty():
            raise IndexError("bo'sh stek")
        return self._items[-1]
    def is_empty(self):
        return len(self._items) == 0
    def __len__(self):
        return len(self._items)

s = Stack()
s.push(10); s.push(20); s.push(30)
print(s.peek())        # -> 30
print(s.pop())         # -> 30
print(s.pop())         # -> 20
print(len(s))          # -> 1
print(s.is_empty())    # -> False

Nega oxirini "top" deb tanladik, boshini emas? Agar massivning boshini top qilsak, har push/pop da qolgan hamma elementni bir katakka surishimiz kerak bo'lardi β€” O(n). Oxirni tanlash bilan suvrish kerak emas β€” O(1). Tafsilot: ADT bir xil, lekin implementatsiya tanlovi tezlikni hal qiladi.

Implementatsiya β€” bog'langan ro'yxat bilan

Stekni bir tomonlama bog'langan ro'yxat bilan ham qurish mumkin: boshini ("head") top deb belgilaymiz. Boshiga tugun qo'shish va boshidagi tugunni o'chirish β€” ro'yxatda O(1) (oxiriga emas, boshiga!). Massivdan farqi: bu yerda hech qanday "ikkilantirib nusxalash" yo'q, har amal qat'iy O(1) (amortized emas). Evaziga har element qo'shimcha ko'rsatkich uchun xotira oladi va kesh-do'stligi pasayadi.

Trade-off: Massivli stek β€” kesh-do'st, kam xotira, lekin amortized O(1) (gohida resize). Ro'yxatli stek β€” qat'iy O(1), lekin ko'proq xotira va sekinroq kesh. Amaliyotda massivli (Python list) versiya deyarli har doim afzal.

Stack qayerda ishlatiladi

  • Funksiya chaqiruvlar steki (call stack). Dastur funksiyani chaqirganda, uning qaytish manzili va lokal o'zgaruvchilari stekka push qilinadi; funksiya tugaganda pop qilinadi. Bu LIFO β€” eng oxirgi chaqirilgan funksiya birinchi tugaydi. Rekursiya aynan shu stekka tayanadi.
  • Qavslar balansini tekshirish. Ochiq qavsni stekka qo'yamiz, yopiq qavsda mosini tekshiramiz (quyida kod bor).
  • Ifoda hisoblash. Infiksdan (3 + 4) postfiksga (3 4 +) o'tkazish va postfiksni baholash stek bilan qilinadi (quyida).
  • Orqaga qaytarish (undo). Matn muharririda har amal stekka qo'yiladi; "undo" bosilganda eng oxirgisi pop qilinadi. Brauzerdagi "orqaga" tugmasi ham β€” sahifalar steki.
  • DFS (chuqurlikka qidiruv). Grafni aylanishda (29-bob) keyingi tashrif buyuriladigan tugunlar stekda saqlanadi (yoki rekursiya steki orqali).

Qo'llanma: qavslar balansi

Matnda qavslar to'g'ri joylashtirilganmi? (a[b]{c}) β€” to'g'ri; ([)] β€” noto'g'ri (qavslar kesishgan). G'oya: har ochiq qavsni stekka qo'yamiz; yopiq qavs kelganda stek tepasida aynan unga mos ochiq qavs bo'lishi shart.

def qavs_balansli(matn):
    juft = {')': '(', ']': '[', '}': '{'}
    stek = []
    for ch in matn:
        if ch in '([{':
            stek.append(ch)
        elif ch in ')]}':
            if not stek or stek.pop() != juft[ch]:
                return False
    return not stek   # oxirida stek bo'sh bo'lishi shart

print(qavs_balansli("(a[b]{c})"))   # -> True
print(qavs_balansli("([)]"))        # -> False
print(qavs_balansli("(("))          # -> False
print(qavs_balansli(""))            # -> True

Nega ishlaydi (to'g'rilik eskizi): Stek tepasi β€” hozircha hali yopilmagan eng yaqin ochiq qavs. Yopiq qavs aynan shu eng yaqin ochiqqa mos kelishi kerak (qavslar uyalangan, kesishmaydi). Agar mos kelmasa yoki stek bo'sh bo'lsa β€” xato. Oxirida stek bo'sh bo'lsa, hamma ochiq qavs yopilgan demakdir. LIFO tartibi qavslarning uyalanish (nesting) tabiatiga aynan mos keladi.

Qo'llanma: postfiks (Polsha teskari) ifodani baholash

Odatdagi yozuv β€” infiks: amal operandlar orasida (3 + 4). Postfiks (RPN) da amal operandlardan keyin keladi: 3 4 +. Postfiksda qavs umuman kerak emas va uni stek bilan bir o'tishda baholash mumkin:

token postfiks: token operand bo'lsa -> stekka push
                token amal bo'lsa     -> 2 operandni pop, hisobla, natijani push
oxirida stekda bitta qiymat qoladi β€” bu javob
def postfix_baholash(ifoda):
    stek = []
    amallar = {'+', '-', '*', '/'}
    for token in ifoda.split():
        if token in amallar:
            b = stek.pop()         # ikkinchi operand
            a = stek.pop()         # birinchi operand
            if token == '+': stek.append(a + b)
            elif token == '-': stek.append(a - b)
            elif token == '*': stek.append(a * b)
            elif token == '/': stek.append(a / b)
        else:
            stek.append(float(token))
    return stek.pop()

print(postfix_baholash("3 4 +"))             # -> 7.0
print(postfix_baholash("5 1 2 + 4 * + 3 -")) # -> 14.0
print(postfix_baholash("2 3 4 * +"))         # -> 14.0

Trassirovka: 5 1 2 + 4 * + 3 - ifodasi uchun stek holati har bir tokenda:

token amal stek (keyin)
5 push 5 [5]
1 push 1 [5, 1]
2 push 2 [5, 1, 2]
+ 1+2=3 [5, 3]
4 push 4 [5, 3, 4]
* 3*4=12 [5, 12]
+ 5+12=17 [17]
3 push 3 [17, 3]
- 17-3=14 [14]

Oxirgi natija: 14.0. Operandlar tartibiga e'tibor bering: a - b da a avval pop qilingan ikkinchi operand emas, birinchi push qilingani β€” shuning uchun b ni avval, a ni keyin pop qilamiz.


Queue (navbat) β€” FIFO

Queue β€” bu shunday to'plamki, element bir uchidan (orqadan, "rear") qo'shiladi, lekin boshqa uchidan (oldindan, "front") olinadi. Birinchi kelgan element birinchi chiqadi. Bu tartib β€” FIFO: First In, First Out β€” "birinchi kirgan birinchi chiqadi".

Analogiya: Kassadagi yoki bankdagi navbat. Birinchi kelgan odam birinchi xizmat oladi (front); yangi kelgan odam navbat oxiriga turadi (rear). Adolatli tartib.

Queue: enqueue orqadan, dequeue oldindan, FIFO tartibi

Amallar

Amal Ma'nosi Murakkablik (to'g'ri impl.)
enqueue(x) x ni oxiriga (rear) qo'shadi O(1)
dequeue() boshidagi (front) elementni olib qaytaradi O(1)
front() boshidagini ko'radi, olmaydi O(1)
is_empty() navbat bo'shmi? O(1)

Sodda massivli yondashuvning muammosi

Stekni biz massiv oxiri bilan osongina yasagandik. Queue bilan muammo bor. Aytaylik, massivning boshi front bo'lsin. dequeue boshidan element olib tashlaydi β€” lekin endi qolgan hamma element bir katakka chapga surilishi kerak! Bu O(n). n ta dequeue qilsak β€” O(nΒ²). Bu juda sekin.

[A, B, C, D]   dequeue() -> A olindi
[B, C, D]      <- B, C, D hammasi chapga surildi: O(n)

Bu muammoning uchta yechimi bor:

  1. Doiraviy bufer (circular / ring buffer) β€” eng nafis yechim. Quyida.
  2. Ikki ko'rsatkich β€” alohida head va tail indekslarini saqlaymiz, surmaymiz; lekin massiv tugab qoladi (yoki ortib ketadi), shuning uchun ko'pincha baribir doiraviy qilamiz.
  3. Bog'langan ro'yxat β€” boshiga ham, oxiriga ham ko'rsatkich (head, tail) saqlasak, ikkalasidan ham O(1) (13-bob). Python'da bu collections.deque ichida tayyor.

Doiraviy bufer (ring buffer)

G'oya: sobit hajmli massivni halqaga aylantiramiz β€” oxirgi katakdan keyin yana birinchi katakka "o'raladi". Ikki indeks saqlaymiz: head (front qayerda) va tail (keyingi bo'sh joy). Har ikkalasi ham % hajm orqali aylanadi. Hech narsa surilmaydi β€” har amal O(1).

Doiraviy bufer: head va tail ko'rsatkichlari aylanadi

Kalit formula β€” indeksni aylantirish:

keyingi_indeks = (indeks + 1) % hajm

% hajm (modul) tufayli indeks hajm - 1 dan keyin 0 ga qaytadi β€” aynan halqaning "o'ralishi".

class Queue:
    def __init__(self, hajm):
        self._buf = [None] * hajm
        self._hajm = hajm
        self._head = 0     # front shu yerda
        self._tail = 0     # keyingi bo'sh joy
        self._soni = 0     # nechta element bor
    def enqueue(self, x):
        if self._soni == self._hajm:
            raise OverflowError("navbat to'la")
        self._buf[self._tail] = x
        self._tail = (self._tail + 1) % self._hajm
        self._soni += 1
    def dequeue(self):
        if self._soni == 0:
            raise IndexError("navbat bo'sh")
        x = self._buf[self._head]
        self._buf[self._head] = None
        self._head = (self._head + 1) % self._hajm
        self._soni -= 1
        return x
    def front(self):
        if self._soni == 0:
            raise IndexError("navbat bo'sh")
        return self._buf[self._head]
    def __len__(self):
        return self._soni

q = Queue(3)
q.enqueue('A'); q.enqueue('B')
print(q.dequeue())   # -> A
q.enqueue('C'); q.enqueue('D')   # tail 0 ga o'raladi
print(q.front())     # -> B
print(q.dequeue())   # -> B
print(q.dequeue())   # -> C
print(q.dequeue())   # -> D
print(len(q))        # -> 0

Nega alohida _soni saqlaymiz? Faqat head va tail bilan "bo'sh" va "to'la" holatlarni ajratib bo'lmaydi: ikkalasida ham head == tail bo'lishi mumkin. Element sonini alohida hisoblash bu noaniqlikni yo'qotadi. (Muqobil yo'l β€” bitta katakni doim bo'sh qoldirish.)

Trassirovka (hajm = 3): head, tail, soni qiymatlari amallar bo'yicha (boshlang'ich hammasi 0):

amal buf head tail soni
enqueue A [A,_,_] 0 1 1
enqueue B [A,B,_] 0 2 2
dequeue→A [_,B,_] 1 2 1
enqueue C [_,B,C] 1 0 2
enqueue D [D,B,C] 1 1 3

E'tibor bering: enqueue C da tail 2β†’0 ga o'raldi ((2+1)%3 = 0), va D massivning [0] katagiga, ya'ni eng boshiga yozildi β€” bu halqaning butun mohiyati.

Queue qayerda ishlatiladi

  • BFS (kenglikka qidiruv). Grafni qatlam-qatlam aylanishda (29-bob) keyingi tugunlar navbatda saqlanadi β€” bu DFS'dan (stek) asosiy farqi.
  • Vazifa navbati (task queue). Server so'rovlari, printer ishlari, fon vazifalari kelgan tartibda bajariladi.
  • Bufer (buffering). Klaviatura bosishlari, tarmoq paketlari, audio/video oqimi β€” ishlab chiqaruvchi (producer) va iste'molchi (consumer) tezligi farq qilganda doiraviy bufer ma'lumotni vaqtincha ushlab turadi.

Deque (ikki uchli navbat)

Deque (double-ended queue, "dek" deb o'qiladi) β€” ikkala uchidan ham qo'shish va o'chirishga ruxsat beradigan struktura. To'rt asosiy amal, hammasi O(1):

Amal Ma'nosi
append(x) o'ng (orqa) uchga qo'shadi
appendleft(x) chap (old) uchga qo'shadi
pop() o'ng uchdan oladi
popleft() chap uchdan oladi

Deque β€” eng moslashuvchan struktura: u stek ham, queue ham bo'la oladi. Faqat o'ng uchni ishlatsangiz (append/pop) β€” bu stek. Bir uchdan qo'shib (append), boshqasidan olsangiz (popleft) β€” bu queue.

Python'da deque tayyor β€” collections.deque. U ichkarida ikki tomonlama bog'langan bloklar bilan qurilgan, shuning uchun ikkala uchdan ham qat'iy O(1):

from collections import deque
d = deque()
d.append(1)        # o'ngga: [1]
d.append(2)        # o'ngga: [1, 2]
d.appendleft(0)    # chapga: [0, 1, 2]
print(list(d))     # -> [0, 1, 2]
print(d.pop())     # -> 2   (o'ngdan)
print(d.popleft()) # -> 0   (chapdan)
print(list(d))     # -> [1]

Eslatma: Python'da navbat (queue) kerak bo'lsa, oddiy list ni pop(0) bilan ishlatmang β€” u O(n)! Buning o'rniga collections.deque ni ishlating: popleft() O(1). Stek uchun esa oddiy list (append/pop) yetarli va tez.


Priority queue β€” qisqacha eslatma

Oddiy queue'da tartib qat'iy FIFO: kim avval kelsa, o'sha avval chiqadi. Lekin ba'zan bizga muhimlik (prioritet) bo'yicha tartib kerak: navbatdagi tartibdan qat'i nazar, eng yuqori prioritetli element birinchi chiqsin.

Buni priority queue (prioritetli navbat) beradi. Masalan, kasalxonadagi tez yordam β€” kelish tartibida emas, og'irlik darajasi bo'yicha qabul qiladi. Yoki Dijkstra algoritmida (30-bob) eng kichik masofali tugun birinchi olinadi.

Priority queue'ni samarali (har amal O(log n)) qilib heap (uyum) bilan quramiz β€” bu 19-bobning mavzusi. Hozir shuni eslab qoling: priority queue oddiy queue emas β€” undagi "birinchi chiqadigan" element kelish vaqti bilan emas, prioritet bilan aniqlanadi.


Asosiy g'oyalar (bobni qisqacha)

  • ADT β€” amallar to'plamini (shartnoma) implementatsiyadan (qanday qurilgani) ajratadi. Bir ADT'ni ko'p xil usulda qurish mumkin.
  • Stack β€” LIFO, hamma uchidan (top): push, pop, peek β€” hammasi O(1). Qo'llanmalar: call stack, qavs balansi, postfiks baholash, undo, DFS.
  • Queue β€” FIFO, bir uchdan qo'shib (rear), boshqasidan olamiz (front): enqueue, dequeue β€” to'g'ri impl.da O(1).
  • Sodda massivli queue'da dequeue O(n) (surish kerak). Yechim: doiraviy bufer (% hajm bilan aylanish), ikki ko'rsatkich yoki bog'langan ro'yxat.
  • Deque β€” ikkala uchidan ham O(1); stek ham, queue ham bo'la oladi. Python'da collections.deque.
  • Priority queue β€” prioritet bo'yicha chiqaradi (FIFO emas); heap bilan O(log n) (19-bob).

Mashqlar

Oson

1-mashq. Bo'sh stekka quyidagi amallar qo'llanildi: push(1), push(2), push(3), pop(), push(4), pop(), pop(). Har pop() qanday qiymat qaytaradi va oxirida stekda nima qoladi?

2-mashq. Bo'sh queue'ga: enqueue(1), enqueue(2), enqueue(3), dequeue(), enqueue(4), dequeue(). Har dequeue() nima qaytaradi va oxirida navbatda nima qoladi?

3-mashq. Quyidagi qaysi vazifalarga stack, qaysiga queue mos keladi? (a) brauzerdagi "orqaga" tugmasi; (b) printerga yuborilgan hujjatlar; (c) matn muharriridagi "undo"; (d) tezligi bir xil bo'lmagan tarmoqdan kelayotgan paketlarni bufer qilish.

O'rta

4-mashq. qavs_balansli funksiyasini qo'lda kuzating: "{[()]}" va "{[(])}" uchun stek qanday o'zgaradi va natija nima?

5-mashq. Faqat stek strukturasidan foydalanib (massivni indeks bilan teskari aylantirmasdan), ro'yxatni teskari qiluvchi funksiya yozing. Nega LIFO bu vazifaga tabiiy mos keladi?

6-mashq. Ikkita stek yordamida queue (FIFO) yasang: enqueue va dequeue amallarini implement qiling. Nega bu ishlaydi va dequeue ning amortizatsiyalangan murakkabligi nima?

Qiyin

7-mashq. Postfiks ifoda "6 2 / 3 -" ni qo'lda baholang β€” har tokenda stek holatini yozing. Natija nima?

8-mashq. Sobit hajmli doiraviy buferni head, tail va soni bilan implement qiling (yuqoridagi Queue klassiga o'xshash), va hajm = 3 bilan enqueue('A'), enqueue('B'), enqueue('C'), dequeue(), enqueue('D') ketma-ketligida head/tail/soni qiymatlarini trassirovka jadvalida ko'rsating. D qaysi indeksga tushadi?

9-mashq. Min-stack yarating: oddiy stek amallaridan tashqari get_min() β€” stekdagi eng kichik elementni O(1) da qaytaradi. (Maslahat: ikkinchi yordamchi stek ishlating.) Nega bu O(1) ishlaydi?

Yechimlar

1-mashq yechimi

Stek (chapdan o'ngga, o'ng uchi top):

amal stek (keyin) qaytdi
push(1) [1] β€”
push(2) [1, 2] β€”
push(3) [1, 2, 3] β€”
pop() [1, 2] 3
push(4) [1, 2, 4] β€”
pop() [1, 2] 4
pop() [1] 2

pop() lar mos ravishda 3, 4, 2 qaytaradi. Oxirida stekda [1] qoladi. LIFO: har gal eng oxirgi qo'yilgan chiqadi.

2-mashq yechimi

Queue (chap β€” front, o'ng β€” rear):

amal navbat (keyin) qaytdi
enqueue(1) [1] β€”
enqueue(2) [1, 2] β€”
enqueue(3) [1, 2, 3] β€”
dequeue() [2, 3] 1
enqueue(4) [2, 3, 4] β€”
dequeue() [3, 4] 2

dequeue() lar mos ravishda 1, 2 qaytaradi. Oxirida navbatda [3, 4] qoladi. FIFO: har gal eng birinchi kelgan chiqadi β€” stek (1-mashq) bilan tartibi tamoman teskari.

3-mashq yechimi

  • (a) brauzer "orqaga" β€” stack (LIFO). Eng oxirgi tashrif buyurilgan sahifaga birinchi qaytasiz.
  • (b) printer hujjatlari β€” queue (FIFO). Birinchi yuborilgan birinchi chop etiladi (adolatli tartib).
  • (c) "undo" β€” stack (LIFO). Eng oxirgi amal birinchi bekor qilinadi.
  • (d) paketlarni bufer qilish β€” queue (FIFO). Kelgan tartibni saqlash kerak; odatda doiraviy bufer bilan.

4-mashq yechimi

"{[()]}" (to'g'ri uyalangan):

ch amal stek
{ push ['{']
[ push ['{','[']
( push ['{','[','(']
) pop ( mos βœ“ ['{','[']
] pop [ mos βœ“ ['{']
} pop { mos βœ“ []

Oxirida stek bo'sh β†’ True.

"{[(])}":

ch amal stek
{ push ['{']
[ push ['{','[']
( push ['{','[','(']
] pop β†’ ( chiqadi, lekin ] ga [ kerak βœ— β€”

] kelganda stek tepasi (, mos emas β†’ darhol False. Qavslar kesishgan ([ ( ichida ochilib, ] ) dan oldin yopilmoqchi) β€” bu noto'g'ri uyalanish.

5-mashq yechimi

def teskari(ketma):
    stek = list(ketma)   # hamma elementni stekka push qildik
    natija = []
    while stek:
        natija.append(stek.pop())   # LIFO: oxirgisidan boshlab chiqaradi
    return natija

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

LIFO tartibi tabiiy mos keladi, chunki stek elementlarni kirish tartibiga teskari chiqaradi: birinchi push qilingan (1) oxirgi pop qilinadi, oxirgi push qilingan (4) birinchi pop qilinadi. Demak pop'lar ketma-ketligi β€” aynan teskari tartib. Murakkablik O(n).

6-mashq yechimi

Ikki stek: kirish (enqueue shu yerga) va chiqish (dequeue shu yerdan). dequeue da chiqish bo'sh bo'lsa, kirishdagi hammasini chiqishga ag'daramiz β€” bu tartibni teskari qiladi, ya'ni eng eski element chiqish tepasiga keladi.

class TwoStackQueue:
    def __init__(self):
        self._kirish = []
        self._chiqish = []
    def enqueue(self, x):
        self._kirish.append(x)
    def dequeue(self):
        if not self._chiqish:                 # chiqish bo'sh bo'lsa
            while self._kirish:                # kirishni teskari ag'daramiz
                self._chiqish.append(self._kirish.pop())
        return self._chiqish.pop()

tq = TwoStackQueue()
tq.enqueue(1); tq.enqueue(2); tq.enqueue(3)
print(tq.dequeue())  # -> 1
tq.enqueue(4)
print(tq.dequeue())  # -> 2
print(tq.dequeue())  # -> 3
print(tq.dequeue())  # -> 4

Nega ishlaydi: kirish LIFO bilan to'planadi ([1,2,3], top β€” 3). Uni chiqishga bittalab ag'darganda tartib teskari bo'ladi ([3,2,1], top β€” 1), shuning uchun chiqish pop'i eng eski elementni (1) beradi β€” FIFO. Amortizatsiyalangan murakkablik: har element umrida aniq bir marta kirishga push, bir marta pop, bir marta chiqishga push, bir marta pop bo'ladi β€” jami 4 ta O(1) amal. Demak n ta amalga jami O(n), ya'ni amortized O(1) har dequeue uchun, ag'darish gohida qimmat ko'rinsa-da.

7-mashq yechimi

"6 2 / 3 -":

token amal stek (keyin)
6 push 6 [6.0]
2 push 2 [6.0, 2.0]
/ 6/2 = 3 [3.0]
3 push 3 [3.0, 3.0]
- 3βˆ’3 = 0 [0.0]

Natija: 0.0. Diqqat: / da a=6 (birinchi pop qilingan ikkinchi operand emas β€” biz bni avval, ani keyin pop qilamiz), b=2, demak a/b = 6/2 = 3. Xuddi shunday - da 3 βˆ’ 3 = 0.

8-mashq yechimi

class DoiraviyBufer:
    def __init__(self, hajm):
        self._buf = [None] * hajm
        self._hajm = hajm
        self._head = 0
        self._tail = 0
        self._soni = 0
    def enqueue(self, x):
        if self._soni == self._hajm:
            raise OverflowError("to'la")
        self._buf[self._tail] = x
        self._tail = (self._tail + 1) % self._hajm
        self._soni += 1
    def dequeue(self):
        if self._soni == 0:
            raise IndexError("bo'sh")
        x = self._buf[self._head]
        self._head = (self._head + 1) % self._hajm
        self._soni -= 1
        return x

Trassirovka (hajm = 3):

amal buf head tail soni
enqueue A [A,_,_] 0 1 1
enqueue B [A,B,_] 0 2 2
enqueue C [A,B,C] 0 0 3
dequeue→A [A,B,C] 1 0 2
enqueue D [D,B,C] 1 1 3

enqueue C da tail 2β†’0 ga o'raldi. enqueue D tail = 0 ga, ya'ni massivning [0] indeksiga yozdi (eski A ustiga β€” u allaqachon dequeue qilingan, bo'sh hisoblanadi). Demak D indeks 0 ga tushadi. Bu halqaning butun afzalligi: massiv to'lib qolmasa, hech narsa surilmaydi, bo'shagan boshidagi joy qayta ishlatiladi.

9-mashq yechimi

Ikkinchi stek minlar saqlaymiz: har bosqichda minlar tepasida β€” asosiy stekning shu paytdagi eng kichik elementi. pushda yangi element joriy mindan kichik yoki teng bo'lsa, uni minlarga qo'yamiz; aks holda joriy minni takror qo'yamiz. Shunda ikkala stek doim bir xil balandlikda bo'ladi va popda ikkalasidan ham olamiz.

class MinStack:
    def __init__(self):
        self._asosiy = []
        self._minlar = []
    def push(self, x):
        self._asosiy.append(x)
        if not self._minlar or x <= self._minlar[-1]:
            self._minlar.append(x)
        else:
            self._minlar.append(self._minlar[-1])
    def pop(self):
        self._minlar.pop()
        return self._asosiy.pop()
    def get_min(self):
        return self._minlar[-1]

ms = MinStack()
ms.push(5); ms.push(3); ms.push(7); ms.push(2)
print(ms.get_min())  # -> 2
ms.pop()             # 2 chiqdi
print(ms.get_min())  # -> 3
ms.pop()             # 7 chiqdi
print(ms.get_min())  # -> 3

Nega O(1): get_min shunchaki minlar[-1] ni o'qiydi β€” qidiruv yo'q, O(1). push/pop ham bittadan element qo'shadi/oladi β€” O(1). Sir shundaki, biz minimumni har holat uchun oldindan hisoblab, minlar stekining mos balandligida saqlaymiz; pop qilinganda oldingi holatning minimumi minlar tepasida tayyor turadi. Evaziga 2x xotira ishlatamiz β€” klassik vaqt-xotira trade-off.


⬅️ Oldingi: 13 β€” Bog'langan ro'yxatlar (linked list) Β· 🏠 README Β· Keyingi: 15 β€” Hash jadval (hash table) ➑️