SEN2211 · 4. hafta · Linked List Applications and Review

Bağlı liste uygulamaları ve tekrar

Listeyi ters çevirme, ortayı bulma, döngü tespiti, iki sıralı listeyi birleştirme ve liste ile dizi karşılaştırması. Vizenin “write a method” sorularının çoğu bu dört algoritmadan gelir.

reverse a linked listslow and fast pointersmiddle nodecycle detectionFloyd's algorithmmerge two sorted listsdummy nodetwo-pointer techniquearray vs linked list

Bu hafta yeni yapı yok. İlk üç haftada kurduğun düğüm becerisini, sınavda en sık istenen dört algoritmada kullanıyorsun: listeyi ters çevirmek, ortayı bulmak, döngüyü tespit etmek ve iki sıralı listeyi birleştirmek. Dördü de O(n) sürede ve ekstra dizi kullanmadan çalışır. Sonda liste ile diziyi karşılaştıran bir özet var; vize öncesi tekrar için bu sayfa yeter.

Altın kurallar:

  1. Bir referansı değiştirmeden önce, ihtiyacın olan devamı sakla.
  2. İki referans tekniği (two pointers): biri hızlı biri yavaş, ya da biri önde biri arkada.
  3. Sahte baş düğüm (dummy node) “liste boş mu, head değişiyor mu” kontrollerini yok eder.
  4. Her metodu boş liste ve tek elemanlı listede kafanda dene.

Tekrar: üç haftanın özeti

İşlemTek yönlü (head)Tek yönlü (head+tail)Çift yönlü (head+tail)Dizi
Başa ekleO(1)O(1)O(1)O(n)
Sona ekleO(n)O(1)O(1)O(1)*
Baştan silO(1)O(1)O(1)O(n)
Sondan silO(n)O(n)O(1)O(1)
i. elemanO(n)O(n)O(n)O(1)
AramaO(n)O(n)O(n)O(n), sıralıysa O(log n)

* Dizide yer varsa. Dolarsa yeni diziye kopyalama O(n).

Listeyi ters çevirme (reverse)

Fikir: her düğümün okunu geriye çevir. Ama curr.next’i değiştirdiğin an listenin geri kalanına ulaşamazsın, o yüzden önce sakla.

static Node reverse(Node head) {
    Node prev = null, curr = head;
    while (curr != null) {
        Node next = curr.next;   // 1) devamı sakla
        curr.next = prev;        // 2) oku ters çevir
        prev = curr;             // 3) prev bir ileri
        curr = next;             // 4) curr bir ileri
    }
    return prev;
}

1 2 3 4 üzerinde her turdan sonraki durum (program çıktısı):

step 1: prev-list = 1 | curr = 2
step 2: prev-list = 2 1 | curr = 3
step 3: prev-list = 3 2 1 | curr = 4
step 4: prev-list = 4 3 2 1 | curr = null
4 3 2 1

TUZAK · return head yazmak. Döngü bitince eski head (1) artık listenin son düğümüdür ve next’i null. Yeni baş prev’dir. Çağıran tarafta da head = reverse(head); diye atamayı unutma.

Ortadaki düğüm: yavaş ve hızlı referans

Önce uzunluğu sayıp sonra n/2 adım yürümek de doğru cevaptır (iki geçiş). Tek geçişte yapmak için iki referans kullan: fast iki adım atarken slow bir adım atar. fast sona vardığında slow yolun yarısındadır.

static Node middle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
1..7 -> 4
1..6 -> 4
10 20 30 40 50 -> 30

Çift uzunlukta iki orta vardır (1..6 için 3 ve 4). Bu döngü koşuluyla ikinci orta döner. Soru ilk ortayı isterse koşulu fast.next != null && fast.next.next != null yap.

Döngü tespiti (Floyd)

Hatalı bir kod son düğümün next’ini yanlışlıkla önceki bir düğüme bağlamış olabilir. Bu listeyi yazdırmaya çalışırsan sonsuz döngüye girersin. Döngü olup olmadığını O(1) bellekle anlamak için aynı slow/fast fikri kullanılır: pistte koşan iki kişiden hızlı olan, pist dairesel ise yavaşı mutlaka bir tur bindirir.

