SEN2211 · 3. hafta · Doubly Linked Lists, Ordered Linked Lists

Ç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.

doubly linked listprev referencetailordered linked listsorted insertcircular linked listdo-while traversalJosephus problemsentinel

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:

  1. Çift yönlü listede her bağlantı iki taraflıdır. a.next = b yaptıysan b.prev = a da yapmalısın.
  2. head ve tail her işlemde aday. Boş listeye ekleme ve son elemanı silme ikisini birden değiştirir.
  3. Sıralı ekleme = doğru yeri bul + normal ekleme.
  4. Dairesel listede null yok. 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.

İşlemSırasız listeSıralı liste
EklemeO(1) başaO(n) yer bulma
Arama (bulunamadı)O(n), hep sonuna kadarO(n), ama erken durabilir
En küçük elemanO(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-while kullan 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üncellemekHer bağlantıyı iki yönde kur: next ve prev
Boş listeye eklerken sadece head’i ayarlamakhead = tail = n
Son elemanı silince tail’i bırakmakListe boşaldıysa head ve tail ikisi de null
p.prev.next = p.next head’de de çalışırp head ise p.prev null: önce kontrol et
insertSorted’da başa ekleme durumunu unutmakx < head.data ise yeni head
Sıralı bağlı listede binary searchOrtaya O(1) erişim yok, arama O(n)
Dairesel listede while (p != null)do { } while (p != tail.next)
Tek elemanlı dairesel listede n.next = nullTek düğüm kendini gösterir: n.next = n

Kendini test et

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

Soru 1kolay

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
8 2 4 6 / 6 4 2 8

[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.

Soru 2kolay

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
4

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.

Soru 3orta

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
5 / 2 / 1 2 10 4

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.

Soru 4orta

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
tail = tail.prev; then tail.next = null (or head = null if the list became empty).

Ç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.

Soru 5zor

Write void insertSorted(int x) for a singly linked list kept in ascending order. Equal values go after existing ones.

Cevabı göster
Insert at head if empty or x < head.data; otherwise advance while p.next.data <= x, then link.

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.

Soru 6kolay

Values are inserted with insertSorted in this order: 9, 1, 8, 3, 5. What does the list print?

Cevabı göster
1 3 5 8 9

[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.

Soru 7orta

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
4

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).

Soru 8orta

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 tail
Cevabı göster
9 7 4

[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.

Soru 9kolay

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
No node's next is null; use do-while until p returns to tail.next.

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.

Soru 10zor

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
2 4 6 3 1, survivor 5

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ı.

Soru 11orta

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
Crashes when p is the head or the tail (NullPointerException); check p.prev == null / p.next == null and update head / tail.

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;

Soru 12kolay

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
removeLast

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ı.

Deftere not kartı

DNode: data, prev, next liste: head + tail addFirst: n.next = head; head.prev = n; head = n; (boşsa head = tail = n) addLast: n.prev = tail; tail.next = n; tail = n; Araya ekleme (p'den sonra): 4 referans: n.prev, n.next, p.next.prev, p.next Düğüm silme: p.prev.next = p.next; p.next.prev = p.prev; head/tail ise özel durum removeLast çift yönlüde O(1) (tail.prev), tek yönlüde O(n) Geriye yazdır: for (p = tail; p != null; p = p.prev) Sıralı liste insertSorted: boş veya x < head.data → başa; yoksa while (p.next != null && p.next.data <= x) Sıralı aramada erken çıkış: p.data > x ise dur (en kötü yine O(n)) Dairesel: son.next = ilk; null YOK; sadece tail tut, ilk = tail.next Dairesel gezinme: do { ... p = p.next; } while (p != tail.next); rotate: tail = tail.next Josephus: rotate + removeFirst tekrar