Tek Yönlü Bağlı Listeler - Singly Linked List

Elemanları pointer'larla birbirine bağlayan dinamik veri yapısı

🗂️ Veri Yapıları 📅 20 February 2026 👁️ 21 görüntülenme

Bağlı Liste Nedir?

Bağlı liste (linked list), elemanların bellekte ardışık olmak zorunda olmadığı, her elemanın bir sonraki elemanın adresini tutarak birbirine bağlandığı dinamik bir veri yapısıdır. Dizilerde elemanlar yan yana dururken, bağlı listede her eleman belleğin herhangi bir yerinde olabilir; önemli olan birbirlerini tanımalarıdır.

Bunu bir kağıt oyununa benzetebilirsiniz: her kağıtta bir bilgi ve bir sonraki kağıdın nerede olduğunu gösteren bir ipucu yazılıdır. İlk kağıdı (head) bildiğiniz sürece, ipuçlarını takip ederek tüm kağıtlara sırayla ulaşabilirsiniz. Son kağıttaki ipucu "yok" (NULL) der ve zincir biter.

Tek Yönlü Bağlı Liste Yapısı
head

10
next
20
next
30
next
40
next
NULL
Her düğüm (node) = veri (data) + sonraki düğümün adresi (next)

Temel Kavramlar

Düğüm (Node) Nedir?

Bağlı listenin her bir elemanına düğüm (node) denir. Tek yönlü bağlı listede her düğüm iki parçadan oluşur: saklanan veri (data) ve bir sonraki düğümü gösteren pointer (next). Dizideki bir eleman sadece veri tutarken, bağlı listedeki bir düğüm hem veriyi hem de "sonraki eleman nerede" bilgisini taşır. Bu ekstra pointer, bağlı listenin esnekliğinin bedelidir.

Head (Baş) Pointer

Head, listenin ilk düğümünü gösteren pointer'dır. Listeye erişmenin tek yolu head pointer'ıdır. Head'i kaybederseniz listenin tamamını kaybedersiniz, çünkü düğümlere ulaşmanın başka bir yolu yoktur. Boş bir listede head NULL'dur.

NULL (Son İşareti)

Listenin son düğümünün next pointer'ı NULL değerini tutar. Bu, "benden sonra kimse yok, liste burada bitiyor" anlamına gelir. Traverse işleminde NULL'a ulaşmak döngünün durma koşuludur.

Dizi ile Temel Fark

Dizi (Array)

Bellekte ardışık, sabit boyut, indeks ile O(1) erişim, ekleme/silme yavaş (O(n)), tanımda boyut gerekli

Bağlı Liste (Linked List)

Bellekte dağınık, dinamik boyut, erişim yavaş (O(n)), ekleme/silme hızlı (O(1)), boyut gerekmez

Bellekte Dizi vs Linked List

Farkı somutlaştırmak için ikisinin bellekteki görünümünü karşılaştıralım. Dizi elemanları yan yana otururken, linked list düğümleri belleğin tamamen farklı bölgelerinde olabilir. Birbirlerini pointer'lar aracılığıyla tanırlar.

Bellekte Karşılaştırma
Dizi: Ardışık bellek blokları
0x100
10
0x104
20
0x108
30
0x10C
40
Adresler art arda, 4'er byte arayla
Linked List: Dağınık bellek, pointer bağlantıları
0x200
10
→0x58C
0x58C
20
→0x340
0x340
30
→0x7A0
0x7A0
40
NULL
Adresler rastgele, her düğüm sonrakini tanır

Neden Bağlı Liste Kullanırız?

Diziler harika bir veri yapısıdır, peki o zaman neden linked list'e ihtiyaç duyarız? Çünkü bazı senaryolarda dizinin sınırlamaları ciddi sorun oluşturur:

Bilinmeyen veri miktarı: Bir metin editöründe kullanıcı kaç satır yazacağını önceden bilemezsiniz. Dizi ile ya çok büyük alan ayırıp israf edersiniz ya da sık sık realloc ile boyut değiştirirsiniz. Linked list'te her yeni satır için basitçe bir düğüm eklersiniz.

Sık ekleme ve silme: Bir müzik çalma listesinde şarkı eklemek, silmek ve sıra değiştirmek çok yaygın işlemlerdir. Dizide araya eleman eklemek O(n) kaydırma gerektirirken, linked list'te sadece birkaç pointer değiştirmek yeterlidir.

Bellek fragmentasyonu: Büyük bir ardışık bellek bloğu ayırmak bazen mümkün olmayabilir. Linked list düğümleri küçük parçalar halinde farklı yerlere dağılabildiği için bu sorundan etkilenmez.

Diğer veri yapılarının temeli: Stack, queue, hash table'ın chaining mekanizması, graf'ların komşuluk listeleri, işletim sistemlerinin process queue'ları — bunların hepsi arka planda linked list kullanır. Linked list'i anlamadan bu yapıları gerçek anlamda kavramak mümkün değildir.

Node Yapısı ve Liste Oluşturma

