Ders 03 / 26
Bağlı Listeler
Düğüm ve bağ yapısı, tek ve çift yönlü listeler, işaretçi yeniden bağlamanın maliyeti ve pratikteki sınırları.
İçindekiler
Dizinin ekleme ve silme maliyeti, tek bir gereklilikten doğuyordu: elemanların bitişik durması. Bitişiklik terk edilirse kaydırma da ortadan kalkar.
Bağlı liste (linked list) bu değiş tokuşu yapar. Elemanlar bellekte istedikleri yerde durur; sıra, her elemanın bir sonrakinin adresini taşımasıyla kurulur.
Düğüm ve Bağ
Yapının birimi düğümdür (node): bir değer ve bir sonraki düğümün adresi.
baş → [12 | •] → [18 | •] → [7 | •] → [25 | ⏚]
Son düğümün bağı boştur; listenin sonunu bu boşluk belirtir. Listenin kendisi tek bir şeyle temsil edilir: ilk düğümün adresi, yani baş.
Adres aritmetiği burada geçersizdir. Üçüncü elemanın nerede olduğu hesaplanamaz; baştan başlanıp iki bağ izlenmelidir. Bu, dizinin sabit zamanlı erişimini kaybetmek demektir: bağlı listede ’nci elemana erişim ’dir.
class Dugum: """Bir değer ve bir sonraki düğümün bağı.""" def __init__(self, deger: int) -> None: self.deger = deger self.sonraki: "Dugum | None" = None class BagliListe: def __init__(self) -> None: self.bas: Dugum | None = None self.uzunluk = 0 def basa_ekle(self, deger: int) -> None: """Yeni düğümü başa bağlar: sabit zamanlı.""" yeni = Dugum(deger) yeni.sonraki = self.bas self.bas = yeni self.uzunluk += 1 def bul(self, deger: int) -> int: """Değerin sırasını döndürür; yoksa -1. İzlenen bağ sayısı kadar adım atar.""" sira = 0 dugum = self.bas while dugum is not None: if dugum.deger == deger: return sira dugum = dugum.sonraki sira += 1 return -1 def degerler(self) -> list[int]: sonuc = [] dugum = self.bas while dugum is not None: sonuc.append(dugum.deger) dugum = dugum.sonraki return sonuc liste = BagliListe() for olcum in (25, 7, 18, 12): # ters sırada eklenir liste.basa_ekle(olcum) print(liste.degerler()) # [12, 18, 7, 25] print(liste.bul(7), liste.bul(99)) # 2 -1 print(liste.uzunluk) # 4
Başa ekleme sabit zamanlıdır: iki bağ yazılır, hiçbir eleman taşınmaz. Dizide aynı işlem tüm elemanları kaydırıyordu.
Konum Bulmak ile Bağlamak
Bağlı listenin en sık yanlış anlaşılan yanı şudur: ekleme ve silme ucuzdur, ancak yerini bulmak pahalıdır.
Elde bir düğüm referansı varsa, ondan sonrasına eklemek birkaç işaretçi yazımıdır — . Ancak “onuncu elemandan sonraya ekle” denirse, önce dokuz bağ izlenir; toplam maliyet yine olur.
Bu ayrım, bağlı listenin doğru kullanım alanını belirler: ilgili düğümün referansı zaten elde tutuluyorsa yapı kazanç sağlar; her işlemde baştan aranıyorsa sağlamaz.
Çift Yönlü Liste
Tek yönlü listede bir düğümü silmek, önceki düğümün bağını değiştirmeyi gerektirir; oysa düğümden geriye gidilemez. Çözüm, her düğüme bir de geri bağ eklemektir.
Çift yönlü listede her düğüm hem sonrakini hem öncekini bilir. Sonuçları:
- Elde bir düğüm referansı varken silme olur.
- Liste iki yönde gezilebilir.
- Her düğüm bir işaretçi daha taşır; bellek ek yükü artar.
- Her ekleme ve silmede iki yerine dört bağ güncellenir; kod daha hatalıdır.
Baş ve son işaretçilerinin ikisi birden tutulursa, listenin her iki ucuna ekleme sabit zamanlı olur. Bu, sonraki iki dersteki yığıt ve kuyruk yapılarının doğal temelidir.
Üçüncü bir varyant, son düğümün başa bağlandığı dairesel listedir; sıradaki öğeye sürekli dönen zamanlama ve tampon uygulamalarında kullanılır.
Sınır Düğümü
Bağlı liste kodunun hata kaynağı, uç durumlardır: boş listeye ekleme, ilk düğümü silme,
son düğümden sonrasına ekleme. Her biri ayrı bir if bloğu gerektirir ve bu bloklardan
biri unutulduğunda hata, yalnızca o durumda ortaya çıkar.
Sınır düğümü (sentinel), değer taşımayan sahte bir düğümdür ve listenin başına kalıcı olarak yerleştirilir. Baş her zaman var olduğundan, “ilk düğümü silmek” ayrı bir durum olmaktan çıkar — her düğümün bir öncesi vardır.
class SinirliListe: """Baştaki sınır düğümü sayesinde silme tek bir kodla yürür.""" def __init__(self) -> None: self.sinir = Dugum(0) # değeri kullanılmaz def ekle(self, deger: int) -> None: yeni = Dugum(deger) yeni.sonraki = self.sinir.sonraki self.sinir.sonraki = yeni def sil(self, deger: int) -> bool: onceki = self.sinir # her zaman geçerli bir "önceki" var while onceki.sonraki is not None: if onceki.sonraki.deger == deger: onceki.sonraki = onceki.sonraki.sonraki return True onceki = onceki.sonraki return False liste = SinirliListe() for o in (7, 18, 12): liste.ekle(o) print(liste.sil(12), liste.sil(99)) # True False
Aynı teknik, dairesel çift yönlü listelerde de kullanılır ve kodun uç durum sayısını belirgin biçimde azaltır.
Bellek Ek Yükü ve Önbellek
Bağlı listenin ilan edilen üstünlükleri, pratikte iki maliyetle sınırlanır.
Ek yer. Her düğüm, değerin yanında bir veya iki işaretçi taşır. Sekiz baytlık işaretçilerle, dört baytlık tam sayılardan oluşan tek yönlü bir listede yapı verisi, gerçek veriden fazladır. Diziyle karşılaştırıldığında bellek kullanımı kat düzeyinde artabilir.
Yerellik. Düğümler öbekte dağınık durur. Bir düğümden diğerine geçmek, bellekte öngörülemeyen bir adrese atlamaktır; önbellek satırı getirmenin kazancı yoktur ve önden getirme çalışmaz. Bilgisayarlar Nasıl Çalışır kursunun önbellek dersinde hesaplanan fark burada tersine işler: dizi taraması satır başına birçok eleman kullanırken, liste taraması her düğüm için ayrı bir ıska riski taşır.
Sonuç şudur: kâğıt üzerinde olan bağlı liste eklemesi, gerçek ölçümlerde olan dizi eklemesinden yavaş kalabilir — özellikle eleman sayısı küçük veya orta ölçekteyken. Karmaşıklık sınıfı, sabit çarpanları görmezden gelir; donanım görmezden gelmez.
Ne Zaman Bağlı Liste
Yapının meşru kullanım alanları dardır ama gerçektir:
- Düğüm referansı elde tutuluyorsa. En bilinen örnek, bir karma tablosuyla birlikte kullanılan çift yönlü listedir: tablo düğümü doğrudan verir, liste sıralamayı sabit zamanda günceller. En az kullanılanı çıkaran önbellek tasarımı bu ikilinin üzerine kurulur.
- Yeniden ayırma kabul edilemezse. Dinamik dizinin büyümesi, tek bir işlemi uzun sürdürür; gecikme sınırı olan sistemlerde bu kabul edilmeyebilir.
- Referans kararlılığı gerekiyorsa. Bağlı listede bir düğümün adresi, listeye yeni elemanlar eklendiğinde değişmez; dinamik dizide yeniden ayırma tüm adresleri geçersizleştirir.
- Listelerin birleştirilmesi sıksa. İki listeyi uç uca eklemek, bağ yazmaktan ibarettir.
Bunların dışında, varsayılan seçim dizidir.
Maliyet Tablosu
| Yapı | Erişim | Arama | Başa ekleme | Sona ekleme | Silme (düğüm elde) |
|---|---|---|---|---|---|
| Dizi (sabit boyut) | * | ||||
| Dinamik dizi | amortize | ||||
| Tek yönlü bağlı liste | ** | ||||
| Çift yönlü bağlı liste | ** |
* Yer kaldığı sürece. ** Son işaretçisi tutuluyorsa .
Özet
- Bağlı listede elemanlar bitişik durmaz; sıra, her düğümün bir sonrakinin adresini taşımasıyla kurulur.
- Adres aritmetiği geçersizdir; ’nci elemana erişim doğrusal zamanlıdır.
- Ekleme ve silme, işaretçi yazmaktan ibaret olduğu için ucuzdur; pahalı olan, konumu bulmaktır.
- Çift yönlü listede geri bağ, elde düğüm varken silmeyi sabit zamanlı yapar; karşılığı ek bellek ve daha karmaşık güncellemedir.
- İşaretçi ek yükü ve dağınık yerleşim, listenin pratik başarımını kuramsal maliyetinin altına çekebilir.
- Yapı, düğüm referansının elde tutulduğu, yeniden ayırmanın kabul edilmediği veya referans kararlılığının gerektiği durumlarda seçilir.
Sonraki Adım
Sonraki iki ders, buraya kadar tanımlanan yapıların üzerine kurulan iki soyut veri tipini ele alacak. İlki, yalnızca son eklenen elemana erişime izin veren yığıttır; fonksiyon çağrılarının nasıl yönetildiğini önceki kurstan hatırlayacaksınız.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.