static boolean hasCycle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

İz: 1 → 2 → 3 → 4 → 5 ve 5 tekrar 3’ü gösteriyor.

adım   slow   fast
 0      1      1
 1      2      3
 2      3      5
 3      4      4    (5 → 3 → 4)  buluştular → true

Program da aynı sonucu verdi: met after 3 steps, buluşma düğümü 4. 6 düğümlü ve sonu başa bağlanan listede 6 adımda 1’de buluşurlar.

İPUCU · Neden mutlaka yakalar? İkisi de döngüye girdikten sonra her adımda hızlı olan bir düğüm fazla ilerler, yani aralarındaki mesafe 1 azalır. Mesafe 0’a düşünce buluşurlar, atlayıp geçemezler.

İki sıralı listeyi birleştirme (merge)

Merge sort’un kalbi bu işlem (14. hafta). İki listenin başlarını karşılaştır, küçüğü sonuca ekle, o listede bir ilerle.

static Node merge(Node a, Node b) {
    Node dummy = new Node(0), t = dummy;
    while (a != null && b != null) {
        if (a.data <= b.data) { t.next = a; a = a.next; }
        else                  { t.next = b; b = b.next; }
        t = t.next;
    }
    t.next = (a != null) ? a : b;
    return dummy.next;
}
1 2 3 4 7 8 9
2 5 10 10 12 15
3 6

Üçüncü satır merge(null, 3 6): biri boşsa diğeri aynen döner. dummy olmasaydı ilk elemanı ayrıca seçip head yapmak gerekirdi; dummy bu özel durumu ortadan kaldırır. Sonuç dummy.next’tir, dummy’nin kendisi değil.

<= kullanmak eşit değerlerde önce a’dan alır. Bu, sıralama algoritmalarında kararlılık (stability) dediğimiz özelliği korur.

Diğer iki referans kalıpları

Sondan k. eleman. lead’i k adım öne al, sonra ikisini birlikte yürüt. lead null olduğunda trail sondan k. düğümdedir. 10 20 30 40 50 60 ve k = 2 için 50.

Sıralı listede tekrarları silme. Eşit komşuyu atla, ilerleme; farklıysa ilerle. 1 1 2 3 3 3 4 → 1 2 3 4.

İkişer yer değiştirme. p = p.next.next ile ikişer atlayıp veri takası. 1 2 3 4 5 → 2 1 4 3 5.

Liste mi, dizi mi?

KriterDizi (array)Bağlı liste (linked list)
i. elemana erişimO(1)O(n)
Başa ekleme / silmeO(n), kaydırmaO(1)
Bilinen bir düğümün arkasına eklemeO(n)O(1)
BoyutSabit (dolunca kopyala)Dinamik
BellekSadece veri, ardışıkVeri + next (+ prev)
İkili arama (sıralıysa)Mümkün, O(log n)Mümkün değil
Önbellek (cache) dostuEvetHayır, düğümler dağınık

Kural: çok rastgele erişim varsa dizi, çok baştan/ortadan ekleme silme varsa liste.

Sık yapılan hatalar

YanlışDoğrusu
Ters çevirmede next’i saklamamakÖnce Node next = curr.next;
reverse sonunda return headreturn prev
Döngü kontrolünde slow.data == fast.dataslow == fast (aynı düğüm)
while (fast.next != null)Önce fast != null kontrolü, yoksa NPE
Merge sonunda kalan listeyi eklememekt.next = (a != null) ? a : b;
Merge’de return dummyreturn dummy.next
Tekrar silerken sildikten sonra da ilerlemekÜçlü tekrarlar kalır; sadece farklıysa ilerle
“Liste her zaman diziden hızlı”Erişimde dizi kazanır, eklemede liste

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

What does the list print after head = reverse(head) if the list was 1 2 3 4? After the second iteration of the loop, what list does prev point to, and which node is curr?

Cevabı göster
4 3 2 1; after step 2: prev = 2 1, curr = 3

