Teskari qilish
Bir qatorga tizilgan odamlarni tasavvur qiling: hammasi orqasini o‘girib, yuzlarini qarama-qarshi tomonga burishi kerak. Buning uchun odamlarning o‘zini ko‘chirish shart emas — ular joyida turgan holda faqat kimga qarab turishlarini o‘zgartirish kifoya. Linked listni teskari qilish ham aynan shu mantiqqa asoslanadi: node’lar xotirada o‘z joyida qoladi, faqat ularning bir-biriga bog‘lanish yo‘nalishi o‘zgaradi.
Masala sharti
Bosh (head) reference’i berilgan singly linked list mavjud. Ro‘yxatni shunday o‘zgartirish kerakki, oxirgi
element yangi bosh, birinchi element esa yangi oxir bo‘lsin — va butun ketma-ketlik teskari tartibda o‘qilsin.
Bo‘sh ro‘yxatni yoki bitta elementli ro‘yxatni teskari qilish natijasi o‘zgarishsiz qoladi.
Nega array’dagidek oddiy emas?
Arrayni teskari qilish uchun ikki chetdagi elementlarni index orqali almashtirish yetarli, chunki istalgan indexga
O(1) vaqtda kirish mumkin. Linked listda esa oxirgi elementga index orqali bevosita kirib bo‘lmaydi — unga
yetish uchun boshidan boshlab yurish kerak. Bundan tashqari, elementlarning qiymatini joy-joyida almashtirish
o‘rniga, bog‘lanishning o‘zini — next reference’ini — teskari yo‘naltirish kerak bo‘ladi.
Optimal yechim: uchta ko‘rsatkich bilan iterativ teskari aylantirish
G‘oya: ro‘yxat bo‘ylab bir marta yurib, har bir node’ning nextini oldingi node’ga yo‘naltiramiz. Buning uchun
uchta reference kerak bo‘ladi:
- previous — hozirgacha teskari aylantirilgan qismning boshi (dastlab
NULL, chunki hali hech narsa teskari aylantirilmagan); - current — hozir ishlov berilayotgan node;
- next_node —
current.nextni o‘zgartirishdan oldin uni vaqtincha saqlab turadigan reference.
FUNCTION REVERSE(head)
previous = NULL
current = head
WHILE current != NULL
next_node = current.next // eski bog'lanishni yo'qotmaslik uchun saqlab qo'yamiz
current.next = previous // bog'lanishni teskari yo'naltiramiz
previous = current // "teskari aylantirilgan qism" bir node oldinga siljiydi
current = next_node // asl ro'yxat bo'ylab bir node oldinga siljiymiz
RETURN previous // sikl tugaganda previous - yangi head
next_node reference’i shart, chunki current.next = previous bajarilgan zahoti currentdan asl ro‘yxatdagi
keyingi node’ga olib boruvchi yagona yo‘l yo‘qoladi. Agar bu qadamdan oldin next_nodeda saqlab qo‘yilmasa,
zanjirning qolgan qismi butunlay yo‘qoladi.
Bosqichma-bosqich misol
Kirish: 1 → 2 → 3 → NULL.
Boshlanish:
previous = NULL
current = [1]
[1] → [2] → [3] → NULL
1-qadam (current = 1):
next_node = 2
1.next = previous (NULL) → NULL ← [1] [2] → [3] → NULL
previous = 1
current = 2
2-qadam (current = 2):
next_node = 3
2.next = previous (1) → NULL ← [1] ← [2] [3] → NULL
previous = 2
current = 3
3-qadam (current = 3):
next_node = NULL
3.next = previous (2) → NULL ← [1] ← [2] ← [3]
previous = 3
current = NULL → sikl to'xtaydi
Natija: previous = [3], demak yangi head = 3
Yakuniy ko'rinish: 3 → 2 → 1 → NULL
Har bir qadamda faqat bitta next reference’i o‘zgaradi. Qiymatlarning o‘zi — 1, 2, 3 — hech qayerga
ko‘chirilmadi; ular xotirada o‘z joyida qoldi, o‘zgargani faqat ular orasidagi yo‘nalish.
Nega bu ishlaydi?
Sikl invarianti quyidagicha ta’riflanadi: har bir iteratsiya boshlanishida previous orqali erishiladigan
zanjir — asl ro‘yxatning currentgacha bo‘lgan qismining teskari holatidir. Boshida bu zanjir bo‘sh
(previous = NULL), chunki hali hech narsa teskari aylantirilmagan. Har qadamda bitta node shu teskari
zanjirga qo‘shiladi, va current asl ro‘yxat bo‘ylab bir qadam oldinga siljiydi. current NULLga yetganda,
butun ro‘yxat teskari zanjirga qo‘shilgan bo‘ladi — shu sababli previous yangi head bo‘ladi.
Vaqt va xotira murakkabligi
Har bir node aynan bir marta ko‘riladi va unda doimiy miqdordagi amal (uchta reference yangilanishi) bajariladi.
Shu sababli vaqt murakkabligi O(n). Yordamchi xotira sifatida faqat uchta reference — previous, current,
next_node — ishlatiladi, ro‘yxat uzunligidan qat’i nazar bu son o‘zgarmaydi, shuning uchun xotira murakkabligi
O(1).
Rekursiv variant
Xuddi shu masalani rekursiv usulda ham yechish mumkin:
FUNCTION REVERSE_RECURSIVE(current, previous)
IF current = NULL
RETURN previous
next_node = current.next
current.next = previous
RETURN REVERSE_RECURSIVE(next_node, current)
Vaqt murakkabligi baribir O(n), chunki har bir node uchun bitta chaqiruv bajariladi. Ammo bu yerda qo‘shimcha
xotira O(1) emas, O(n), chunki har bir rekursiv chaqiruv call stackda joy egallaydi. Juda uzun ro‘yxatda bu
stack chegarasidan oshib ketishi mumkin. Shu sababli amaliyotda iterativ variant odatda afzal ko‘riladi.
Nozik holatlar
- Bo‘sh ro‘yxat.
head = NULLbo‘lsa, sikl birinchi tekshiruvdayoq to‘xtaydi va funksiyaNULLqaytaradi — bu to‘g‘ri natija. - Bitta elementli ro‘yxat. Sikl bir marta bajariladi:
current.nextpreviousga, ya’niNULLga o‘rnatiladi. Natija — o‘zgarishsiz qolgan bitta node. - Doubly linked listni teskari qilish. Bu holatda har bir node’da
nextvapreviousham joy almashishi kerak, bundan tashqari ro‘yxatningheadvatailboshqaruv reference’lari ham almashtiriladi. Bu maqoladagi singly linked list uchun yozilgan algoritm to‘g‘ridan-to‘g‘ri qo‘llanmaydi, ammo asosiy g‘oya — bog‘lanish yo‘nalishini teskari aylantirish — bir xil qoladi.
Keng tarqalgan xatolar
next_nodeni saqlamaslik. Bu eng ko‘p uchraydigan xato. current.next = previous bajarilishidan oldin
current.nextning asl qiymati boshqa joyda saqlanmasa, undan keyingi butun zanjirga yo‘l uziladi va faqat bitta
node qaytariladi.
Yangi head’ni noto‘g‘ri qaytarish. Sikl tugagach current doim NULL bo‘ladi — yangi head emas. Yangi head
har doim previousda saqlanadi, chunki aynan u охирги ishlov berilgan node’ni ko‘rsatib turadi.
Asl headni yo‘qotib qo‘yish. Agar chaqiruvchi kod funksiyadan qaytgan qiymatni saqlamasa yoki eski head
reference’iga tayanishda davom etsa, teskari aylantirilgan ro‘yxatning boshiga endi faqat bitta reference —
funksiyadan qaytgan qiymat — orqali yetish mumkinligini unutib qo‘yishi mumkin. Eski head endi ro‘yxatning
oxirgi elementiga aylanadi, boshiga emas.
Xulosa
Linked listni teskari qilish qiymatlarni ko‘chirishni emas, faqat bog‘lanishlar yo‘nalishini o‘zgartirishni talab
qiladi. Uchta reference — previous, current, next_node — yordamida ro‘yxat bo‘ylab bir marta yurib, har bir
next bog‘lanishini teskari yo‘naltirish bu masalaning markazidagi g‘oya. Xuddi shu “bog‘lanishni almashtirishdan
oldin eskisini saqlab qo‘yish” tamoyili linked list bilan ishlashning ko‘plab boshqa masalalarida, jumladan ikki
ro‘yxatni birlashtirishda ham qayta uchraydi.