C dilinde bağlı liste düğümü bir struct ile tanımlanır. Struct içinde veri alanı ve bir sonraki düğümü gösteren pointer bulunur. Bu yapı kendi kendini referans alan (self-referential) bir yapıdır çünkü struct'ın içinde yine aynı struct türünden bir pointer vardır.

node_yapisi.c
#include <stdio.h>
#include <stdlib.h>

// Düğüm yapısı
struct Node {
    int data;              // saklanan veri
    struct Node* next;     // sonraki düğümün adresi
};

// Yeni düğüm oluşturma fonksiyonu
struct Node* yeniDugum(int deger) {
    struct Node* dugum = (struct Node*)malloc(sizeof(struct Node));
    if (dugum == NULL) {
        printf("Bellek ayrılamadı!\n");
        exit(1);
    }
    dugum->data = deger;
    dugum->next = NULL;
    return dugum;
}
Tek Bir Düğümün Yapısı (Bellekte)
data42
next0xA4
data: saklanan değer | next: sonraki düğümün bellek adresi

Şimdi bu düğümleri birbirine bağlayarak bir liste oluşturalım:

liste_olusturma.c
int main() {
    // 3 düğüm oluştur
    struct Node* head   = yeniDugum(10);
    struct Node* ikinci = yeniDugum(20);
    struct Node* ucuncu = yeniDugum(30);

    // Düğümleri birbirine bağla
    head->next   = ikinci;   // 10 → 20
    ikinci->next = ucuncu;   // 20 → 30
    // ucuncu->next zaten NULL (yeniDugum'da atandı)

    // Sonuç: 10 → 20 → 30 → NULL
    return 0;
}
malloc neden gerekli? Dizilerde bellekte ne kadar yer ayrılacağı derleme zamanında bilinir. Bağlı listede ise her yeni düğüm çalışma zamanında (runtime) oluşturulur. malloc() fonksiyonu heap bellek bölgesinden istenen kadar byte ayırır ve başlangıç adresini döndürür. Bu sayede liste ihtiyaca göre büyüyüp küçülebilir.

typedef ile Temiz Kod

Her yerde struct Node yazmak zahmetlidir. typedef kullanarak daha temiz bir sözdizimi elde edebilirsiniz. Bu yazım biçimi özellikle büyük projelerde kodun okunabilirliğini artırır ve hata yapma olasılığını azaltır.

typedef_kullanimi.c
// typedef ile tanımlama
typedef struct Node {
    int data;
    struct Node* next;  // burada hâlâ struct Node yazmak zorunlu
} Node;

// Artık struct yazmadan kullanabilirsiniz
Node* head = NULL;
Node* yeni = (Node*)malloc(sizeof(Node));

// typedef olmadan aynı kod:
struct Node* head = NULL;
struct Node* yeni = (struct Node*)malloc(sizeof(struct Node));

Self-Referential Struct

Node struct'ının kendine referans vermesi (self-referential) ilk başta kafa karıştırıcı olabilir. struct Node içinde struct Node* yazmak, "bu struct türünden bir pointer tut" demektir. Struct'ın kendisini tutmak değil, sadece adresini tutmak söz konusudur. Bu fark çok önemlidir çünkü bir pointer her zaman sabit boyuttadır (64-bit sistemde 8 byte), ancak struct'ın kendisi değişken boyutta olabilir.

Her Düğümün Bellek Maliyeti

Bir int dizisinde her eleman sadece 4 byte yer kaplar. Ancak linked list'te her düğüm hem veriyi hem de bir pointer'ı taşır. 64-bit bir sistemde bu hesap şöyle olur:

AlanBoyutAçıklama
data (int)4 byteSaklanan veri
padding4 byteHizalama (alignment) için boşluk
next (pointer)8 byte64-bit sistemde pointer boyutu
Toplam16 byteDizide aynı veri 4 byte tutardı
4 kat daha fazla bellek! Tek bir int saklamak için linked list dizinin 4 katı bellek kullanır. 1 milyon elemanlı bir listede bu fark yaklaşık 12 MB'a karşılık gelir. Bu yüzden linked list'i "ekleme/silme hızlı diye" her yerde kullanmak doğru değildir. Kullanım senaryosuna göre doğru veri yapısını seçmek kritik öneme sahiptir.

Traverse İşlemi (Dolaşma)

Bağlı listede indeks yoktur. Bir elemana ulaşmak için head'den başlayıp next pointer'larını takip ederek ilerlemek zorundasınız. Bu işleme traverse denir. Geçici bir pointer (genellikle temp veya current) oluşturulur, head'den başlatılır ve her adımda temp = temp->next ile bir sonraki düğüme geçilir. NULL'a ulaşınca liste bitmiştir.

