6 - Bağlı Listede Ekleme, Silme ve Arama
2026
Geçen hafta bağlı listenin temel yapısını görmüştük. Bu hafta, var olan bir bağlı liste üzerinde işlem yapmaya odaklanacağız.
Bu dersin sonunda şunları yapabiliyor olmanız beklenir:
head’in değişip değişmeyeceğini yorumlamak,struct node ** kullanımının gerekli olduğunu ayırt etmek,Bu işlemleri tek yönlü bağlı liste üzerinde ele alacağız. Burada kurduğunuz mantık, daha sonra çift yönlü ve dairesel bağlı listeler için de temel oluşturur.
head, bağlı listenin ilk düğümünü gösterir.next alanı NULL olur.head == NULL ise liste boştur.Bu derste örnek başlangıç durumunu çoğu zaman şöyle düşüneceğiz:
head -> 1 -> 2 -> 3 -> NULL
Kod parçaları kavramı göstermek için sadeleştirilmiştir. Doğrudan derlemek istediğinizde uygun yerlerde stdio.h, stdlib.h ve main fonksiyonu eklemeyi unutmayın.
head’dir.NULL’a ulaşmaktır.Traversal, bağlı listedeki birçok işlemin temel kalıbıdır. Çünkü arama, sona ekleme ve sondan silme gibi işlemler çoğu zaman önce dolaşmayı gerektirir.
void printList(struct node *head) {
struct node *current = head;
printf("Liste: ");
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}Basit gezme işleminde liste yapısını değiştirmeyiz. Bu yüzden doğrudan head üzerinde ilerlemek yerine genellikle geçici bir işaretçi kullanırız.
Neden temp veya current kullanıyoruz?
head ilk düğümü göstermeye devam etmelidir. Eğer doğrudan head = head->next yaparsanız, listenin başlangıcını kaybedersiniz.
Tam dolaşım yapıldığında bütün düğümler tek tek ziyaret edildiği için maliyet genellikle \(O(n)\) olur.
Arama, traversal mantığının özel bir kullanımından ibarettir.
head’den başlanır.key ile karşılaştırılır.NULL döndürülür.struct node *search(struct node *head, int key) {
struct node *current = head;
while (current != NULL) {
if (current->data == key) {
return current;
}
current = current->next;
}
return NULL;
}Arama, istenen düğüm sonda olabilir diye en kötü durumda genellikle \(O(n)\) zaman alır.
Arama çoğu zaman tek başına yapılmaz; bir sonraki işleme hazırlık için yapılır.
struct node *found = search(head, 2);
if (found != NULL) {
printf("Dugum bulundu: %d\n", found->data);
}Buradaki önemli fikir şudur:
insertAfter veya deleteAfter gibi işlemlerde kullanılabilir.Ekleme işlemlerinde temel düşünce aynıdır:
head’in bağlantısını güncelle.Ekleme işlemine başlamadan önce şu üç noktayı düşünmek gerekir:
head değerini değiştirebilir mi?Burada en kritik nokta şudur:
Bağlantıyı yanlış sırada değiştirirseniz listenin bir kısmını kaybedebilirsiniz.
Başa ekleme, bağlı listede en sezgisel ve en verimli işlemlerden biridir.
newNode->next, eski head’i gösterecek şekilde ayarlanır.head, yeni düğümü gösterecek şekilde güncellenir.Bu işlemde listenin geri kalanını aramamız gerekmez.
void insertAtBeginning(struct node **headRef, int newData) {
struct node *newNode = malloc(sizeof *newNode);
if (newNode == NULL) {
printf("Bellek ayirma basarisiz.\n");
return;
}
newNode->data = newData;
newNode->next = *headRef;
*headRef = newNode;
}struct node **headRef Kullanıyoruz?Başa ekleme ve baştan silme gibi işlemler, head değişkeninin kendisini değiştirebilir. Bu nedenle fonksiyona yalnızca head’in değeri değil, head’in adresi gönderilir.
headRef: head değişkeninin adresi*headRef: gerçek head değeriFonksiyon içinde *headRef = newNode; dediğimizde, çağıran koddaki gerçek head güncellenmiş olur.
Temel ayrım
Sadece mevcut bir düğümün sonrasını değiştiriyorsak çoğu zaman struct node ** gerekmez. Ama head değişebiliyorsa genellikle gerekir.
Yeni düğümü listenin en sonuna eklemek için önce son düğümü bulmamız gerekir.
next alanı NULL yapılır.head olur.next alanı yeni düğümü gösterecek şekilde ayarlanır.void insertAtEnd(struct node **headRef, int newData) {
struct node *newNode = malloc(sizeof *newNode);
if (newNode == NULL) {
printf("Bellek ayirma basarisiz.\n");
return;
}
newNode->data = newData;
newNode->next = NULL;
if (*headRef == NULL) {
*headRef = newNode;
return;
}
struct node *last = *headRef;
while (last->next != NULL) {
last = last->next;
}
last->next = newNode;
}tail tutulmuyorsa son düğümü aramayı gerektirir; bu yüzden çoğu zaman \(O(n)\)’dir.Bu karşılaştırma, bağlı listede neden bazen tail tutulduğunu da açıklar.
Bu işlem aslında “belirli bir düğümden sonra ekleme” işlemidir. Dolayısıyla kritik bilgi, ekleme yapılacak yerin önceki düğümünü bilmektir.
next alanı, önceki düğümün eski next değerine bağlanır.next alanı yeni düğüme çevrilir.Sıra önemlidir: önce yeni düğümün bağlantısı kurulur, sonra eski düğüm yeni düğüme bağlanır.
Önemli nüans: prevNode zaten biliniyorsa bu bağlantı güncellemesi genellikle \(O(1)\)’dir. Ama önce uygun düğümü aramak gerekiyorsa toplam maliyet çoğu zaman \(O(n)\) olur.
void insertAfter(struct node *prevNode, int newData) {
if (prevNode == NULL) {
printf("Onceki dugum NULL olamaz.\n");
return;
}
struct node *newNode = malloc(sizeof *newNode);
if (newNode == NULL) {
printf("Bellek ayirma basarisiz.\n");
return;
}
newNode->data = newData;
newNode->next = prevNode->next;
prevNode->next = newNode;
}insertAfterBaşlangıç durumu:
found = search(head, 2) ise found, değeri 2 olan düğümü gösterir.
insertAfter(found, 99) sonrasında:
Burada yapılan şey yeni düğümü 2 ile 3 arasına eklemektir. Dikkat edilmesi gereken asıl nokta, 2 düğümünün eski next bilgisinin kaybedilmeden önce 99 düğümüne aktarılmasıdır.
Silme, eklemeden biraz daha risklidir; çünkü yanlış sırada işlem yaparsanız hem düğümü hem de bağlantıyı kaybedebilirsiniz.
Silmede tipik düşünce şudur:
free ile serbest bırak.Silme işlemine başlamadan önce yine şu noktaları kontrol etmek gerekir:
head değerini değiştirebilir mi?Kritik kural
free ettikten sonra o düğüme tekrar erişmeye çalışmayın. O bellek artık programınıza ait değildir.
Baştaki düğümü silmek için head değişeceği için yine işaretçinin işaretçisine ihtiyaç duyarız.
head, bir sonraki düğümü gösterecek şekilde güncellenir.free edilir.Önce:
head -> 1 -> 2 -> 3 -> NULL
temp = head
head = head->next
free(temp)
Sonra:
head -> 2 -> 3 -> NULLBurada kritik nokta şudur: önce yeni head belirlenir, sonra eski baş düğüm serbest bırakılır.
Tek yönlü listede son düğümü silmek için son düğümün bir öncesine kadar gitmemiz gerekir.
head = NULL yap.next alanını NULL yap.free et.void deleteFromEnd(struct node **headRef) {
if (*headRef == NULL) {
return;
}
if ((*headRef)->next == NULL) {
free(*headRef);
*headRef = NULL;
return;
}
struct node *secondLast = *headRef;
while (secondLast->next->next != NULL) {
secondLast = secondLast->next;
}
struct node *temp = secondLast->next;
secondLast->next = NULL;
free(temp);
}Önce:
head -> 1 -> 2 -> 3 -> NULL
secondLast, 2 düğümünü gösterene kadar ilerler
temp = 3 düğümü
secondLast->next = NULL
free(temp)
Sonra:
head -> 1 -> 2 -> NULLNeden son düğümü değil, bir önceki düğümü buluyoruz?
Tek yönlü bağlı listede her düğüm yalnızca kendisinden sonrakini bilir. Bu yüzden son düğümü listeden koparmak için, onun öncesindeki düğüme ulaşmamız gerekir.
Tek yönlü listede aradan silme işlemi de çoğu zaman silinecek düğümün önceki düğümünü bilmeyi gerektirir.
Bu yüzden aşağıdaki fonksiyonun adı özellikle deleteAfter seçildi; çünkü fonksiyon doğrudan verilen düğümden sonraki düğümü siler.
Önemli nüans: önceki düğüm zaten elinizdeyse silme bağlantısı genellikle \(O(1)\)’dir. Ama o düğümü bulmak için listeyi dolaşmanız gerekiyorsa toplam maliyet çoğu zaman \(O(n)\) olur.
deleteAfterBaşlangıç durumu:
found = search(head, 2) ise found, değeri 2 olan düğümü gösterir.
deleteAfter(found) sonrasında:
Bu işlemde 2 düğümünün next alanı, doğrudan 3 düğümünü gösterecek şekilde güncellenir; sonra aradaki düğüm free edilir.
Bazen insertAfter veya deleteAfter gibi işlemlerden önce uygun düğümü bulmak için arama yaparız.
insertAtBeginning(&head, 4);
insertAtEnd(&head, 5);
struct node *found = search(head, 2);
if (found != NULL) {
insertAfter(found, 6);
}
found = search(head, 2);
if (found != NULL) {
deleteAfter(found);
}
deleteFromBeginning(&head);
deleteFromEnd(&head);Bu örnekler, iki tür parametre farkını netleştirir:
&head: fonksiyon head’i değiştirebilir.head veya found: fonksiyon mevcut düğümler üzerinden işlem yapar.| İşlem | Maliyet | Neden? |
|---|---|---|
| Gezme | \(O(n)\) | Tüm düğümler sırayla gezilebilir |
| Arama | \(O(n)\) | İstenen düğüm sonda olabilir |
| Başa ekleme | \(O(1)\) | Doğrudan head üzerinden yapılır |
| Sona ekleme | \(O(n)\) | tail yoksa son düğüm aranır |
| Baştan silme | \(O(1)\) | Sadece head güncellenir |
| Sondan silme | \(O(n)\) | Sondan bir önceki düğüm aranır |
insertAfter / deleteAfter |
\(O(1)\) veya \(O(n)\) | Önceki düğüm biliniyorsa \(O(1)\), aranıyorsa toplam maliyet artar |
Bu tablo, bağlı listede doğrudan erişim olmamasının doğal sonucudur.
malloc sonucunu kontrol etmeden alanı kullanmakhead->next erişimi yapmakhead değişmesi gereken yerde sadece head kopyasıyla işlem yapmakfree edilen düğümü tekrar kullanmakhead’i değiştiriyorsa çoğu zaman struct node ** gerekir.Bağlı liste sorularında asıl başarı, kodu ezberlemekten çok “hangi bağlantı şimdi değişecek?” sorusunu doğru cevaplamaktır.