SEN2211 · 2. hafta · Introduction to Linked Lists

Tek yönlü bağlı liste (singly linked list)

Node sınıfı, başa/sona/araya ekleme, silme ve gezinme. Vizede “listenin son hali ne?” ve “şu metodu yaz” sorularının kaynağı.

linked listnodeheadtailnext referencetraversaladdFirstaddLastremoveFirstnull referenceNullPointerException

Dizi (array) güzel bir yapı ama iki dertlidir: boyutu baştan sabittir ve ortaya eleman eklemek için diğerlerini kaydırman gerekir. Bağlı liste (linked list) bu iki derdi çözer. Her eleman kendi küçük kutusunda (node) durur ve bir sonraki kutunun adresini taşır. Sınavda iki tip soru gelir: bir dizi işlemden sonra listenin son halini yazmak, ve addLast, remove, countNodes gibi bir metodu sıfırdan yazmak.

Altın kurallar:

  1. Liste = head referansı. Head’i kaybedersen listeyi kaybedersin.
  2. Gezinme her zaman p = p.next ile. Bitiş koşulu p != null (her düğümü gez) ya da p.next != null (son düğümde dur).
  3. Ekleme sırasında önce yeni düğümü bağla, sonra eskisini yönlendir.
  4. Silmek için öncekini bul. Tek yönlü listede geri gidemezsin.
  5. Boş liste, tek elemanlı liste ve head her zaman özel durumdur.

Node sınıfı

Bir düğüm iki şey taşır: veri (data) ve bir sonraki düğüme referans (next). Son düğümün next’i null’dır.

class Node {
    int data;
    Node next;
    Node(int data) { this.data = data; }   // next otomatik null
}

class SinglyLinkedList {
    Node head;   // ilk düğüm; boş listede null
    int size;
}

Bellekte 10 → 20 → 30 listesi şöyle görünür:

head
 |
 v
[10 | *]--->[20 | *]--->[30 | null]

Her kutu bellekte farklı bir yerde durabilir. Diziden farkı bu: a[2] doğrudan hesaplanır, ama listede üçüncü elemana ulaşmak için head’den başlayıp iki kez next izlemen gerekir. Bu yüzden indeksle erişim O(n).

TUZAK · Node p = head; yeni düğüm yaratmaz. Sadece var olan düğümü gösteren ikinci bir ok oluşturur. p.data = 99 yaparsan head’in verisi de 99 olur, çünkü aynı kutu.

Gezinme (traversal)

Listeyi yazdırmak, toplamını almak, eleman aramak: hepsi aynı kalıp.

void print() {
    Node p = head;
    while (p != null) {
        System.out.print(p.data + " ");
        p = p.next;
    }
    System.out.println();
}

boolean contains(int x) {
    for (Node p = head; p != null; p = p.next)
        if (p.data == x) return true;
    return false;
}

head’in kendisini ilerletme (head = head.next). Yazdırdıktan sonra liste boş görünür. Her zaman yardımcı bir p kullan.

Ekleme

Başa ekleme (addFirst)

void addFirst(int x) {
    Node n = new Node(x);
    n.next = head;   // yeni düğüm eski ilk düğümü göstersin
    head = n;        // head yeni düğüm olsun
    size++;
}

İki satır, döngü yok: O(1). Boş listede de çalışır (head null ise n.next = null olur).

Sona ekleme (addLast)

Sadece head varsa son düğüme yürümek zorundasın.

void addLast(int x) {
    Node n = new Node(x);
    if (head == null) { head = n; size++; return; }
    Node p = head;
    while (p.next != null) p = p.next;   // p artık son düğüm
    p.next = n;
    size++;
}

Süre O(n). Sınıfa bir tail referansı eklersen tail.next = n; tail = n; ile O(1) olur, ama o zaman her silmede tail’i de güncellemen gerekir.

Araya ekleme (insertAt)

İndeks index konumuna eklemek için index - 1 konumundaki düğüme git.