Her turda curr'ün next'i prev'e çevrilir ve iki referans bir ileri kayar. Adım 1: prev = 1, curr = 2. Adım 2: prev = 2 → 1, curr = 3. Adım 4'te curr null olur, prev = 4 3 2 1 yeni head'dir.

Soru 2orta

Write a static method Node reverse(Node head) that reverses a singly linked list in place (no new nodes) and returns the new head.

Cevabı göster
Three pointers prev, curr, next; flip curr.next each step; return prev.

Üç referans: prev (ters çevrilmiş kısım), curr (işlenecek düğüm), next (kaybetmemek için saklanan devam).

static Node reverse(Node head) {
    Node prev = null, curr = head;
    while (curr != null) {
        Node next = curr.next;   // devamı sakla
        curr.next = prev;        // oku ters çevir
        prev = curr;             // bir ileri
        curr = next;
    }
    return prev;                 // yeni head
}
Süre O(n), ek bellek O(1). Boş listede döngü çalışmaz, null döner. next'i saklamadan curr.next = prev yaparsan listenin geri kalanını kaybedersin.

Soru 3orta

Using slow/fast pointers with the loop condition fast != null && fast.next != null, which node does middle() return for the list 1 2 3 4 5 6? For 10 20 30 40 50?

Node slow = head, fast = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}
return slow;
Cevabı göster
4 and 30

fast iki adım, slow bir adım atar; fast sona vardığında slow ortadadır. 6 elemanda slow 1 → 2 → 3 → 4, fast 1 → 3 → 5 → null. İki orta varsa (3 ve 4) ikincisini verir: 4. 5 elemanda tam orta: 30.

Soru 4zor

Write static boolean hasCycle(Node head) that returns true if the list contains a cycle. Use O(1) extra memory.

Cevabı göster
Floyd: slow moves 1, fast moves 2; if they ever meet there is a cycle, if fast reaches null there is not.

Döngü yoksa hızlı referans sonunda null'a ulaşır. Döngü varsa ikisi de döngünün içinde döner ve hızlı olan yavaşı mutlaka yakalar (her adımda aradaki mesafe 1 azalır).

static boolean hasCycle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;   // aynı DÜĞÜM (==), aynı veri değil
    }
    return false;
}
slow.data == fast.data yazma: farklı düğümlerde aynı değer olabilir. Referans karşılaştırması gerekir.

Soru 5zor

The list is 1 → 2 → 3 → 4 → 5 and node 5's next points back to node 3. Running Floyd's algorithm (both start at head), after how many steps do slow and fast meet, and at which node?

Cevabı göster
3 steps, at node 4

Adım 1: slow 2, fast 3. Adım 2: slow 3, fast 5. Adım 3: slow 4, fast 5 → 3 → 4. İkisi de 4'te. Program çıktısı da met after 3 steps, düğüm 4.

Soru 6kolay

What does merge(a, b) return for the sorted lists a = 1 4 7 and b = 2 3 8 9?

Cevabı göster
1 2 3 4 7 8 9

Her adımda iki listenin başındaki küçük olan alınır: 1(a), 2(b), 3(b), 4(a), 7(a). a bitti, b'nin kalanı (8 9) olduğu gibi eklenir.

Soru 7zor

Write static Node merge(Node a, Node b) that merges two ascending lists into one ascending list by relinking the existing nodes.

Cevabı göster
Use a dummy head and a tail pointer; repeatedly attach the smaller front node; attach the leftover list at the end.

Sahte bir baş düğüm (dummy) başa ekleme özel durumunu ortadan kaldırır.

static Node merge(Node a, Node b) {
    Node dummy = new Node(0), t = dummy;
    while (a != null && b != null) {
        if (a.data <= b.data) { t.next = a; a = a.next; }
        else                  { t.next = b; b = b.next; }
        t = t.next;
    }
    t.next = (a != null) ? a : b;   // kalan liste
    return dummy.next;
}
5 10 15 ile 2 10 12 → 2 5 10 10 12 15. Listelerden biri null ise diğeri aynen döner. Süre O(n + m), yeni düğüm yok.

Soru 8orta

