Sparround

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 + b addım atıb
  • Sürətli pointer iki dəfə çox atıb və üstəlik dövrəni k də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 olunurYaddaş
Linked list-də cycle varmı?slow 1 addım, fast 2 addım; görüşürlərsə cycle varO(1)
Cycle harada başlayır?Görüşdən sonra birini head-ə qaytar, ikisini də 1 addımla aparO(1)
Siyahının ortası (bir keçiddə)fast sona çatanda slow ortadadırO(1)
Sondan k-cı elementBir 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 deyilO(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ırO(1) — massivi dəyişmədən
Linked list palindromdurmu?Ortanı tap, ikinci yarını çevir, müqayisə etO(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.