Linked list (bog‘langan ro‘yxat)
Tasavvur qiling, musiqa pleyeridagi qo‘shiqlar ketma-ketligini boshqaryapsiz. Har bir qo‘shiqdan keyin qaysi qo‘shiq ijro etilishi kerakligini bilish kifoya. Ro‘yxat o‘rtasiga yangi qo‘shiq qo‘shilsa, undan keyingi barcha qo‘shiqlarni boshqa joyga ko‘chirish shart emas: oldingi qo‘shiqning “keyingisi” haqidagi ma’lumotini yangi qo‘shiqqa, yangi qo‘shiqnikini esa navbatdagi qo‘shiqqa yo‘naltirish mumkin.
Linked list, ya’ni bog‘langan ro‘yxat, elementlarni aynan shu g‘oya asosida saqlaydigan ma’lumotlar tuzilmasidir. Undagi elementlar xotirada yonma-yon turishi shart emas. Har bir element o‘z qiymati bilan birga ketma-ketlikdagi boshqa elementga olib boruvchi bog‘lanishni ham saqlaydi.
Bu tuzilma arraydagi muhim cheklovni yumshatish uchun kerak: array o‘rtasiga element qo‘shilganda yoki undan element o‘chirilganda boshqa elementlarni surish talab qilinishi mumkin. Linked listda esa kerakli joy oldindan topilgan bo‘lsa, odatda bir nechta bog‘lanishni almashtirish yetarli. Buning evaziga linked list arraydagi index orqali tezkor kirish imkoniyatidan voz kechadi. Masalan, uchinchi elementga yetish uchun birinchi elementdan boshlab bog‘lanishlar bo‘ylab yurish kerak.
Linked listni tushunishning markazida bitta muvozanat turadi: elementlarni jismonan yonma-yon saqlash o‘rniga, ularning mantiqiy tartibini bog‘lanishlar orqali saqlash. Bu ayrim qo‘shish va o‘chirish amallarini qulay qiladi, lekin qidirish, index bo‘yicha kirish va xotiradan samarali foydalanishni qimmatlashtiradi.
Zanjir analogiyasi
Linked listni alohida halqalardan tuzilgan zanjirga o‘xshatish mumkin. Har bir halqa keyingi halqaga ulangan. Zanjirning boshini bilsak, birinchi halqadan ikkinchisiga, undan uchinchisiga o‘tib, oxirigacha yetamiz.
O‘rtaga yangi halqa qo‘shish uchun butun zanjirni yangidan yasash shart emas. Bitta ulanishni uzib, yangi halqani ikki tomonga ulaymiz. Ammo “yuzinchi halqani darhol ber” deyilsa, zanjirning boshidan boshlab 99 ta ulanish bo‘ylab yurishga to‘g‘ri keladi. Chunki halqalarda arraydagi kabi joylashuv indexidan manzil hisoblaydigan mexanizm yo‘q.
Analogiyaning chegarasi ham bor: kompyuter xotirasidagi node’lar bir-biriga jismonan tegib turmaydi. Ularni bog‘laydigan narsa — boshqa node joylashgan manzilni bildiruvchi reference.
Asosiy terminlar
Linked list bilan ishlaganda quyidagi tushunchalarni ajratish kerak:
- node (tugun) — qiymat va kamida bitta bog‘lanishni saqlaydigan yaxlit qism;
- value yoki data — node ichidagi foydali ma’lumot, masalan, son, buyurtma yoki qo‘shiq;
- reference — boshqa node’ni topish imkonini beruvchi ma’lumot;
- next — keyingi node’ga olib boruvchi reference;
- previous yoki prev — oldingi node’ga olib boruvchi reference;
- head — ro‘yxatdagi birinchi node’ga reference;
- tail — ro‘yxatdagi oxirgi node’ga reference;
- traversal — bog‘lanishlar bo‘ylab node’larni ketma-ket ko‘rib chiqish;
- length — ro‘yxatdagi node’lar soni.
Element va node bir xil tushuncha emas. Element deganda foydalanuvchi saqlamoqchi bo‘lgan qiymat nazarda tutilishi mumkin. Node esa shu qiymatni va tuzilma ishlashi uchun zarur reference’larni o‘rab turadi:
element: 25
node:
+----------+------------------+
| value=25 | next=manzil_8200 |
+----------+------------------+
Reference bilan obyektning o‘zini ham aralashtirmaslik kerak. Reference node’ning nusxasi emas; u node’ga qanday yetib borishni bildiradi. Past darajali tushuntirishlarda buning uchun ko‘pincha pointer so‘zi ishlatiladi. Pointer odatda xotira manzilini bevosita saqlaydi. Yuqori darajali tillardagi reference esa tashqi ko‘rinishda boshqacha boshqarilishi mumkin. Linked listning fundamental modeli uchun ikkalasi ham “boshqa node’ga olib boruvchi bog‘lanish” vazifasini bajaradi.
Bog‘lanishning mavjud emasligini pseudocodeda NULL bilan belgilaymiz. Muayyan til bunga null, nil, None
yoki boshqa nom berishi mumkin.
Xotirada qanday ishlaydi?
Klassik array uchun elementlar uzluksiz xotira (contiguous memory) hududida yonma-yon joylashadi. Linked list node’lari esa xotiraning turli joylarida bo‘lishi mumkin. Faraz qilaylik, uchta node quyidagi manzillarda yaratilgan:
head = 5000
Manzil 5000 Manzil 9100
+------------+------------+ +------------+------------+
| value = 10 | next = 2400| ------> | value = 30 | next = NULL|
+------------+------------+ +------------+------------+
|
| Manzil 2400
+-> +------------+------------+
| value = 20 | next = 9100|
+------------+------------+
Mantiqiy tartib: 10 → 20 → 30
Diagrammadagi qutilarning chizilish tartibi xotiradagi jismoniy tartibni bildirmaydi. 20 qiymatli node pastroq
manzilda tursa ham, mantiqiy ketma-ketlikda ikkinchi keladi. Tartibni manzillarning kattaligi emas, next
reference’lari belgilaydi.
Ro‘yxatning o‘zi hech bo‘lmaganda head reference’ini saqlashi kerak. Ko‘p implementatsiyalarda oxiriga tez
qo‘shish uchun tail, uzunlikni darhol bilish uchun esa length ham saqlanadi:
list:
head → birinchi node
tail → oxirgi node (ixtiyoriy, lekin ko‘p uchraydi)
length → node’lar soni (ixtiyoriy)
tail va length qulaylik beradi, ammo har bir o‘zgarishda ularni ham to‘g‘ri yangilash kerak. Masalan, oxirgi
node o‘chirilganda tail yangi oxirgi node’ga o‘tishi shart. Aks holda tuzilmaning ichki holati buziladi.
Node yaratish va xotirani bo‘shatish
Yangi element qo‘shilganda odatda yangi node uchun alohida xotira ajratiladi. Qiymat va reference shu node ichiga yoziladi, keyin mavjud bog‘lanishlar yangilanadi. Node o‘chirilganda esa unga olib boruvchi bog‘lanish zanjirdan chiqariladi. Shundan so‘ng xotirani kim va qachon bo‘shatishi dasturlash tilining xotira boshqaruviga bog‘liq:
- qo‘lda boshqariladigan muhitda dasturchi ajratilgan xotirani bo‘shatishi kerak;
- avtomatik xotira boshqaruvida node’ga boshqa reference qolmagach, garbage collector uni keyinroq tozalashi mumkin.
Bog‘lanishni uzishning o‘zi har qanday muhitda xotira darhol bo‘shadi degani emas. Boshqa joyda o‘sha node’ga reference saqlanib turgan bo‘lishi mumkin.
Nega linked list amalda arraydan sekinroq bo‘lishi mumkin?
Big O ikki amal o‘sish tartibi haqida ma’lumot beradi, lekin apparatdagi barcha xarajatni ko‘rsatmaydi. Array elementlari yonma-yon bo‘lgani uchun protsessor bir elementni o‘qiyotganda uning yonidagilarni ham tezkor xotira — CPU cachega olib kelishi mumkin. Linked list node’lari tarqoq joylashgani sababli navbatdagi node uchun asosiy xotiraning boshqa hududiga murojaat qilish talab qilinishi mumkin.
Shu sababli array va linked list bo‘ylab yurish ikkalasi ham O(n) bo‘lsa-da, array amalda ko‘pincha tezroq
bo‘ladi. Bundan tashqari, har bir linked list node’i qiymatdan tashqari kamida bitta reference saqlaydi va odatda
alohida ajratiladi. Bu qo‘shimcha xotira va xotira boshqaruvi xarajatini keltirib chiqaradi.
Nega aynan shunday qurilgan?
Array tartibni elementlarning xotiradagi joylashuvi bilan ifodalaydi. Shu sababli indexdan manzilni hisoblash
oson, ammo o‘rtada bo‘sh joy ochish qiyin. Linked list tartibni xotiradagi joylashuvdan ajratadi: “qaysi node
keyin keladi?” degan javob next ichida turadi.
Bu qaror quyidagi almashinuvni yuzaga keltiradi:
- elementlarni ko‘chirmasdan bog‘lanishni o‘zgartirish mumkin;
- ro‘yxat uchun oldindan katta uzluksiz xotira hududi kerak emas;
- lekin har bir node reference uchun qo‘shimcha joy ishlatadi;
- kerakli o‘ringa bog‘lanishlar bo‘ylab yetib borish talab qilinadi;
- node’larning tarqoq joylashuvi cache samaradorligini pasaytiradi.
Demak, linked list “arrayning har jihatdan yaxshiroq turi” emas. U boshqa operatsiyalar to‘plamini ustun qo‘yadigan modeldir.
Linked list turlari
Bog‘langan ro‘yxatlar node ichida qaysi yo‘nalishdagi reference’lar saqlanishi va oxirgi node qayerga ulanishiga qarab farqlanadi.
Singly linked list
Singly linked list, ya’ni bir tomonlama bog‘langan ro‘yxatda har bir node faqat keyingi node’ga reference
saqlaydi. Oxirgi node’ning next qiymati NULL bo‘ladi.
Ro‘yxat bo‘ylab faqat oldinga yurish mumkin. C node’da turganda B ga qaytish uchun oldingi node’ga tayyor
bog‘lanish yo‘q. Uni topish kerak bo‘lsa, headdan yana yurish talab qilinadi.
Singly linked list reference uchun kamroq qo‘shimcha xotira ishlatadi va bog‘lanishlarni boshqarish nisbatan sodda. U quyidagi vaziyatlarda mos keladi:
- faqat oldinga ketma-ket ishlov beriladigan vazifalar;
- elementlar boshiga tez-tez qo‘shiladigan ro‘yxat;
- alohida zanjirlash ishlatadigan hash table bucketlari;
- bo‘sh bloklar zanjiri kabi sodda ichki boshqaruv tuzilmalari.
Masalan, hash table’da bir xil bucketga tushgan yozuvlar kichik singly linked listga ulanadigan implementatsiya bo‘lishi mumkin. Yangi yozuvni zanjir boshiga ulash tez, barcha yozuvlarni faqat oldinga ko‘rib chiqish esa bu vazifa uchun yetarli. Zamonaviy implementatsiyalar uzun zanjirlar uchun boshqa tuzilmalarga o‘tishi ham mumkin; bu universal qoida emas.
Doubly linked list
Doubly linked list, ya’ni ikki tomonlama bog‘langan ro‘yxatda har bir node keyingi node’dan tashqari oldingi node’ga ham reference saqlaydi.
Aniqroq node modeli quyidagicha:
Bu ro‘yxatda oldinga ham, orqaga ham yurish mumkin. Agar o‘chiriladigan node’ga reference allaqachon mavjud
bo‘lsa, uning oldingi node’ini boshidan qidirmasdan previous orqali topish mumkin. Buning evaziga har bir node
yana bitta reference saqlaydi va har bir qo‘shish yoki o‘chirishda ko‘proq bog‘lanishni to‘g‘ri yangilash talab
qilinadi.
Real misol sifatida LRU cacheni ko‘rish mumkin. U yaqinda ishlatilgan elementlarni bir chetga, eng uzoq vaqt
ishlatilmaganini boshqa chetga ko‘chirib turadi. Hash table elementni tez topadi, doubly linked list esa topilgan
node’ni ro‘yxat o‘rtasidan uzib, boshiga O(1) vaqtda ulash imkonini beradi. Eng eski element tail tomondan tez
chiqariladi. Bu yerda faqat linked listning o‘zi kalit bo‘yicha tez qidirishni ta’minlamaydi; O(1) kutiladigan
qidirish hash table bilan birgalikda hosil bo‘ladi.
Brauzer tarixini soddalashtirilgan modelda ikki tomonlama ro‘yxat bilan tasvirlash ham mumkin: previous orqaga,
next oldinga o‘tishni bildiradi. Amaliy brauzerlar tarix, tab va navigatsiyani bundan murakkabroq tuzilmalar
bilan boshqarishi mumkin, lekin ikki yo‘nalishli bog‘lanish g‘oyasini bu misol yaxshi ko‘rsatadi.
Circular linked list
Circular linked list, ya’ni halqasimon bog‘langan ro‘yxatda oxirgi node ro‘yxat tugaganini bildiruvchi NULL
o‘rniga yana boshidagi node’ga ulanadi.
Circular ro‘yxat singly yoki doubly bo‘lishi mumkin. Doubly circular ko‘rinishda head.previous tail’ga,
tail.next esa head’ga ulanadi:
Bu model tabiiy oxiri bo‘lmagan navbatlar uchun qulay. Masalan, round-robin rejalashtirishda vazifalar navbat bilan oz miqdorda protsessor vaqti oladi. Oxirgi vazifadan keyin navbat yana birinchi vazifaga o‘tadi. Ko‘p o‘yinchili stol o‘yinida ham yurish oxirgi o‘yinchidan keyin birinchi o‘yinchiga qaytadi.
Circular tuzilmaning asosiy xavfi — traversal uchun NULLga yetishni to‘xtash sharti qilib bo‘lmaydi. Boshlagan
node’ga qaytilgani tekshirilmasa, sikl cheksiz davom etadi.
Sentinel node ishlatiladigan ro‘yxat
Ba’zi implementatsiyalar haqiqiy foydalanuvchi qiymatini saqlamaydigan maxsus sentinel yoki dummy node’dan foydalanadi. U ro‘yxat boshi yoki oxiridagi alohida holatlarni kamaytiradi:
Masalan, birinchi haqiqiy node’ni o‘chirish ham o‘rtadagi node’ni o‘chirishga o‘xshab qoladi, chunki undan oldin sentinel mavjud. Koddagi shartlar kamayadi, ammo bitta qo‘shimcha node saqlanadi va sentinelni haqiqiy ma’lumot bilan adashtirmaslik kerak. Sentinel alohida asosiy turdan ko‘ra implementatsiya usulidir.
Asosiy amallar
Quyidagi pseudocode avvalo singly linked listni ko‘rsatadi. Unda head, ixtiyoriy tail va length mavjud deb
faraz qilamiz. Implementatsiya bu maydonlarning ayrimlarini saqlamasa, ayrim amallarning narxi o‘zgaradi.
Bo‘sh ro‘yxat yaratish
Bo‘sh linked listda birinchi node yo‘q:
Bu amal O(1) vaqt va O(1) xotira talab qiladi, chunki elementlar sonidan qat’i nazar faqat bir nechta
boshqaruv qiymati yaratiladi.
Traversal
Traversal ro‘yxat boshidan boshlanib, next reference’lari bo‘ylab oxirigacha yuradi:
FUNCTION TRAVERSE(head)
current = head
WHILE current != NULL
VISIT current.value
current = current.next
Har bir node bir marta ko‘riladi. n ta node bo‘lsa, vaqt murakkabligi O(n). current uchun bitta qo‘shimcha
reference yetarli bo‘lgani sababli iterativ traversalning qo‘shimcha xotira murakkabligi O(1).
Circular ro‘yxatda algoritm boshqacha to‘xtashi kerak:
FUNCTION TRAVERSE_CIRCULAR(head)
IF head = NULL
RETURN
current = head
REPEAT
VISIT current.value
current = current.next
UNTIL current = head
Avval bo‘sh ro‘yxat tekshiriladi. Aks holda mavjud bo‘lmagan node’ga murojaat qilinadi.
Index bo‘yicha elementga kirish
Arrayda index orqali manzil hisoblanadi. Linked listda esa index faqat nechta qadam yurish kerakligini bildiradi:
FUNCTION GET(head, index, length)
IF index < 0 OR index >= length
RETURN XATO
current = head
position = 0
WHILE position < index
current = current.next
position = position + 1
RETURN current.value
0-index uchun yurish talab qilinmaydi va eng yaxshi holat O(1). Oxirgi index uchun deyarli barcha node’lardan
o‘tiladi, shuning uchun eng yomon holat O(n). Umumiy holda i-indexga kirish O(i), Big O jadvalida esa odatda
O(n) deb ko‘rsatiladi. Qo‘shimcha xotira O(1).
Doubly linked listda tail mavjud bo‘lsa, index ro‘yxatning ikkinchi yarmida ekaniga qarab orqadan yurish mumkin.
Bu qadamlar sonini kamaytiradi, lekin eng yomon holat baribir O(n) bo‘lib qoladi.
Qiymatni qidirish
Saralanmagan linked listda qidiruv boshidan ketma-ket amalga oshiriladi:
FUNCTION FIND(head, target)
current = head
WHILE current != NULL
IF current.value = target
RETURN current
current = current.next
RETURN TOPILMADI
Birinchi node mos kelsa eng yaxshi holat O(1). Qiymat oxirida bo‘lsa yoki umuman mavjud bo‘lmasa, barcha n ta
node tekshiriladi — O(n). Faqat bitta current reference’i ishlatilgani uchun qo‘shimcha xotira O(1).
Ro‘yxat saralangan bo‘lsa, targetdan katta qiymatga yetganda qidiruvni erta to‘xtatish mumkin. Ammo o‘rtadagi node’ga bevosita kira olmaganimiz sababli oddiy linked listda arraydagidek samarali binary search qilib bo‘lmaydi. O‘rtani topishning o‘zi yurishni talab qiladi.
Boshiga element qo‘shish
Yangi node avval eski head’ga ulanadi, keyin head yangi node’ga o‘tkaziladi:
FUNCTION PREPEND(list, value)
new_node = NEW NODE(value)
new_node.next = list.head
list.head = new_node
IF list.tail = NULL
list.tail = new_node
list.length = list.length + 1
Ro‘yxat qanchalik katta bo‘lmasin, bir xil miqdordagi reference yangilanadi. Vaqt O(1). Yangi node uchun O(1)
qo‘shimcha xotira ajratiladi. Bo‘sh ro‘yxatda yangi node ham head, ham tail bo‘lishi alohida hisobga olinadi.
Warning
Avval headni yangi node’ga o‘tkazib, eski head reference’ini saqlab qolmaslik zanjirning qolgan qismini
yo‘qotishi mumkin. Bog‘lanishlarni yangilash tartibi muhim.
Oxiriga element qo‘shish
tail saqlansa, oxirgi node’ni qidirmasdan yangi node ulash mumkin:
FUNCTION APPEND(list, value)
new_node = NEW NODE(value)
IF list.head = NULL
list.head = new_node
list.tail = new_node
ELSE
list.tail.next = new_node
list.tail = new_node
list.length = list.length + 1
tail bilan vaqt O(1). tail saqlanmasa, avval oxirgi node’ni topish uchun boshidan yurish kerak va vaqt
O(n) bo‘ladi. Demak, “linked list oxiriga qo‘shish har doim O(1)” degan gap to‘liq emas; bu implementatsiya
qaysi boshqaruv reference’larini saqlashiga bog‘liq.
Ma’lum node’dan keyin qo‘shish
Oldingi node’ga reference previous_node ko‘rinishida allaqachon berilgan bo‘lsa:
FUNCTION INSERT_AFTER(previous_node, value)
IF previous_node = NULL
RETURN XATO
new_node = NEW NODE(value)
new_node.next = previous_node.next
previous_node.next = new_node
Bog‘lanishlarni yangilash O(1). Ammo “qiymati 10 bo‘lgan node’dan keyin qo‘sh” deyilsa va o‘sha node’ga
reference tayyor bo‘lmasa, uni topish O(n) vaqt olishi mumkin. Joyni topish narxi bilan topilgan joyga
ulash narxini alohida hisoblash kerak.
Index bo‘yicha qo‘shish
FUNCTION INSERT_AT(list, index, value)
IF index < 0 OR index > list.length
RETURN XATO
IF index = 0
PREPEND(list, value)
RETURN
previous = NODE_AT(list.head, index - 1)
new_node = NEW NODE(value)
new_node.next = previous.next
previous.next = new_node
IF new_node.next = NULL
list.tail = new_node
list.length = list.length + 1
Birinchi joyga qo‘shish O(1). Umumiy index uchun oldingi node’ni topish kerak, shu sababli eng yomon vaqt
O(n). Ulanishning o‘zi esa O(1). Yangi node O(1) qo‘shimcha xotira oladi.
Boshidan element o‘chirish
FUNCTION REMOVE_FIRST(list)
IF list.head = NULL
RETURN XATO
removed = list.head
list.head = list.head.next
IF list.head = NULL
list.tail = NULL
list.length = list.length - 1
RETURN removed.value
Faqat head o‘zgartiriladi, shuning uchun vaqt O(1), yordamchi xotira O(1). Bitta elementli ro‘yxatda
o‘chirishdan keyin head ham, tail ham NULL bo‘lishi kerak.
Ma’lum node’dan keyingi elementni o‘chirish
FUNCTION REMOVE_AFTER(list, previous_node)
IF previous_node = NULL OR previous_node.next = NULL
RETURN XATO
removed = previous_node.next
previous_node.next = removed.next
IF removed = list.tail
list.tail = previous_node
list.length = list.length - 1
RETURN removed.value
Oldingi node tayyor bo‘lsa, bu amal O(1). Singly linked listda faqat o‘chiriladigan node’ning o‘ziga reference
berilishi har doim yetarli emas: uning oldingisidagi nextni o‘zgartirish kerak. Doubly linked listda
removed.previous mavjud bo‘lgani uchun, head va tail holatlari to‘g‘ri boshqarilsa, berilgan node’ni O(1)
vaqtda uzish mumkin.
Oxiridan element o‘chirish
Singly linked listda tail’ni bilishning o‘zi yetmaydi. Yangi tail bo‘ladigan oldingi node’ni topish uchun boshidan yurish kerak:
FUNCTION REMOVE_LAST_SINGLY(list)
IF list.head = NULL
RETURN XATO
IF list.head.next = NULL
value = list.head.value
list.head = NULL
list.tail = NULL
list.length = 0
RETURN value
current = list.head
WHILE current.next != list.tail
current = current.next
value = list.tail.value
current.next = NULL
list.tail = current
list.length = list.length - 1
RETURN value
Vaqt O(n), qo‘shimcha xotira O(1). Doubly linked listda esa tail.previous yangi tail’ni darhol beradi,
shuning uchun oxiridan o‘chirish O(1) bo‘lishi mumkin. Yana bir reference saqlash aynan shunday amallarni
tezlashtirish evaziga keladi.
Qiymatni yangilash
Node’ga reference tayyor bo‘lsa, uning qiymatini almashtirish O(1):
Lekin index yoki eski qiymat orqali yangilash so‘ralsa, avval kerakli node qidiriladi. Bu qidiruv odatda O(n),
almashtirish esa O(1). Node qiymatini yangilash uning next yoki previous bog‘lanishini o‘zgartirmaydi.
Bosqichma-bosqich misol
Boshlanishida quyidagi singly linked list bor:
25 dan keyin 30 qo‘shamiz. previous reference’i 25 node’ni ko‘rsatib turibdi deb faraz qilamiz.
30qiymatli yangi node yaratiladi.- Yangi node’ning
nextqiymati25.next, ya’ni40node’ga o‘rnatiladi. 25.nextyangi node’ga o‘rnatiladi.
1-qadam:
head → [12] → [25] → [40] → NULL [30] → NULL
2-qadam:
head → [12] → [25] → [40] → NULL
↑
[30]
3-qadam:
head → [12] → [25] → [30] → [40] → NULL
Ikkinchi va uchinchi qadam teskari bajarilsa, 40 ga olib boruvchi yagona bog‘lanish yo‘qolishi mumkin. Avval
yangi node’ni zanjirning qolgan qismiga ulash, keyin oldingi node’ni yangi node’ga yo‘naltirish xavfsizroq.
Endi 25 ni o‘chirish uchun undan oldingi 12 node’i ma’lum deb olamiz:
Oldin: head → [12] → [25] → [30] → [40] → NULL
12.next = 25.next
Keyin: head → [12] ───────→ [30] → [40] → NULL
[25] zanjirdan chiqdi
Qiymatlar nusxalanmadi va 30, 40 node’lari xotirada ko‘chirilmadi. Faqat 12.next reference’i o‘zgardi.
Linked listning qo‘shish va o‘chirishdagi asosiy foydasi shu.
Vaqt va xotira murakkabligi
Big O real sekundni emas, ma’lumot hajmi oshganda ish miqdori qanday o‘sishini bildiradi. O(1) amal mutlaqo
vaqt olmaydi degani emas; ro‘yxat 10 ta yoki millionta node’dan iborat bo‘lsa ham, bajariladigan bog‘lanish
yangilanishlari soni o‘zgarmaydi. O(n) esa eng yomon holatda node’lar soniga mutanosib yurish kerakligini
bildiradi.
Quyidagi jadval klassik implementatsiyalar uchun umumiy ko‘rinishni beradi:
| Amal | Singly linked list | Doubly linked list | Nega? |
|---|---|---|---|
| Boshidagi elementni olish | O(1) |
O(1) |
head bevosita birinchi node’ni ko‘rsatadi |
Oxiridagi elementni olish (tail bilan) |
O(1) |
O(1) |
tail bevosita oxirgi node’ni ko‘rsatadi |
| Index bo‘yicha kirish | O(n) |
O(n) |
Bog‘lanishlar bo‘ylab yurish kerak |
| Qiymat bo‘yicha qidirish | O(n) |
O(n) |
Eng yomon holatda barcha node tekshiriladi |
| Boshiga qo‘shish | O(1) |
O(1) |
Faqat chetdagi reference’lar yangilanadi |
Oxiriga qo‘shish (tail bilan) |
O(1) |
O(1) |
Oxirgi node’ni qidirish shart emas |
| Ma’lum node’dan keyin qo‘shish | O(1) |
O(1) |
Joy allaqachon topilgan |
| Index bo‘yicha qo‘shish | O(n) |
O(n) |
Avval kerakli joyga yetib boriladi |
| Boshidan o‘chirish | O(1) |
O(1) |
head yangilanadi |
| Oxiridan o‘chirish | O(n) |
O(1) |
Singly turda oldingi node qidiriladi; doubly turda previous bor |
| Ma’lum node’ni o‘chirish | Oldingi node berilsa O(1) |
O(1) |
Zarur qo‘shnilar tayyor bo‘lsa, faqat bog‘lanishlar o‘zgaradi |
| To‘liq traversal | O(n) |
O(n) |
Har bir node bir marta ko‘riladi |
Jadvaldagi O(1) qo‘shish va o‘chirish natijasi kerakli node’ga reference oldindan mavjud bo‘lganda to‘g‘ri.
Joyni qiymat yoki index orqali avval topish kerak bo‘lsa, umumiy amal O(n) bo‘ladi.
Tuzilmaning xotira murakkabligi
n ta qiymat uchun linked listning umumiy xotirasi O(n). Singly turda har bir node qiymat va bitta next,
doubly turda esa qiymat, next va previous saqlaydi:
Big O ikkalasini ham O(n) deydi, ammo doubly listning doimiy xarajati kattaroq. Bundan tashqari, node obyektining
xizmat ma’lumotlari va xotira ajratish tizimining xarajatlari ham bo‘lishi mumkin. Aniq bayt miqdori til,
platforma va implementatsiyaga bog‘liq.
Iterativ amallar odatda O(1) yordamchi xotira ishlatadi. Rekursiv traversal yozilsa, har bir chaqiruv call stackda
joy egallashi sababli O(n) yordamchi xotira talab qilishi va juda uzun ro‘yxatda stack chegarasidan oshishi
mumkin.
Linked list bilan keng tarqalgan algoritmik yondashuvlar
Bu yondashuvlar bog‘langan ro‘yxat masalalarida tez-tez uchraydi. Ularning to‘liq masalalari alohida mashq mavzusi bo‘lsa-da, asosiy g‘oyani tushunish linked listning tabiatini ochib beradi.
Ikki ko‘rsatkich
Bir ro‘yxat bo‘ylab ikki reference turli tezlikda yoki turli masofada yurishi mumkin. Ko‘pincha ular slow va
fast deb ataladi:
slow = head
fast = head
WHILE fast != NULL AND fast.next != NULL
slow = slow.next
fast = fast.next.next
slow bir qadam, fast ikki qadam yuradi. fast oxirga yetganda slow taxminan o‘rtada bo‘ladi. Shu usul bilan
uzunlikni oldindan bilmasdan o‘rta node’ni topish mumkin. Halqa mavjud bo‘lsa, tez va sekin ko‘rsatkichlar ma’lum
sharoitda uchrashadi; bu cycle detection algoritmlarining asosiy g‘oyasidir.
Oraliqni saqlaydigan ikki ko‘rsatkich
Oxiridan k-node’ni topish uchun birinchi reference avval k qadam oldinga yuradi. Keyin ikkala reference bir
vaqtda siljiydi. Birinchisi oxirga yetganda ikkinchisi kerakli masofada turadi. Bu usul ro‘yxat uzunligini alohida
hisoblamasdan bitta traversal bilan ishlaydi.
Bog‘lanishlarni teskari aylantirish
Singly linked listni teskari qilishda node qiymatlari ko‘chirilmaydi; next yo‘nalishlari almashtiriladi:
Bunda previous, current va next_node kabi reference’lar kerak bo‘ladi. current.next o‘zgartirilishidan oldin
eski keyingi node saqlanmasa, zanjirning qolgan qismi yo‘qoladi. Har bir node bir marta ko‘rilgani uchun vaqt
O(n), iterativ variantda yordamchi xotira O(1).
Array va linked list taqqoslanishi
Ikki tuzilma ham tartibli elementlar ketma-ketligini ifodalaydi, ammo tartibni turli yo‘l bilan saqlaydi.
| Xususiyat | Array | Linked list |
|---|---|---|
| Xotirada joylashish | Odatda uzluksiz | Node’lar tarqoq bo‘lishi mumkin |
| Index bo‘yicha kirish | O(1) |
O(n) |
| O‘rtadagi joy topilganidan keyin qo‘shish | Elementlarni surish sabab O(n) |
Bog‘lanishni almashtirish O(1) |
| O‘rtadagi joy topilganidan keyin o‘chirish | Bo‘shliqni yopish sabab O(n) |
Zarur qo‘shnilar ma’lum bo‘lsa O(1) |
| Qiymat bo‘yicha oddiy qidirish | O(n) |
O(n) |
| Har element uchun qo‘shimcha xotira | Odatda reference shart emas | Bir yoki ikki reference kerak |
| CPU cache bilan ishlash | Odatda samaraliroq | Tarqoq node’lar sabab ko‘pincha sustroq |
| O‘lchamni o‘zgartirish | Fixed arrayda qiyin; dynamic array ko‘chishi mumkin | Node’larni alohida qo‘shish mumkin |
Linked list o‘rtasiga qo‘shishning O(1) bo‘lishi ko‘pincha noto‘g‘ri talqin qilinadi. Agar “5000-indexga yangi
element qo‘sh” deyilsa, oldingi node’ga yetish O(n). Arrayda esa joy darhol topiladi, ammo keyingi elementlarni
surish O(n). Ikkalasining umumiy murakkabligi bir xil ko‘rinsa ham, xarajatning sababi boshqa.
Amaliy dasturda dynamic array ko‘pincha boshlang‘ich tanlov bo‘ladi: index orqali tez kiradi, cache bilan yaxshi ishlaydi va xotira xarajati kam. Linked list esa node’ga reference’lar uzoq vaqt saqlanadigan hamda ro‘yxat ichida tez-tez uzish-ulash haqiqatan kerak bo‘ladigan vazifalarda foydali.
Real qo‘llanishlar
LRU cache
LRU cache sig‘imi to‘lganda eng uzoq vaqt ishlatilmagan yozuvni chiqaradi. Doubly linked list foydalanish tartibini saqlaydi, hash table esa kalitdan node’ni tez topadi. Yozuv o‘qilganda uning node’i ro‘yxat boshiga ko‘chiriladi; sig‘im to‘lganda tail chiqariladi. Aynan ikki tomonlama bog‘lanish node’ni oldingi va keyingi qo‘shnilaridan tez uzishga imkon beradi.
Matn muharriridagi ketma-ket bo‘laklar
Katta matnni bitta uzluksiz arrayda saqlashda o‘rtaga kiritish ko‘p belgini ko‘chirishga olib kelishi mumkin. Ayrim muharrir tuzilmalari matnni bo‘laklarga ajratib, bo‘laklar orasidagi bog‘lanishlarni boshqaradi. Oddiy linked list bu vazifaning barcha talabini hal qilmaydi — amalda piece table, rope yoki muvozanatlangan daraxt kabi tuzilmalar ishlatilishi mumkin — ammo “ma’lumotni ko‘chirish o‘rniga bo‘laklarni qayta ulash” g‘oyasi linked list modeliga yaqin.
Round-robin navbat
Vazifalar yoki o‘yinchilar navbati doim takrorlansa, circular linked list oxiridan boshiga tabiiy o‘tishni beradi.
Joriy node’dan nextga o‘tish O(1). Vazifa navbatdan chiqsa, uning qo‘shni bog‘lanishlari yangilanadi. Amaliy
rejalashtiruvchi tizimlarda ustuvorlik, kutish va parallel bajarilish sabab tuzilma murakkabroq bo‘lishi mumkin.
Undo/redo va navigatsiya
Holatlar orasida oldinga va orqaga yurish kerak bo‘lsa, doubly linked list mantiqiy model bo‘la oladi. Joriy
holatning previous bog‘lanishi undo’ga, next bog‘lanishi redo’ga olib boradi. Foydalanuvchi undo’dan keyin yangi
amal bajarsa, oldingi redo tarmog‘i odatda bekor qilinadi. Oddiy chiziqli tarix uchun bu model mos, tarmoqlanadigan
tarix uchun esa tree yoki graph kerak bo‘lishi mumkin.
Hash collision zanjirlari
Alohida zanjirlash (separate chaining) usulidagi hash table bir bucketga tushgan bir nechta yozuvni zanjirda
saqlashi mumkin. Bucket zanjir boshini ko‘rsatadi, keyin mos kalit topilguncha node’lar tekshiriladi. Unumdorlik
zanjirlar qisqa bo‘lishiga bog‘liq; juda ko‘p collision yuz bersa qidirish O(n)gacha yomonlashadi.
Afzalliklari
- O‘lcham bosqichma-bosqich o‘sadi. Har yangi element uchun node ajratiladi; oldindan katta uzluksiz hudud topish shart emas.
- Topilgan joyda uzish va ulash tez. Zarur node reference’lari tayyor bo‘lsa, elementlarni ko‘chirmasdan bir nechta bog‘lanish yangilanadi.
- Node reference’i barqaror bo‘lishi mumkin. Boshqa node’lar qo‘shilganda dynamic arraydagi qayta joylashishga o‘xshab barcha node’lar bitta yangi blokka ko‘chirilmaydi. Aniq barqarorlik xotira boshqaruvi va til modeliga bog‘liq.
- Turli bog‘lanish modellari mavjud. Faqat oldinga, ikki tomonga yoki halqa bo‘ylab yurish vazifaga mos tanlanadi.
Bu afzalliklarning har biri xarajatga ega: alohida xotira ajratish, reference’lar uchun joy, murakkabroq bog‘lanish invariantlari va sustroq cache ishlashi.
Cheklovlari
- Index orqali ixtiyoriy elementga bevosita kirib bo‘lmaydi.
- Oddiy qidirish va traversal
O(n)vaqt oladi. - Har node qiymatdan tashqari bir yoki ikki reference saqlaydi.
- Node’larning tarqoq joylashuvi CPU cache’dan foydalanishni yomonlashtirishi mumkin.
- Ko‘p mayda xotira ajratish va bo‘shatish qo‘shimcha xarajat hamda fragmentatsiyaga sabab bo‘lishi mumkin.
- Bog‘lanishlarni noto‘g‘ri yangilash ma’lumotning bir qismini yo‘qotishi yoki cheksiz sikl yaratishi mumkin.
- Binary search uchun mos emas, chunki o‘rtadagi elementga
O(1)vaqtda kirish yo‘q.
Keng tarqalgan xatolar va nozik holatlar
Bo‘sh ro‘yxatni hisobga olmaslik
head = NULL bo‘lganda head.next yoki head.valuega murojaat qilib bo‘lmaydi. Traversal, o‘chirish va qidirish
oldidan bo‘shlik tekshiriladi. Birinchi element qo‘shilganda head va tail bir node’ni ko‘rsatishi mumkin.
Bitta elementli ro‘yxat
Yagona node o‘chirilsa, ro‘yxat yana bo‘sh holatga qaytadi. Faqat headni tozalab, tailni eski node’da qoldirish
ichki holatni buzadi. Circular ro‘yxatda esa bitta node’ning nexti ko‘pincha o‘zini ko‘rsatadi.
Bog‘lanishni juda erta almashtirish
current.next o‘zgartirilishidan oldin eski keyingi node kerak bo‘lsa, u vaqtinchalik reference’da saqlanishi
shart. Aks holda ro‘yxatning qolgan qismiga yo‘l yo‘qoladi. Har bir o‘zgarishda “qaysi reference hali kerak?” degan
savol berish foydali.
Head va tail’ni yangilamaslik
Birinchi yoki oxirgi node qo‘shilganda va o‘chirilganda ro‘yxatning tashqi reference’lari ham yangilanadi. O‘rtadagi node uchun to‘g‘ri ishlaydigan kod chekka node’larda xato berishi mumkin.
Doubly listda faqat bir tomonni yangilash
A ⇄ B ⇄ C ichidan B o‘chirilganda A.next = Cning o‘zi yetarli emas; C.previous = A ham bajarilishi kerak.
Aks holda oldinga yurish to‘g‘ri ko‘rinsa-da, orqaga yurish o‘chirilgan node’ga kiradi. Doubly linked listning
invarianti shuki, qo‘shni node’lar ikki yo‘nalishda bir-biriga mos kelishi kerak.
Circular ro‘yxatda noto‘g‘ri to‘xtash sharti
Circular ro‘yxatda next tabiiy ravishda NULL bo‘lmasligi mumkin. Traversal boshlang‘ich node’ga qaytilganda yoki
oldindan belgilangan qadamlar soniga yetganda to‘xtashi kerak. Bo‘sh ro‘yxat alohida tekshirilmasa, boshlang‘ich
node’ning o‘zi mavjud bo‘lmaydi.
Qiymat va node’ni adashtirish
Takroriy qiymatlar bo‘lishi mumkin:
“10 ni o‘chir” talabi birinchi mos node’nimi, barcha mos node’larnimi yoki aniq bir node’nimi — algoritm buni
belgilashi kerak. Bir xil qiymatli node’lar xotirada alohida obyektlardir.
Tashqi reference’larning ta’siri
Node ro‘yxatdan uzilganidan keyin unga tashqi reference qolishi mumkin. Bu node hali mavjud bo‘lsa ham, endi ro‘yxatning a’zosi emas. Aksincha, node’ga yetib boruvchi yagona bog‘lanish tasodifan yo‘qotilsa, uning ortidagi butun zanjir ro‘yxatdan uzilishi mumkin.
Rekursiyada stack chegarasi
Rekursiv traversal qisqa ko‘rinadi, ammo har node uchun yangi funksiya chaqiruvi hosil qiladi. Juda uzun ro‘yxat
call stackni to‘ldirishi mumkin. Oddiy traversal uchun iterativ yondashuv odatda xavfsizroq va O(1) yordamchi
xotira ishlatadi.
Qachon ishlatish kerak?
Linked list quyidagi talablar ustun bo‘lganda mos kelishi mumkin:
- kerakli node’ga reference allaqachon mavjud va uning atrofida tez-tez qo‘shish yoki o‘chirish bajariladi;
- elementlar soni o‘zgaradi va ularni bitta uzluksiz xotira hududida saqlash talab qilinmaydi;
- ketma-ket faqat oldinga yoki ikki tomonga yurish yetarli;
- node’larni boshqa tuzilmalar, masalan, hash table bilan birga barqaror tartibda ulash kerak;
- circular navbat kabi oxiridan boshiga tabiiy qaytadigan model kerak.
Tanlovni tuzilmaning nomi emas, eng ko‘p bajariladigan operatsiya belgilaydi. “Tez-tez o‘chirish bor” degan jumla ham yetarli emas: o‘chiriladigan node qanday topilishi va oldingi node ma’lummi, shuni aniqlash kerak.
Qachon ishlatmaslik kerak?
Quyidagi vaziyatlarda boshqa tuzilma ko‘pincha yaxshiroq:
- Index bo‘yicha tez-tez kirish kerak bo‘lsa, array yoki dynamic array mosroq. Linked list har safar boshidan yuradi.
- Asosiy ish ketma-ket o‘qish bo‘lsa, array cache samaradorligi va kamroq xotira xarajati sababli odatda ustun.
- Kalit bo‘yicha tez qidirish kerak bo‘lsa, hash table; tartibli qidirish va diapazonlar kerak bo‘lsa, muvozanatlangan qidiruv daraxti ma’qul bo‘lishi mumkin.
- Faqat oxiriga qo‘shish va index orqali o‘qish kerak bo‘lsa, amortized
O(1)append beradigan dynamic array odatda soddaroq va amalda tezroq. Amortized degani ayrim kengayishlar qimmat bo‘lsa ham, ko‘p qo‘shishlarning o‘rtacha taqsimlangan xarajati doimiy bo‘lishidir. - Xotira juda cheklangan bo‘lsa, har bir node uchun reference va ajratish xarajati qimmat tushishi mumkin.
- Binary search kerak bo‘lsa, saralangan array mosroq, chunki o‘rtadagi elementga tez kiradi.
- Parallel ishlov berish ko‘p bo‘lsa, pointer bog‘lanishlarini xavfsiz o‘zgartirish murakkablashadi. Maxsus concurrent tuzilma yoki boshqa model talab qilinishi mumkin.
Stack va queue abstraksiyalarini linked list bilan qurish mumkin, lekin bu ularning yagona implementatsiyasi emas. Dynamic array yoki circular buffer amaliy talabga qarab yaxshiroq cache samaradorligi berishi mumkin.
Eng muhim xulosa
Linked list elementlarni xotirada yonma-yon joylashtirish orqali emas, node’lar orasidagi reference’lar orqali
tartiblaydi. Shu sababli kerakli joy allaqachon topilgan bo‘lsa, node’ni qo‘shish yoki o‘chirish bir nechta
bog‘lanishni almashtirish bilan O(1) vaqtda bajariladi. Ammo index bo‘yicha kirish va qidirish odatda O(n), har
bir node esa qo‘shimcha reference xotirasini talab qiladi.
Singly linked list kamroq xotira bilan faqat oldinga yuradi. Doubly linked list ikki yo‘nalishda yurish va ma’lum node’ni tez uzishni qo‘shimcha reference evaziga beradi. Circular linked list esa oxirgi node’ni yana boshiga ulab, takrorlanuvchi navbatlarni tabiiy ifodalaydi.
Linked listni tanlashdan oldin uchta savol bering:
- Kerakli node qanday topiladi?
- Eng ko‘p bajariladigan amal index bo‘yicha kirishmi yoki topilgan joyda uzish-ulashmi?
- Qo‘shimcha reference va sustroq cache ishlashi bu foydaga arziydimi?
Linked list node va reference’lar orqali chiziqli ketma-ketlik quradi. Keyingi tabiiy qadam — shu ketma-ketlikka amal bajarish qoidalarini qo‘shadigan stack va queue kabi abstrakt tuzilmalarni o‘rganishdir: ularda elementlarning qanday bog‘langani emas, qaysi tartibda qo‘shilib, qaysi tartibda olinishi markaziy masalaga aylanadi.