void insertAt(int index, int x) {   // 0 = başa
    if (index == 0) { addFirst(x); return; }
    Node p = head;
    for (int i = 0; i < index - 1; i++) p = p.next;
    Node n = new Node(x);
    n.next = p.next;   // 1) yeni düğüm, p'nin eski sonrakini göstersin
    p.next = n;        // 2) p yeni düğümü göstersin
    size++;
}
Önce:   p=[20] ---> [30]
1)      [25] ---> [30]   (n.next = p.next)
2)      [20] ---> [25] ---> [30]   (p.next = n)

TUZAK · Satırların sırası. Önce p.next = n; yazarsan p.next’in eski değeri (30) kaybolur. Sonra n.next = p.next; aslında n.next = n olur. Düğüm kendini gösterir, liste sonsuz döngüye girer.

Silme

Baştan silme

int removeFirst() {
    if (head == null) throw new RuntimeException("empty list");
    int x = head.data;
    head = head.next;
    size--;
    return x;
}

Eski ilk düğümü gösteren kimse kalmaz, Java’nın çöp toplayıcısı (garbage collector) onu temizler. O(1).

Sondan silme

Son düğümü silmek için sondan bir önceki düğümün next’ini null yapmalısın.

int removeLast() {
    if (head == null) throw new RuntimeException("empty list");
    if (head.next == null) {            // tek eleman
        int x = head.data; head = null; size--; return x;
    }
    Node p = head;
    while (p.next.next != null) p = p.next;   // p = sondan bir önceki
    int x = p.next.data;
    p.next = null;
    size--;
    return x;
}

Tail referansı olsa bile bu O(n). Tail sana son düğümü verir ama sondan öncekini vermez.

Değere göre silme

boolean remove(int x) {
    if (head == null) return false;
    if (head.data == x) { head = head.next; size--; return true; }
    Node p = head;
    while (p.next != null && p.next.data != x) p = p.next;
    if (p.next == null) return false;
    p.next = p.next.next;   // hedefi atla
    size--;
    return true;
}

Döngü koşulunun sırası önemli: p.next != null önce kontrol edilir, böylece p.next.data null üzerinde çağrılmaz (&& kısa devre yapar).

Çözümlü örnek: işlem izi

Boş listeyle başla ve her adımda listeyi yaz:

list.addFirst(20);
list.addFirst(10);
list.addLast(30);
list.addLast(40);
list.print();
list.insertAt(2, 25);
list.print();
System.out.println("removeFirst=" + list.removeFirst());
System.out.println("removeLast=" + list.removeLast());
list.print();
System.out.println(list.remove(25) + " " + list.remove(99));
list.print();
10 20 30 40 
10 20 25 30 40 
removeFirst=10
removeLast=40
20 25 30 
true false
20 30 
addFirst(20)   [20]
addFirst(10)   [10, 20]
addLast(30)    [10, 20, 30]
addLast(40)    [10, 20, 30, 40]
insertAt(2,25) [10, 20, 25, 30, 40]   (indeks 1'deki 20'den sonra)
removeFirst    [20, 25, 30, 40]   döner 10
removeLast     [20, 25, 30]       döner 40
remove(25)     [20, 30]           true
remove(99)     [20, 30]           false, değişmedi

Sınavda bu tabloyu kendin çiz. Her satırda listenin tamamını yazmak hata yapmanı engeller.

Karmaşıklık özeti

İşlemSadece headhead + tailDizi (array)
addFirstO(1)O(1)O(n) (kaydırma)
addLastO(n)O(1)O(1) (yer varsa)
removeFirstO(1)O(1)O(n)
removeLastO(n)O(n)O(1)
get(i)O(n)O(n)O(1)
containsO(n)O(n)O(n)

Sık yapılan hatalar

YanlışDoğrusu
Gezinirken head = head.nextYardımcı Node p = head; kullan
Araya eklerken önce p.next = nÖnce n.next = p.next, sonra p.next = n
addLast’te boş liste kontrolü yokhead == null ise head = n
Silerken hedef düğümün kendisinde durmakBir öncekinde dur, p.next = p.next.next
while (p.next.data != x && p.next != null)Null kontrolü önce: p.next != null && ...
Tail varsa removeLast O(1)Hâlâ O(n): sondan öncekini bulmak için yürümek gerekir
p = p.next.next her zaman güvenliTek sayıda düğümde NullPointerException