Traverse — temp pointer'ının ilerleyişi
Adım 1: temp = head
10
20
30
NULL
Adım 2: temp = temp->next
10
20
30
NULL
Adım 3: temp = temp->next
10
20
30
NULL
Adım 4: temp = NULL → DUR
traverse.c
void listeYazdir(struct Node* head) {
    struct Node* temp = head;

    while (temp != NULL) {
        printf("%d → ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

// Çıktı: 10 → 20 → 30 → NULL
Sonsuz döngü tehlikesi: Traverse sırasında temp = temp->next yazmayı unutursanız, pointer hep aynı düğümü gösterir ve döngü asla bitmez. Bu, bağlı listelerle çalışırken en sık yapılan hatalardan biridir.

Belirli Pozisyondaki Elemana Erişim

Dizide arr[3] yazmak O(1) sürede doğrudan 4. elemana erişir. Linked list'te böyle bir lüks yoktur. 4. elemana ulaşmak için head'den başlayıp 3 kez next'i takip etmeniz gerekir. Bu, bağlı listenin en büyük dezavantajıdır.

pozisyona_erisim.c
// n. pozisyondaki elemanın değerini döndür (0-indexed)
int pozisyondakiEleman(struct Node* head, int pozisyon) {
    struct Node* temp = head;
    int i = 0;

    while (temp != NULL) {
        if (i == pozisyon)
            return temp->data;
        temp = temp->next;
        i++;
    }

    printf("Geçersiz pozisyon!\n");
    return -1;
}

// Kullanım: 10 → 20 → 30 → 40 → NULL
pozisyondakiEleman(head, 0);  // 10 (1 adım)
pozisyondakiEleman(head, 2);  // 30 (3 adım)
pozisyondakiEleman(head, 5);  // -1 (geçersiz)
Random access yok: Linked list'te 1000. elemana erişmek ile 1. elemana erişmek arasında 1000 kat hız farkı vardır. Eğer uygulamanız sık sık belirli indekslere erişim gerektiriyorsa (örneğin bir for döngüsünde get(i) çağırmak), dizi her zaman daha iyi bir seçimdir. Linked list'in gücü erişimde değil, ekleme ve silmededir.

Başa Eleman Ekleme

Başa ekleme, bağlı listenin en hızlı ekleme operasyonudur: O(1). Yeni düğüm oluşturulur, next pointer'ı mevcut head'e yönlendirilir ve head güncellenir. Hiçbir eleman kaydırılmaz, sadece iki pointer işlemi yeterlidir. Dizideki O(n) maliyetiyle karşılaştırıldığında devasa bir fark.

Başa 5 Ekleme
1

Mevcut liste:

10
20
30
NULL
2

Yeni düğüm oluştur, next'ini head'e bağla:

5
10
20
30
NULL
3

head = yeni düğüm → Tamamlandı!

basa_ekleme.c
void basaEkle(struct Node** head, int deger) {
    struct Node* yeni = yeniDugum(deger);
    yeni->next = *head;   // yeni düğümü mevcut head'e bağla
    *head = yeni;          // head'i güncelle
}

// Kullanım:
struct Node* head = NULL;
basaEkle(&head, 30);  // 30 → NULL
basaEkle(&head, 20);  // 20 → 30 → NULL
basaEkle(&head, 10);  // 10 → 20 → 30 → NULL
Neden çift pointer (Node**)? C dilinde fonksiyon parametreleri değer kopyası (pass by value) olarak geçer. Eğer Node* geçirsek, fonksiyon içinde head'i değiştirmemiz dışarıya yansımaz. Node** (pointer'ın pointer'ı) kullanarak head'in kendisini değiştirebiliyoruz. Bu pattern bağlı liste fonksiyonlarının neredeyse tamamında kullanılır.

Sona Eleman Ekleme

Sona ekleme için listenin sonuna kadar gitmek gerekir çünkü tek yönlü bağlı listede son düğüme doğrudan erişim yoktur. Bu yüzden sona ekleme O(n) karmaşıklığındadır. Son düğüm bulunur, next pointer'ı yeni düğüme yönlendirilir.

Sona 40 Ekleme
1

Son düğümü bul (next == NULL olan):

10
20
30
NULL
2

Son düğümün next'ini yeni düğüme bağla:

10
20
30
40
NULL
sona_ekleme.c
void sonaEkle(struct Node** head, int deger) {
    struct Node* yeni = yeniDugum(deger);

    // Liste boşsa yeni düğüm head olur
    if (*head == NULL) {
        *head = yeni;
        return;
    }

    // Son düğümü bul
    struct Node* temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }

    temp->next = yeni;  // son düğümün next'ini yeni düğüme bağla
}

Tail Pointer ile O(1) Sona Ekleme

Sona ekleme her seferinde listenin sonuna kadar yürümek zorunda olması can sıkıcıdır. Ama basit bir optimizasyon ile bu O(n) maliyeti O(1)'e düşürebilirsiniz: listenin son düğümünü ayrı bir pointer'da (tail) tutmak. Her sona ekleme işleminde tail güncellenir ve bir sonraki ekleme doğrudan tail üzerinden yapılır.

tail_pointer.c
struct Node* head = NULL;
struct Node* tail = NULL;  // son düğümü takip eder

void sonaEkleTail(int deger) {
    struct Node* yeni = yeniDugum(deger);

    if (head == NULL) {
        head = yeni;
        tail = yeni;   // ilk eleman hem head hem tail
        return;
    }

    tail->next = yeni;  // O(1) — doğrudan sona bağla
    tail = yeni;         // tail'i güncelle
}

// Artık sona ekleme O(1)!
Trade-off: Tail pointer ekstra bir pointer tutmanın maliyetine karşılık sona eklemeyi O(n)'den O(1)'e düşürür. Ancak silme işlemlerinde tail'i güncel tutmayı unutmamak gerekir. Sondan silme işleminde tail'i güncellemek için yine sondan bir önceki düğüme ulaşmanız gerekir ki bu O(n)'dir. Bu sorunu çift yönlü bağlı liste çözer.

Araya Eleman Ekleme

Belirli bir pozisyona eleman eklemek için o pozisyonun bir önceki düğümüne ulaşmak gerekir. Önceki düğümün next pointer'ı yeni düğüme, yeni düğümün next pointer'ı ise eski sonraki düğüme yönlendirilir. Bu işlem pointer seviyesinde O(1)'dir ama pozisyona ulaşmak O(n) sürer.

Pozisyon 2'ye 25 Ekleme (0-indexed)
1

Pozisyon 1'deki düğümü bul (önceki):

10
20
30
NULL
2

Yeni düğümü araya bağla:

10
20
25
30
NULL
araya_ekleme.c
void pozisyonaEkle(struct Node** head, int pozisyon, int deger) {
    if (pozisyon == 0) {
        basaEkle(head, deger);
        return;
    }

    struct Node* yeni = yeniDugum(deger);
    struct Node* temp = *head;

    // Hedef pozisyonun bir öncesine git
    for (int i = 0; i < pozisyon - 1 && temp != NULL; i++) {
        temp = temp->next;
    }

    if (temp == NULL) {
        printf("Geçersiz pozisyon!\n");
        free(yeni);
        return;
    }

    yeni->next = temp->next;   // yeni düğümü sonrakine bağla
    temp->next = yeni;          // önceki düğümü yeniye bağla
}
Sıralama önemli! Araya ekleme sırasında önce yeni->next = temp->next yapılmalı, sonra temp->next = yeni. Bu sırayı tersine çevirirseniz temp->next yeni düğümü gösterir ve sonraki düğümlere olan bağlantı kaybolur. Bu hataya "link kaybı" denir ve listenin bir kısmının erişilemez hale gelmesine yol açar.

Sıralı Listeye Ekleme (Sorted Insert)

Eğer liste sıralı tutuluyorsa, yeni elemanı doğru pozisyona yerleştirmek gerekir. Bu işlem, kendisinden büyük ilk elemanın önüne ekleme yapmak demektir. Sıralı ekleme birçok algoritmada (insertion sort, priority queue) temel bir operasyondur.

sirali_ekleme.c
void siraliEkle(struct Node** head, int deger) {
    struct Node* yeni = yeniDugum(deger);

    // Boş liste veya başa ekleme durumu
    if (*head == NULL || (*head)->data >= deger) {
        yeni->next = *head;
        *head = yeni;
        return;
    }

    // Doğru pozisyonu bul
    struct Node* temp = *head;
    while (temp->next != NULL && temp->next->data < deger) {
        temp = temp->next;
    }

    yeni->next = temp->next;
    temp->next = yeni;
}

// Kullanım:
// Liste: 10 → 20 → 40 → NULL
siraliEkle(&head, 30);
// Sonuç: 10 → 20 → 30 → 40 → NULL (sıra korundu)

Baştan Silme

Baştan silme, başa ekleme gibi O(1) karmaşıklığındadır. Head pointer bir sonraki düğüme kaydırılır ve eski head'in belleği serbest bırakılır.

Baştan Silme
1

Silinecek düğümü kaydet:

10
20
30
NULL
2

head = head->next, eski düğümü free():

20
30
NULL
bastan_silme.c
void bastanSil(struct Node** head) {
    if (*head == NULL) {
        printf("Liste zaten boş!\n");
        return;
    }

    struct Node* silinecek = *head;
    *head = (*head)->next;   // head'i bir ileri kaydır
    free(silinecek);           // eski head'in belleğini serbest bırak
}

Sondan Silme

Sondan silme için son düğümün bir önceki düğümüne ulaşmak gerekir. Önceki düğümün next pointer'ı NULL yapılır ve son düğüm serbest bırakılır. Tek yönlü bağlı listede geriye gidemediğimiz için baştan ilerlememiz gerekir, bu da O(n) karmaşıklığı demektir.

Sondan Silme
1

Sondan bir önceki düğümü bul:

10
20
30
NULL
önceki: 20 (mavi) | silinecek: 30 (kırmızı)
2

Öncekinin next'ini NULL yap, son düğümü free():

10
20
NULL
sondan_silme.c
void sondanSil(struct Node** head) {
    if (*head == NULL) {
        printf("Liste zaten boş!\n");
        return;
    }

    // Tek eleman varsa
    if ((*head)->next == NULL) {
        free(*head);
        *head = NULL;
        return;
    }

    // Sondan bir önceki düğümü bul
    struct Node* temp = *head;
    while (temp->next->next != NULL) {
        temp = temp->next;
    }

    free(temp->next);     // son düğümü serbest bırak
    temp->next = NULL;    // yeni son düğümün next'ini NULL yap
}

Belirli Bir Elemanı Silme

Belirli bir değere sahip düğümü silmek için, o düğümün bir öncesini bulmak gerekir. Önceki düğümün next pointer'ı, silinecek düğümün next'ine yönlendirilir ve silinecek düğüm free() ile serbest bırakılır.

Değeri 20 Olan Düğümü Silme
1

Silinecek düğümü ve bir öncesini bul:

10
20
30
NULL
önceki: 10 (mavi) | silinecek: 20 (kırmızı)
2

Öncekini sonrakine bağla, silineni free():

10
30
NULL
belirli_eleman_silme.c
void degereSil(struct Node** head, int deger) {
    if (*head == NULL) return;

    // Silinecek düğüm head ise
    if ((*head)->data == deger) {
        struct Node* silinecek = *head;
        *head = (*head)->next;
        free(silinecek);
        return;
    }

    // Silinecek düğümün bir öncesini bul
    struct Node* temp = *head;
    while (temp->next != NULL && temp->next->data != deger) {
        temp = temp->next;
    }

    if (temp->next == NULL) {
        printf("%d listede bulunamadı!\n", deger);
        return;
    }

    struct Node* silinecek = temp->next;
    temp->next = silinecek->next;  // atlayarak bağla
    free(silinecek);
}

Eleman Arama

Bağlı listede eleman aramak, dizideki doğrusal aramaya benzer: head'den başlayıp her düğümün verisini kontrol edersiniz. İndeks ile doğrudan erişim mümkün olmadığı için en kötü durumda tüm listeyi dolaşmanız gerekir. Karmaşıklık O(n)'dir.

Değeri 30 Olan Düğümü Arama
10 ≠ 30 → devam
10
20
30
NULL
20 ≠ 30 → devam
10
20
30
NULL
30 = 30 ✓ Bulundu! (pozisyon: 2)
10
20
30
NULL
eleman_arama.c
// Değere göre arama — pozisyon döndürür
int elemanAra(struct Node* head, int aranan) {
    struct Node* temp = head;
    int pozisyon = 0;

    while (temp != NULL) {
        if (temp->data == aranan)
            return pozisyon;  // bulundu
        temp = temp->next;
        pozisyon++;
    }

    return -1;  // bulunamadı
}

// Recursive versiyon
int elemanAraRecursive(struct Node* head, int aranan) {
    if (head == NULL) return -1;
    if (head->data == aranan) return 0;

    int sonuc = elemanAraRecursive(head->next, aranan);
    if (sonuc == -1) return -1;
    return sonuc + 1;
}

// Kullanım:
int poz = elemanAra(head, 20);
// poz = 1 (0-indexed, 2. düğümde bulundu)

Liste Uzunluğu Bulma

Dizilerin aksine bağlı listelerde eleman sayısını doğrudan bilmenin bir yolu yoktur. Listenin uzunluğunu bulmak için head'den başlayıp NULL'a kadar ilerleyerek her düğümde bir sayaç artırılır.

uzunluk.c
// Iterative
int listeUzunlugu(struct Node* head) {
    int sayac = 0;
    struct Node* temp = head;

    while (temp != NULL) {
        sayac++;
        temp = temp->next;
    }

    return sayac;
}

// Recursive
int listeUzunluguRecursive(struct Node* head) {
    if (head == NULL) return 0;
    return 1 + listeUzunluguRecursive(head->next);
}

// Kullanım: 10 → 20 → 30 → NULL
printf("Uzunluk: %d\n", listeUzunlugu(head));  // 3
Pratik ipucu: Eğer sık sık liste uzunluğuna ihtiyaç duyuyorsanız, her ekleme ve silme işleminde bir sayaç değişkenini güncellemek çok daha verimlidir. Böylece uzunluk her zaman O(1)'de elde edilir. Birçok gerçek dünya uygulaması bu yaklaşımı kullanır.

Ortadaki Elemanı Bulma (Two Pointer Tekniği)

Listenin ortadaki elemanını bulmak klasik bir mülakat sorusudur. Naive yaklaşım: önce uzunluğu bul, sonra n/2'ye git (iki geçiş). Ancak "slow-fast pointer" tekniği ile tek geçişte bulabilirsiniz. Yavaş pointer (slow) her adımda bir düğüm ilerlerken, hızlı pointer (fast) iki düğüm ilerler. Fast listenin sonuna ulaştığında, slow tam ortada olur.

Slow-Fast Pointer ile Ortayı Bulma
Başlangıç: slow=head, fast=head
10
20
30
40
50
NULL
slow=10, fast=10
Adım 1: slow +1, fast +2
10
20
30
40
50
NULL
slow=20 (yeşil), fast=30 (mavi)
Adım 2: slow +1, fast +2 → fast sona ulaştı!
10
20
30
40
50
NULL
slow=30 → Ortadaki eleman!
ortadaki_eleman.c
int ortadakiEleman(struct Node* head) {
    if (head == NULL) return -1;

    struct Node* slow = head;
    struct Node* fast = head;

    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;          // 1 adım
        fast = fast->next->next;    // 2 adım
    }

    return slow->data;  // slow ortada
}

// 10 → 20 → 30 → 40 → 50 → NULL
// Ortadaki eleman: 30
Two pointer tekniği linked list problemlerinin olmazsa olmazıdır. Ortadaki elemanı bulmanın yanı sıra, döngü tespiti (Floyd's Cycle Detection), sondan n. elemanı bulma ve iki listenin kesişim noktasını bulma gibi problemlerde de aynı teknik kullanılır. Bu tekniği içselleştirmek birçok mülakat sorusunu çözmenizi kolaylaştırır.

Listeyi Tersine Çevirme

Listeyi tersine çevirmek, her düğümün next pointer'ını bir önceki düğüme yönlendirmek demektir. Bu klasik bir mülakat sorusudur ve iki farklı yaklaşımla çözülebilir: iterative (döngüsel) ve recursive (özyinelemeli).

Yöntem 1: Iterative (Döngüsel)

Üç pointer kullanılır: prev (önceki), current (mevcut) ve next (sonraki). Her adımda mevcut düğümün next pointer'ı önceki düğüme çevrilir, ardından üç pointer bir adım ileri kayar.

Iterative Tersine Çevirme — Adım Adım
0

Başlangıç: prev=NULL, curr=head

NULL←prev
10
20
30
NULL
curr = 10 (yeşil)
1

10'un yönünü çevir: next→ yerine ←prev

NULL
10
20
30
NULL
prev = 10 (turuncu) | curr = 20 (yeşil)
2

20'nin yönünü çevir:

NULL
10
20
30
NULL
prev = 20 (turuncu) | curr = 30 (yeşil)
3

30'un yönünü çevir, curr = NULL → DUR

NULL
10
20
30
head = prev = 30 → Yeni liste: 30 → 20 → 10 → NULL
tersine_cevirme_iterative.c
void tersineCevirIterative(struct Node** head) {
    struct Node* prev = NULL;
    struct Node* current = *head;
    struct Node* next = NULL;

    while (current != NULL) {
        next = current->next;       // sonrakini kaydet
        current->next = prev;       // yönü çevir
        prev = current;             // prev'i ilerlet
        current = next;             // current'ı ilerlet
    }

    *head = prev;  // yeni head eski son düğüm
}

// 10 → 20 → 30 → NULL
tersineCevirIterative(&head);
// 30 → 20 → 10 → NULL
Karmaşıklık: Zaman: O(n) — her düğüm tam bir kez ziyaret edilir. Alan: O(1) — sadece 3 pointer kullanılır, ekstra bellek gerekmez. Bu yüzden iterative yöntem pratikte daha çok tercih edilir.

Yöntem 2: Recursive (Özyinelemeli)

Recursive yaklaşım, listenin sonuna kadar gider ve geri dönerken her düğümün yönünü çevirir. Bu yöntem kodun daha zarif ve kısa olmasını sağlar ancak her fonksiyon çağrısı stack'te yer kapladığı için alan karmaşıklığı O(n)'dir.

Mantık şöyledir: önce listenin geri kalanını ters çevir, sonra mevcut düğümü ters çevrilmiş listenin sonuna ekle. Base case: tek elemanlı veya boş liste zaten tersine çevrilmiştir.

Recursive Tersine Çevirme — Çağrı Yığını

İleri gidiş (recursive çağrılar):

ters(10→20→30→NULL)
  ters(20→30→NULL)
    ters(30→NULL) ← base case, 30'u döndür

Geri dönüş (pointer çevirme):

    30→NULL → head=30
  20->next->next=20 → 30→20, 20->next=NULL
10->next->next=10 → 30→20→10, 10->next=NULL

tersine_cevirme_recursive.c
struct Node* tersineCevirRecursive(struct Node* head) {
    // Base case: boş liste veya tek eleman
    if (head == NULL || head->next == NULL) {
        return head;
    }

    // Listenin geri kalanını ters çevir
    struct Node* yeniHead = tersineCevirRecursive(head->next);

    // Mevcut düğümün sonrakisinin next'ini kendine çevir
    head->next->next = head;  // yönü ters çevir
    head->next = NULL;         // mevcut düğümü son düğüm yap

    return yeniHead;  // yeni head her zaman eski son düğüm
}

// Kullanım:
head = tersineCevirRecursive(head);
// 10 → 20 → 30 → NULL  ⟹  30 → 20 → 10 → NULL

Iterative vs Recursive Karşılaştırma

ÖzellikIterativeRecursive
Zaman karmaşıklığıO(n)O(n)
Alan karmaşıklığıO(1) — sadece 3 pointerO(n) — call stack
Kod okunabilirliğiOrta — 3 pointer yönetimiYüksek — kısa ve zarif
Uzun listelerde riskYokStack overflow riski
Debug kolaylığıKolay — adım adım takipZor — çağrı yığını karmaşık
Tavsiye edilen durumÜretim koduÖğrenme / mülakat
Stack overflow: Recursive yöntemde her fonksiyon çağrısı bellekte stack frame oluşturur. 100.000 düğümlü bir listede 100.000 recursive çağrı yapılır ve bu çoğu sistemde stack taşmasına yol açar. Bu yüzden üretim kodunda iterative yöntem tercih edilmelidir. Recursive versiyon ise kavramı anlamak ve mülakatlarda temiz çözüm sunmak için önemlidir.

Sık Yapılan Hatalar

1. Head Güncellemesini Unutmak

Başa ekleme veya baştan silme işlemlerinde head pointer'ını güncellemeyi unutmak en yaygın hatadır. Fonksiyon içinde head'i değiştirmek için çift pointer (Node**) geçmek zorunludur, aksi halde değişiklik fonksiyon dışına yansımaz.

head_hatasi.c
// ❌ YANLIŞ: head değişikliği dışarıya yansımaz
void basaEkleYanlis(struct Node* head, int deger) {
    struct Node* yeni = yeniDugum(deger);
    yeni->next = head;
    head = yeni;  // sadece lokal kopya değişir!
}

// ✅ DOĞRU: çift pointer ile head güncellenir
void basaEkleDoğru(struct Node** head, int deger) {
    struct Node* yeni = yeniDugum(deger);
    yeni->next = *head;
    *head = yeni;  // gerçek head güncellenir
}

2. free() Sonrası Pointer Kullanımı (Dangling Pointer)

Bir düğümü free() ile sildikten sonra, o düğüme ait pointer hâlâ eski adresini tutar. Bu pointer'ı kullanmaya devam etmek tanımsız davranışa yol açar. Silme işleminden sonra pointer'ı NULL'a eşitlemek iyi bir alışkanlıktır.

dangling_pointer.c
struct Node* temp = head;
free(temp);

// ❌ YANLIŞ: silinen belleğe erişim
printf("%d\n", temp->data);    // tanımsız davranış!
temp->next = NULL;             // tanımsız davranış!

// ✅ DOĞRU: free() sonrası pointer'ı NULL yap
free(temp);
temp = NULL;

3. Link Kaybı (Lost Node)

Araya ekleme veya silme işlemlerinde pointer'ları yanlış sırada değiştirmek, düğümler arasındaki bağlantının kopmasına yol açar. Kopan bağlantıdan sonraki düğümlere bir daha ulaşılamaz ve bu düğümlerin belleği de serbest bırakılamaz (memory leak).

link_kaybi.c
// Senaryo: A → B → C listesinde A ile B arasına X eklemek

// ❌ YANLIŞ: Önce A'yı X'e bağlarsan B kaybolur
A->next = X;       // A → X   (ama B'ye artık kimse bağlı değil!)
X->next = B;       // B'yi bulmak artık imkansız

// ✅ DOĞRU: Önce X'i B'ye bağla, sonra A'yı X'e
X->next = A->next; // X → B   (B güvende)
A->next = X;       // A → X → B (tamamlandı)

4. malloc Başarısızlığını Kontrol Etmemek

Sistem belleği dolduğunda malloc() NULL döndürür. Bu durumu kontrol etmeden devam etmek, NULL pointer üzerinde erişim denemesine ve programın çökmesine yol açar. Her malloc çağrısından sonra NULL kontrolü yapılmalıdır.

5. Boş Liste Kontrolü Yapmamak

Neredeyse her linked list fonksiyonunun başında "liste boş mu?" kontrolü yapılmalıdır. Boş listede (head == NULL) traverse, silme veya erişim yapmaya çalışmak çökmeye neden olur. Tek elemanlı liste de özel bir durumdur çünkü hem head hem son düğümdür.

edge_cases.c
// Her fonksiyonda kontrol edilmesi gereken durumlar:

// 1. Boş liste
if (head == NULL) { /* özel işlem */ }

// 2. Tek elemanlı liste
if (head->next == NULL) { /* özel işlem */ }

// 3. İşlem head düğümünü etkiliyor mu?
if (head->data == aranan) { /* head özel olarak güncellenmeli */ }

// 4. Son düğüm (sondan silme için)
if (temp->next->next == NULL) { /* sondan bir önceki */ }

Bağlı Listeler Nerelerde Kullanılır?

İşletim Sistemleri

İşletim sistemleri process (süreç) yönetimi için bağlı listeler kullanır. Çalışan süreçler bir listede tutulur ve zamanlayıcı (scheduler) bu listeden sırayla süreç seçer. Yeni bir süreç başlatıldığında listeye eklenir, sonlandığında listeden çıkarılır. Bellek yönetiminde boş bellek blokları da linked list ile takip edilir.

Web Tarayıcıları

Tarayıcıdaki ileri-geri navigasyon geçmişi aslında bir bağlı liste yapısıdır. Her sayfa bir düğümdür ve "geri" butonu önceki düğüme, "ileri" butonu sonraki düğüme geçer. Bu özellik çift yönlü bağlı liste ile daha verimli gerçeklenir ancak mantık tek yönlü listelerden başlar.

Müzik ve Medya Oynatıcılar

Çalma listeleri bağlı liste ile yönetilir. Şarkı ekleme, silme, sıra değiştirme, tekrarla (repeat) gibi operasyonlar linked list'in güçlü olduğu alanlardır. Döngüsel (circular) linked list kullanıldığında son şarkıdan otomatik olarak ilk şarkıya geçiş sağlanır.

Metin Editörleri

Bazı metin editörleri her satırı bir düğüm olarak bağlı listede saklar. Satır ekleme, silme ve araya satır sokmak böylece çok hızlı yapılır. Undo/redo mekanizması da genellikle bir linked list veya stack (linked list tabanlı) ile gerçeklenir.

Hash Table — Chaining

Hash tablolarında çarpışma (collision) durumunda aynı indekse düşen elemanlar linked list ile zincirlenir. Her hash bucket'ı aslında bir linked list'in head pointer'ıdır. Bu konuyu müfredatın hash table bölümünde detaylı göreceksiniz.

Bellek Yönetimi: free() Kullanımı

C dilinde malloc() ile ayırdığınız her bellek bloğunu işiniz bittiğinde free() ile serbest bırakmanız gerekir. Aksi takdirde bellek sızıntısı (memory leak) oluşur: program çalıştıkça bellek tüketimi artar ve sonunda sistem yavaşlar veya çöker. Listenin tamamını silmek için her düğümü tek tek free() etmelisiniz.

bellek_temizleme.c
void listeyiSil(struct Node** head) {
    struct Node* current = *head;
    struct Node* next;

    while (current != NULL) {
        next = current->next;   // sonrakini kaydet
        free(current);            // mevcut düğümü sil
        current = next;          // sonrakine geç
    }

    *head = NULL;  // head'i NULL yap (dangling pointer engelle)
}

// ❌ YANLIŞ: free() ettikten sonra erişim
free(temp);
printf("%d", temp->data);  // tanımsız davranış!

// ❌ YANLIŞ: Sonrakini kaydetmeden free()
free(current);
current = current->next;  // zaten silinen belleğe erişim!

Zaman Karmaşıklığı Özeti

İşlemLinked ListArrayKazanan
Başa eklemeO(1)O(n)Linked List
Sona eklemeO(n)O(1)Array
Araya eklemeO(n)*O(n)Linked List*
Baştan silmeO(1)O(n)Linked List
Sondan silmeO(n)O(1)Array
İndeks ile erişimO(n)O(1)Array
Eleman aramaO(n)O(n)Berabere
Tersine çevirmeO(n)O(n)Berabere
* Araya ekleme notu: Linked list'te düğüme zaten erişiminiz varsa ekleme O(1)'dir (sadece pointer değiştirme). Ancak pozisyona ulaşmak O(n) sürer. Dizide ise hem pozisyona gitmeniz hem de kaydırma yapmanız gerekir. Pratikte sık araya ekleme gereken senaryolarda linked list avantajlıdır.

Sonuç

Tek yönlü bağlı listeler, dizilerin sabit boyut ve yavaş ekleme/silme gibi sınırlamalarını çözmek için tasarlanmış temel bir veri yapısıdır. Dinamik bellek yönetimi, pointer manipülasyonu ve recursive düşünme gibi kavramları uygulamalı olarak öğretir. Bu konulara hakim olmak, sadece bağlı listeler için değil, ileride karşılaşacağınız ağaçlar, graflar ve daha karmaşık veri yapıları için de sağlam bir temel oluşturur.

Ancak tek yönlü bağlı listelerin de sınırlamaları vardır: geriye doğru gidemezsiniz, son düğümü silmek O(n) sürer ve her düğüm ekstra bir pointer alanı kaplar. Bu sınırlamaları aşmak için bir sonraki konumuzda çift yönlü bağlı listeleri (doubly linked list) inceleyeceğiz. Çift yönlü listelerde her düğüm hem ileri hem geri yönde bağlantıya sahiptir ve bu sayede birçok operasyon çok daha verimli hale gelir.

Bağlı Liste Ailesi

Tek yönlü bağlı liste, bağlı liste ailesinin en temel üyesidir. Müfredatın ilerleyen bölümlerinde karşılaşacağınız diğer üyeler şunlardır:

TürÖzellikAvantajı
Tek Yönlü (Singly)Her düğüm sadece ileriyi görürBasit, az bellek kullanır
Çift Yönlü (Doubly)Her düğüm hem ileri hem geri bağlantıya sahipSondan silme O(1), geri traverse
Dairesel (Circular)Son düğüm head'e geri bağlanırDöngüsel yapılar, round-robin
Çift Yönlü DaireselHem çift yönlü hem daireselEn esnek, her yöne O(1) hareket
Özetleyelim: Bu bölümde tek yönlü bağlı listenin yapısını, tüm temel operasyonlarını, bellek yönetimini ve sık yapılan hataları öğrendiniz. Pointer manipülasyonu ve dinamik bellek kavramlarına hakim olmak, C dilinde ileri seviye programlamanın kapısını açar. Bir sonraki bölümde çift yönlü bağlı listelerle bu temeli güçlendireceğiz.
← Veri Yapıları