The sorted list is 1 1 2 3 3 3 4. What is printed after the method runs?

Node p = head;
while (p != null && p.next != null) {
    if (p.data == p.next.data) p.next = p.next.next;
    else p = p.next;
}
print(head);
Cevabı göster
1 2 3 4

Komşu iki düğüm eşitse sonrakini atlar ve p'yi ilerletmez (üç tane 3 olabilir). Farklıysa ilerler. Sıralı listede tekrarlar yan yana olduğu için bu yeterli. Sonuç: 1 2 3 4.

Soru 9orta

lead is moved k nodes ahead, then lead and trail move together until lead is null. For the list 10 20 30 40 50 60 and k = 2, what is trail.data?

Node lead = head, trail = head;
for (int i = 0; i < k; i++) lead = lead.next;
while (lead != null) { lead = lead.next; trail = trail.next; }
return trail.data;
Cevabı göster
50

Aradaki mesafe hep k = 2 düğüm. lead null'a (sonun bir ötesine) varınca trail sondan 2. düğümdedir: 50. k = 6 olsaydı 10 dönerdi. Tek geçişte sondan k. elemanı bulur.

Soru 10orta

What is printed for the list 1 2 3 4 5?

for (Node p = head; p != null && p.next != null; p = p.next.next) {
    int t = p.data;
    p.data = p.next.data;
    p.next.data = t;
}
print(head);
Cevabı göster
2 1 4 3 5

Her turda p ile p.next'in verisi yer değiştirir ve p iki düğüm atlar. (1,2) → 2 1, (3,4) → 4 3, 5 tek kaldığı için koşul p.next != null sağlanmaz ve dokunulmaz.

Soru 11kolay

Choose array or linked list for each: (a) frequent insertions at the front, (b) frequent access to the i-th element, (c) unknown, constantly changing size with insertions in the middle of a traversal.

Cevabı göster
(a) linked list, (b) array, (c) linked list

(a) Listede başa ekleme O(1), dizide bütün elemanlar kayar: O(n). (b) Dizide a[i] O(1), listede i adım yürümek gerekir: O(n). (c) Liste gerektiği kadar büyür ve elindeki düğümün arkasına O(1)'de ekler; dizi dolunca kopyalanmalı ve ortaya ekleme kaydırma ister.

Soru 12zor

What is printed?

Node x = build(3, 6, 9);
x = reverse(x);
Node y = build(1, 5);
Node z = merge(y, reverse(x));
System.out.println(str(z) + " middle=" + middle(z).data);
Cevabı göster
1 3 5 6 9 middle=5

x ters çevrilince 9 6 3. Tekrar ters çevrilince yine 3 6 9 (sıralı). 1 5 ile birleşince 1 3 5 6 9. Beş elemanlı listede orta 5.

Doğrulama: Tüm algoritmalar verify/SEN2211/W04.java içinde JDK ile çalıştırıldı (java W04.java). Ters çevirme izi programın adım adım çıktısından, döngüde buluşma düğümü ve adım sayısı programın sayacından alındı.

Deftere not kartı

REVERSE: prev=null, curr=head; döngü: next=curr.next; curr.next=prev; prev=curr; curr=next; return prev ORTA: slow 1 adım, fast 2 adım; while (fast != null && fast.next != null); çiftte İKİNCİ orta DÖNGÜ (Floyd): aynı slow/fast; slow == fast ise döngü var; fast null'a varırsa yok Döngüde == ile DÜĞÜM karşılaştır, data değil MERGE: dummy düğüm + t; küçük olanı ekle; sonda t.next = kalan liste; O(n+m) Sondan k.: lead'i k ileri al, sonra ikisini birlikte yürüt Sıralı listede tekrar sil: eşitse p.next = p.next.next (p ilerlemez) Hepsi O(n) süre, O(1) ek bellek Dizi: a[i] O(1), başa/ortaya ekleme O(n), sabit boyut, belleğe sıkı Liste: get(i) O(n), başa ekleme O(1), dinamik boyut, düğüm başına next maliyeti Özel durumlar: boş liste, tek eleman, head değişiyor mu?