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.
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:
- Bir referansı değiştirmeden önce, ihtiyacın olan devamı sakla.
- İki referans tekniği (two pointers): biri hızlı biri yavaş, ya da biri önde biri arkada.
- Sahte baş düğüm (dummy node) “liste boş mu, head değişiyor mu” kontrollerini yok eder.
- Her metodu boş liste ve tek elemanlı listede kafanda dene.
Tekrar: üç haftanın özeti
| İşlem | Tek yönlü (head) | Tek yönlü (head+tail) | Çift yönlü (head+tail) | Dizi |
|---|---|---|---|---|
| Başa ekle | O(1) | O(1) | O(1) | O(n) |
| Sona ekle | O(n) | O(1) | O(1) | O(1)* |
| Baştan sil | O(1) | O(1) | O(1) | O(n) |
| Sondan sil | O(n) | O(n) | O(1) | O(1) |
| i. eleman | O(n) | O(n) | O(n) | O(1) |
| Arama | O(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 headyazmak. 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 dahead = 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?
| Kriter | Dizi (array) | Bağlı liste (linked list) |
|---|---|---|
| i. elemana erişim | O(1) | O(n) |
| Başa ekleme / silme | O(n), kaydırma | O(1) |
| Bilinen bir düğümün arkasına ekleme | O(n) | O(1) |
| Boyut | Sabit (dolunca kopyala) | Dinamik |
| Bellek | Sadece veri, ardışık | Veri + next (+ prev) |
| İkili arama (sıralıysa) | Mümkün, O(log n) | Mümkün değil |
| Önbellek (cache) dostu | Evet | Hayı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 head | return prev |
Döngü kontrolünde slow.data == fast.data | slow == fast (aynı düğüm) |
while (fast.next != null) | Önce fast != null kontrolü, yoksa NPE |
| Merge sonunda kalan listeyi eklememek | t.next = (a != null) ? a : b; |
Merge’de return dummy | return 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ç.
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
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.
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
Üç 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.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
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.
Write static boolean hasCycle(Node head) that returns true if the list contains a cycle. Use O(1) extra memory.
Cevabı göster
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.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
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.
What does merge(a, b) return for the sorted lists a = 1 4 7 and b = 2 3 8 9?
Cevabı göster
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.
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
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.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
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.
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
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.
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
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.
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) 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.
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
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ı.