Fast/slow pointers və cycle detection
Floyd-un "tısbağa və dovşan" (tortoise and hare) alqoritmi bir fikrə əsaslanır: iki pointer eyni yerdən başlayır, biri bir addım, digəri iki addım atır.
Nəticə iki xassədən ibarətdir:
- Zəncir sonludursa — sürətli pointer sona çatır və heç vaxt yavaşı tutmur
- Cycle varsa — sürətli pointer dövrənin içinə girib "əlavə dövrə vurur" və yavaşı mütləq tutur
Niyə mütləq tutur? Hər ikisi dövrənin içinə düşəndən sonra aralarındakı məsafə hər addımda düz 1 azalır (sürətli 2, yavaş 1 addım atır). Məsafə tam ədəddir və azalır, deməli nə vaxtsa 0 olacaq — yəni onlar üstünə tullanmadan görüşəcəklər. Bu izah interview-da çox dəyərlidir, çünki əzbər deyil, sübutdur.
Mürəkkəblik: vaxt O(n), yaddaş `O(1)` — və bütün pattern-in dəyəri məhz bu O(1)-dədir.
Alternativ — hash set: gəzdiyin node-ları Set-ə yığırsan; artıq gördüyün node-a rast gəlsən, cycle var. Bu da O(n) vaxtdır, amma O(n) yaddaş tələb edir. Əvəzində kodu daha sadədir və dövrənin başlanğıcını birbaşa verir. İnterview-da hər iki həlli demək və seçimi əsaslandırmaq düzgün taktikadır: "Hash set ilə başlayardım, çünki sadədir; yaddaş məhdudiyyəti varsa Floyd-a keçərdim."
Dövrənin başlanğıcını tapmaq — Floyd-un ən "sehrli" görünən hissəsi, amma arxasında sadə hesab durur.
Alqoritm: pointer-lər görüşəndən sonra birini başa (head) qaytar, digərini görüş nöqtəsində saxla və indi hər ikisini bir addımla hərəkət etdir. Onlar məhz dövrənin başlanğıcında görüşəcəklər.
Niyə: başlanğıcdan dövrənin girişinə qədər olan məsafə a, girişdən görüş nöqtəsinə qədər b, görüş nöqtəsindən yenidən girişə qədər c olsun (yəni dövrənin uzunluğu b + c).
- Yavaş pointer
a + baddım atıb - Sürətli pointer iki dəfə çox atıb və üstəlik dövrəni
kdəfə fırlanıb:a + b + k(b + c) = 2(a + b) - Sadələşdirsək:
a = k(b + c) − b = (k−1)(b + c) + c
Bu o deməkdir ki, başdan girişə qədər olan məsafə (`a`), görüş nöqtəsindən girişə qədər olan məsafəyə (`c`) bərabərdir (tam dövrələr fərqi dəyişmir). Ona görə biri başdan, biri görüş nöqtəsindən eyni sürətlə gedəndə girişdə qarşılaşırlar.
Bu çıxarışı interview-da tam yazmaq tələb olunmur — amma "a ilə c bərabər çıxır, ona görə işləyir" demək əzbərçilikdən fərqlənməyin ən qısa yoludur.
Bir keçiddə ortanı tapmaq: eyni texnika, sadəcə cycle olmadan. Sürətli pointer sona çatanda yavaş pointer düz ortadadır. Diqqət: cüt uzunluqda iki "orta" var — yuxarıdakı şablon ikincisini qaytarır. Birincisi lazımdırsa, döngə şərtini fast.next !== null && fast.next.next !== null kimi dəyişirsən. İnterview-da bu detalı soruşmaq ("cüt halda hansı ortanı istəyirsiniz?") diqqətlilik göstərir.
| Məsələ | Necə tətbiq olunur | Yaddaş |
|---|---|---|
| Linked list-də cycle varmı? | slow 1 addım, fast 2 addım; görüşürlərsə cycle var | O(1) |
| Cycle harada başlayır? | Görüşdən sonra birini head-ə qaytar, ikisini də 1 addımla apar | O(1) |
| Siyahının ortası (bir keçiddə) | fast sona çatanda slow ortadadır | O(1) |
| Sondan k-cı element | Bir pointeri əvvəlcə k addım irəli apar, sonra ikisini birlikdə | O(1) |
| Happy number tipli məsələlər | "Növbəti dəyər" funksiyası gizli linked list yaradır; dövrə = xoşbəxt deyil | O(1) |
| Massivdə təkrarlanan ədədi tapmaq (1..n) | `i → a[i]` keçidi qraf yaradır; təkrar dövrənin başlanğıcıdır | O(1) — massivi dəyişmədən |
| Linked list palindromdurmu? | Ortanı tap, ikinci yarını çevir, müqayisə et | O(1) |
Interview məsləhəti: cycle detection sualında hər iki həlli de və seçimi əsaslandır — "Hash set: O(n) vaxt, O(n) yaddaş, kodu 5 sətir. Floyd: eyni vaxt, O(1) yaddaş, amma bir az incə. Yaddaş məhdudiyyəti qeyd olunmayıbsa hash set-lə başlayıb Floyd-u təklif edərdim." Bir həll deyən namizədlə iki həlli müqayisə edən namizəd arasındakı fərq qiymətləndirmədə böyükdür. İkinci məsləhət: fast !== null && fast.next !== null şərtini yazmağı unutma — fast.next.next yoxlamasız null üzərində partlayır və bu, ən çox tutulan səhvdir.