Kendini test et

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

Soru 1kolay

Starting from an empty list, what does print() show after these calls?

list.addFirst(3);
list.addFirst(7);
list.addLast(5);
list.addFirst(1);
list.print();
Cevabı göster
1 7 3 5

addFirst(3) → [3]. addFirst(7) → [7, 3]. addLast(5) → [7, 3, 5]. addFirst(1) → [1, 7, 3, 5]. addFirst her zaman yeni head yapar.

Soru 2orta

Starting from an empty list, what is printed?

list.addLast(4);
list.addLast(8);
list.addFirst(2);
list.addLast(6);
list.removeFirst();
list.addFirst(9);
list.removeLast();
list.print();
Cevabı göster
9 4 8

[4] → [4, 8] → [2, 4, 8] → [2, 4, 8, 6] → removeFirst siler 2: [4, 8, 6] → addFirst(9): [9, 4, 8, 6] → removeLast siler 6: [9, 4, 8].

Soru 3orta

What is printed?

Node a = new Node(1);
a.next = new Node(2);
a.next.next = new Node(3);
a.next.next.next = new Node(4);
a.next = a.next.next;
for (Node p = a; p != null; p = p.next)
    System.out.print(p.data + " ");
Cevabı göster
1 3 4

Zincir 1 → 2 → 3 → 4. a.next = a.next.next satırı 1'in next'ini 3'e bağlar. 2'yi gösteren kimse kalmadı, listeden düştü (garbage collector temizler). Kalan: 1 3 4.

Soru 4orta

What is printed?

Node h = new Node(5);
h.next = new Node(10);
h.next.next = new Node(15);
Node t = h.next;
t.next.data = t.data + h.data;
h.data = t.next.data * 2;
for (Node p = h; p != null; p = p.next)
    System.out.print(p.data + " ");
Cevabı göster
30 10 15

