Çift yönlü, sıralı ve dairesel listeler
prev referansı olan çift yönlü liste, elemanları sıralı tutan ordered list ve sonu başa bağlanan dairesel liste. Sınavda referans güncelleme izleri ve insertSorted metodu sorulur.
Geçen hafta tek yönlü listede iki zayıf nokta gördük: sondan silmek O(n), ve bir düğümü silmek için öncekini aramak gerekiyor. Bu hafta üç varyasyon var. Çift yönlü liste her düğüme bir de prev referansı ekler ve geriye gidebilir. Sıralı liste (ordered list) elemanları her zaman küçükten büyüğe tutar. Dairesel liste (circular list) son düğümü başa bağlar. Sınav soruları yine iz sürme ve metot yazma; bu sefer güncellenecek referans sayısı arttığı için hata payı da arttı.
Altın kurallar:
- Çift yönlü listede her bağlantı iki taraflıdır. a.next = b yaptıysan b.prev = a da yapmalısın.
- head ve tail her işlemde aday. Boş listeye ekleme ve son elemanı silme ikisini birden değiştirir.
- Sıralı ekleme = doğru yeri bul + normal ekleme.
- Dairesel listede
nullyok. Döngüyü “başa döndüm mü” ile bitir.
Çift yönlü liste (doubly linked list)
class DNode {
int data;
DNode prev, next;
DNode(int d) { data = d; }
}
class DoublyLinkedList {
DNode head, tail;
int size;
}
head tail
| |
v v
[null|5|*] <-> [*|10|*] <-> [*|20|null]
Her düğüm iki yönü bilir. Bedeli: düğüm başına bir referans fazla bellek ve her işlemde iki kat referans güncellemesi.
Ekleme
void addFirst(int x) {
DNode n = new DNode(x);
if (head == null) { head = tail = n; }
else { n.next = head; head.prev = n; head = n; }
size++;
}
void addLast(int x) {
DNode n = new DNode(x);
if (tail == null) { head = tail = n; }
else { n.prev = tail; tail.next = n; tail = n; }
size++;
}
void insertAfter(DNode p, int x) {
if (p == tail) { addLast(x); return; }
DNode n = new DNode(x);
n.prev = p; // 1
n.next = p.next; // 2
p.next.prev = n; // 3 (sağdaki komşu geri n'yi göstersin)
p.next = n; // 4 (en son: p.next'i hâlâ kullanıyorduk)
size++;
}
Araya eklemede dört referans değişir. 3 ve 4’ün sırası önemli: önce p.next = n yaparsan p.next.prev artık n’nin kendisinin prev’i olur ve sağdaki komşu güncellenmez.
Silme
Elinde silinecek düğümün kendisi varsa, öncesini aramana gerek yok. Bu çift yönlü listenin en büyük avantajı.
void removeNode(DNode p) {
if (p.prev == null) head = p.next; else p.prev.next = p.next;
if (p.next == null) tail = p.prev; else p.next.prev = p.prev;
size--;
}
int removeLast() {
if (tail == null) throw new RuntimeException("empty list");
int x = tail.data;
tail = tail.prev;
if (tail == null) head = null;
else tail.next = null;
size--;
return x;
}
removeLast artık O(1). Tek yönlüde O(n) idi.
Çözümlü örnek
d.addLast(10); d.addLast(20); d.addLast(40); d.addFirst(5);
d.insertAfter(d.find(20), 30);
d.printForward(); d.printBackward();
d.removeNode(d.find(20));
System.out.println("removeLast=" + d.removeLast());
d.printForward(); d.printBackward();
5 10 20 30 40
40 30 20 10 5
removeLast=40
5 10 30
30 10 5
İPUCU · İki yönden yazdır. Bir çift yönlü liste metodunu test etmenin en iyi yolu hem ileri hem geri yazdırmak. İleri doğru doğru görünüp geri doğru farklı çıkıyorsa bir prev’i unutmuşsundur. Sınavda da izi iki yönden kontrol et.
Sıralı bağlı liste (ordered linked list)
Elemanlar her zaman artan sırada tutulur. Kullanıcı nereye ekleyeceğini söylemez; liste doğru yeri kendi bulur.
void insertSorted(int x) {
Node n = new Node(x);
if (head == null || x < head.data) { // boş ya da en küçük → başa
n.next = head;
head = n;
return;
}
Node p = head;
while (p.next != null && p.next.data <= x)
p = p.next; // x'ten büyük ilk düğümün öncesinde dur
n.next = p.next;
p.next = n;
}
5, 2, 8, 2, 6 sırasıyla ekleyip her adımdan sonra yazdıralım:
5
2 5
2 5 8
2 2 5 8
2 2 5 6 8
<= ile eşit değer var olanların arkasına girer; < yazsaydın önüne girerdi. İkisi de sıralı bir liste verir, ama soruda “equal values go after” denirse fark eder.
Sıralı listede arama
Liste sıralı olduğu için aradığın değeri geçtiğin an durabilirsin:
boolean contains(int x) {
for (Node p = head; p != null; p = p.next) {
if (p.data == x) return true;
if (p.data > x) return false; // erken çıkış
}
return false;
}
2 2 5 6 8 listesinde 7’yi ararken 5 düğüme bakılır (8’de durur), 1’i ararken tek düğüme bakılır (2 > 1). Ortalama durum iyileşir ama en kötü durum yine O(n). Bağlı listede ikili arama (binary search) yapamazsın, çünkü ortadaki elemana O(1)‘de atlayamazsın.
| İşlem | Sırasız liste | Sıralı liste |
|---|---|---|
| Ekleme | O(1) başa | O(n) yer bulma |
| Arama (bulunamadı) | O(n), hep sonuna kadar | O(n), ama erken durabilir |
| En küçük eleman | O(n) | O(1), head |
Dairesel liste (circular linked list)
Son düğümün next’i null yerine ilk düğümü gösterir. Pratik bir hile: sadece tail tut. İlk düğüm her zaman tail.next. Böylece hem başa hem sona O(1) erişirsin.
+-----------------------------+
v |
tail.next=[0] -> [1] -> [2] -> [3]=tail-+
void addFirst(int x) {
Node n = new Node(x);
if (tail == null) { tail = n; n.next = n; } // tek düğüm kendini gösterir
else { n.next = tail.next; tail.next = n; }
}
void addLast(int x) { addFirst(x); tail = tail.next; } // başa ekle, tail'i kaydır
int removeFirst() {
Node first = tail.next;
if (first == tail) tail = null; // tek eleman vardı
else tail.next = first.next;
return first.data;
}
void rotate() { if (tail != null) tail = tail.next; } // ilk eleman sona geçer
void print() {
Node p = tail.next;
do {
System.out.print(p.data + " ");
p = p.next;
} while (p != tail.next);
System.out.println();
}
addLast hilesine dikkat: başa ekleyip tail’i bir ileri kaydırınca yeni düğüm son olur. Çözümlü örnek:
c.addLast(1); c.addLast(2); c.addLast(3); c.addFirst(0);
c.print();
c.rotate(); c.print();
System.out.println("removeFirst=" + c.removeFirst());
c.print();
0 1 2 3
1 2 3 0
removeFirst=1
2 3 0
TUZAK ·
while (p != null)dairesel listede sonsuz döngüdür. Hiçbir düğümün next’i null değil.do-whilekullan ve başa dönünce dur.while (p != tail.next)ile başlarsan döngü hiç çalışmaz, çünkü p zaten tail.next’ten başlıyor.
Dairesel listenin tipik kullanımları: sırayla dönen işler (round-robin zamanlayıcı), çok oyunculu oyunda sıra, ve Josephus problemi (çemberde her k. kişiyi elemek).
Sık yapılan hatalar
| Yanlış | Doğrusu |
|---|---|
| Çift yönlüde sadece next’i güncellemek | Her bağlantıyı iki yönde kur: next ve prev |
| Boş listeye eklerken sadece head’i ayarlamak | head = tail = n |
| Son elemanı silince tail’i bırakmak | Liste boşaldıysa head ve tail ikisi de null |
p.prev.next = p.next head’de de çalışır | p head ise p.prev null: önce kontrol et |
| insertSorted’da başa ekleme durumunu unutmak | x < head.data ise yeni head |
| Sıralı bağlı listede binary search | Ortaya O(1) erişim yok, arama O(n) |
Dairesel listede while (p != null) | do { } while (p != tail.next) |
Tek elemanlı dairesel listede n.next = null | Tek düğüm kendini gösterir: n.next = n |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
Starting from an empty doubly linked list, what do printForward() and printBackward() show?
list.addFirst(2);
list.addLast(4);
list.addFirst(1);
list.addLast(6);
list.removeFirst();
list.addFirst(8);Cevabı göster
[2] → [2,4] → [1,2,4] → [1,2,4,6] → removeFirst: [2,4,6] → addFirst(8): [8,2,4,6]. Geriye doğru tail'den prev ile: 6 4 2 8.
When a new node is inserted between two existing nodes of a doubly linked list, how many references must be set or changed?
Cevabı göster
Yeni düğümün prev ve next'i (2 tane), soldaki düğümün next'i ve sağdaki düğümün prev'i (2 tane). Toplam 4. Tek yönlü listede bu sayı 2'dir.
The doubly linked list is 1 2 3 4. What is printed?
DNode p = head.next;
System.out.println(p.prev.data + p.next.next.data);
System.out.println(tail.prev.prev.data);
p.next.data = p.prev.data * 10;
printForward();Cevabı göster
p = 2. p.prev = 1, p.next.next = 4; int toplama: 5. tail.prev.prev: 4 → 3 → 2. Son olarak p.next (3) verisi 1 · 10 = 10 olur: 1 2 10 4.
Write int removeLast() for a doubly linked list with head and tail. It must run in O(1) and handle a one-element list.
Cevabı göster
Çift yönlü listede tail.prev sondan bir önceki düğümü verir, yürümeye gerek yok.
int removeLast() {
if (tail == null) throw new RuntimeException("empty list");
int x = tail.data;
tail = tail.prev;
if (tail == null) // tek eleman vardı
head = null;
else
tail.next = null; // yeni son düğüm
size--;
return x;
}Liste 7 8 9: iki çağrı 9 ve 8 döner, liste 7 kalır. Üçüncü çağrı 7 döner, head ve tail ikisi de null olur.Write void insertSorted(int x) for a singly linked list kept in ascending order. Equal values go after existing ones.
Cevabı göster
Başa ekleme durumu ayrı: liste boşsa ya da x en küçükse. Aksi halde x'ten büyük ilk düğümün öncesinde dur.
void insertSorted(int x) {
Node n = new Node(x);
if (head == null || x < head.data) {
n.next = head;
head = n;
return;
}
Node p = head;
while (p.next != null && p.next.data <= x)
p = p.next;
n.next = p.next;
p.next = n;
}<= kullanıldığı için eşit değerler var olanların arkasına gelir. 5, 2, 8, 2, 6 sırayla eklenince liste 2 2 5 6 8 olur.Values are inserted with insertSorted in this order: 9, 1, 8, 3, 5. What does the list print?
Cevabı göster
[9] → 1 başa: [1,9] → 8 araya: [1,8,9] → 3: [1,3,8,9] → 5: [1,3,5,8,9]. Ekleme sırası ne olursa olsun liste sıralı kalır.
The sorted list is 1 3 5 8 9. The contains method stops as soon as it sees a value greater than the target. How many nodes are examined when searching for 7?
Cevabı göster
1, 3, 5 küçük, devam. 8 > 7, artık ileride 7 olamaz, false döner. 1, 3, 5, 8: dört düğüm. Sırasız listede 5 düğümün hepsine bakmak gerekirdi. En kötü durum yine O(n).
A circular singly linked list keeps only tail (tail.next is the first node). Starting empty, what is printed?
c.addLast(4);
c.addFirst(7);
c.addLast(9);
c.rotate(); // tail = tail.next
c.addFirst(1);
c.removeFirst();
c.rotate();
c.print(); // from tail.next around to tailCevabı göster
[4] → [7,4] → [7,4,9] (tail 9). rotate: tail 7 olur, baştan okuma 4 9 7. addFirst(1): [1,4,9,7]. removeFirst 1'i siler: [4,9,7]. rotate: tail 4, okuma 9 7 4.
Why does this loop never end on a circular linked list, and how should it be written?
Node p = tail.next;
while (p != null) {
System.out.print(p.data + " ");
p = p.next;
}Cevabı göster
Dairesel listede son düğüm ilk düğümü gösterir, hiçbir next null değildir. Durma koşulu başa dönmektir: Node p = tail.next; do { print; p = p.next; } while (p != tail.next); do-while, tek elemanlı listede de bir kez basar.
People 1..6 sit in a circle (circular list, tail = 6). Repeat 5 times: rotate() then removeFirst() (remove every second person). In what order are people removed, and who survives?
Cevabı göster
rotate tail'i 1'e taşır, ilk düğüm 2 silinir. Sonra tail 3'e, 4 silinir. tail 5, 6 silinir. Kalan 1 3 5: tail 1, 3 silinir. Kalan 5 1: tail 5, 1 silinir. Hayatta kalan 5. Bu Josephus problemi, dairesel listenin klasik uygulaması.
A student removes node p from a doubly linked list with the two lines below. On which nodes does this crash, and how is it fixed?
p.prev.next = p.next;
p.next.prev = p.prev;Cevabı göster
p head ise p.prev null, tail ise p.next null; ikisinde de NullPointerException. Düzeltme:
if (p.prev == null) head = p.next;
else p.prev.next = p.next;
if (p.next == null) tail = p.prev;
else p.next.prev = p.prev;Name one operation that is O(n) in a singly linked list with head and tail, but O(1) in a doubly linked list with head and tail.
Cevabı göster
Tek yönlü listede sondan bir önceki düğümü bulmak için baştan yürümek gerekir: O(n). Çift yönlüde tail.prev hazır: O(1). Aynı şekilde elinde referansı olan bir düğümü silmek de çift yönlüde O(1).
Doğrulama: Bütün izler ve metotlar verify/SEN2211/W03.java içinde DoublyLinkedList, SortedList ve CircularList sınıflarıyla JDK'da çalıştırıldı (java W03.java). Karşılaştırma sayıları programdaki sayaçtan alındı.