t ikinci düğüm (10). t.next üçüncü düğüm: data = 10 + 5 = 15 (zaten 15'ti). h.data = t.next.data * 2 = 30. Liste: 30 10 15. Referans değişkeni t yeni düğüm yaratmaz, var olanı gösterir.

Soru 5zor

Using insertAt(index, x) (index 0 = front), what is printed? Start: list 1 2 3.

list.insertAt(1, 9);
list.insertAt(4, 7);
list.insertAt(0, 5);
list.print();
list.remove(2);
list.remove(5);
list.print();
Cevabı göster
5 1 9 2 3 7 / 1 9 3 7

[1,2,3] → insertAt(1,9): [1,9,2,3] → insertAt(4,7) sona: [1,9,2,3,7] → insertAt(0,5): [5,1,9,2,3,7]. Sonra remove(2) değeri 2 olanı siler: [5,1,9,3,7]; remove(5) head'i siler: [1,9,3,7].

Soru 6kolay

Write a method int countNodes() for the SinglyLinkedList class that returns the number of nodes without using the size field.

Cevabı göster
Traverse with p from head to null, counting.

Bir referansı head'den başlatıp null olana kadar ilerlet, her adımda say. Boş listede döngü hiç çalışmaz, 0 döner.

int countNodes() {
    int c = 0;
    Node p = head;
    while (p != null) {
        c++;
        p = p.next;
    }
    return c;
}
Liste 4 7 2 9 5 için 5 döner. Süre O(n).

Soru 7orta

Write void addLast(int x) for a singly linked list that has only a head reference (no tail). Handle the empty list.

Cevabı göster
Walk to the last node (p.next == null) and link the new node.

İki durum var. Boş listede yeni düğüm head olur. Doluysa son düğüme kadar git (p.next null olan düğüm) ve yeni düğümü onun next'ine bağla.

void addLast(int x) {
    Node n = new Node(x);
    if (head == null) {
        head = n;
        return;
    }
    Node p = head;
    while (p.next != null)
        p = p.next;
    p.next = n;
}
Boş liste kontrolü olmazsa p.next satırı NullPointerException fırlatır. Süre O(n), çünkü sona yürümek zorundasın.

Soru 8zor

Write boolean remove(int x) that removes the first node whose data equals x and returns whether something was removed.

Cevabı göster
Special-case head; otherwise stop at the node before the target and do p.next = p.next.next.

Tek yönlü listede bir düğümü silmek için ondan önceki düğüme ihtiyacın var. Head özel durumdur, çünkü öncesi yok.

boolean remove(int x) {
    if (head == null) return false;
    if (head.data == x) {
        head = head.next;
        return true;
    }
    Node p = head;
    while (p.next != null && p.next.data != x)
        p = p.next;
    if (p.next == null) return false;   // bulunamadı
    p.next = p.next.next;               // atla
    return true;
}
Liste 20 25 30 üzerinde remove(25) → true, liste 20 30. remove(99) → false, liste değişmez.

Soru 9kolay

For a singly linked list with only a head reference, what are the time complexities of addFirst, addLast and removeLast? What changes if a tail reference is added?

Cevabı göster
O(1), O(n), O(n); with tail: addLast O(1), removeLast still O(n)

addFirst sadece head'i değiştirir: O(1). addLast ve removeLast sona yürür: O(n). tail eklenince addLast O(1) olur. removeLast ise O(n) kalır, çünkü sondan bir önceki düğümü bulmak için yine baştan yürümek gerekir (tek yönlü listede geri gidemezsin).

Soru 10orta

p points to the first node of the list 1 2 3. A student inserts node n (data 8) after p with the two lines below. What happens when the list is printed?

p.next = n;
n.next = p.next;
Cevabı göster
1 8 8 8 ... (infinite loop: n points to itself)

Sıra yanlış. İlk satırdan sonra p.next artık n. İkinci satır n.next = n yapar, düğüm kendini gösterir. 2 ve 3 kaybolur, yazdırma döngüsü 8'i sonsuza kadar basar. Doğru sıra: önce n.next = p.next; sonra p.next = n;. Kural: yeni düğümü önce zincire bağla, sonra eskisini ona yönlendir.

Soru 11zor

The list contains 10 20 30 40 50. What happens?

for (Node p = head; p != null; p = p.next.next)
    System.out.print(p.data + " ");
Cevabı göster
10 30 50, then NullPointerException

p: 10 → 30 → 50 basılır. 50'deyken p.next null, p.next.next null'ın next'ine erişmeye çalışır ve NullPointerException fırlar. Liste çift sayıda olsaydı (6 eleman) p son adımda null olur ve sorunsuz biterdi. Güvenli koşul: p != null && p.next != null ile kontrol.

Soru 12kolay

The list is 4 7 2 9 5. What is printed?

int mx = head.data, cnt = 0;
for (Node r = head; r != null; r = r.next) {
    if (r.data > mx) mx = r.data;
    if (r.data % 2 == 1) cnt++;
}
System.out.println(mx + " " + cnt);
Cevabı göster
9 3

En büyük eleman 9. Tek olanlar 7, 9, 5: üç tane. Çıktı 9 3.

Doğrulama: Tüm liste işlemleri ve izler verify/SEN2211/W02.java içinde aynı Node/SinglyLinkedList koduyla JDK'da çalıştırıldı (java W02.java); sayfadaki her çıktı programın çıktısından birebir alındı.

Deftere not kartı

Node: int data; Node next; liste = sadece head referansı (+ istersen tail, size) Gezinme: for (Node p = head; p != null; p = p.next) Son düğümde dur: while (p.next != null) p = p.next; addFirst: n.next = head; head = n; O(1) addLast (tail yok): sona yürü, p.next = n; O(n) boş liste kontrolü ŞART Araya ekle (p'den sonra): ÖNCE n.next = p.next; SONRA p.next = n; Sil (p'den sonrakini): p.next = p.next.next; silmek için ÖNCEKİ düğüm lazım removeFirst: head = head.next; O(1) removeLast: sondan bir önceki düğüme yürü, p.next = null; tail olsa bile O(n) Head silinecekse özel durum; boş liste özel durum; tek elemanlı liste özel durum NPE tuzakları: null.next, boş listede head.data, p.next.next Dizi vs liste: a[i] O(1) ama listede i. eleman O(n)