Ders 02 / 26
Dinamik Diziler
Kapasite büyütme, büyütme çarpanının seçimi, amortize maliyet çözümlemesi ve küçülme eşiği.
İçindekiler
Önceki ders, dizinin sabit boyutlu bir bitişik blok olduğunu ve bu bloğun sonradan büyütülemediğini kurdu. Eleman sayısı önceden bilinmediğinde bu kısıt bağlayıcıdır.
Çözüm, kısıtı ortadan kaldırmak değil, onunla yaşamaktır: dizi dolduğunda daha büyük yeni bir dizi ayrılır, eski elemanlar oraya kopyalanır ve eski blok bırakılır. Bu yapıya dinamik dizi (dynamic array) denir.
Kapasite ve Uzunluk
Dinamik dizi iki sayı tutar:
- Uzunluk: İçindeki gerçek eleman sayısı.
- Kapasite: Ayrılmış bloğa sığabilecek en fazla eleman sayısı.
Kapasite her zaman uzunluğa eşit veya ondan büyüktür. Aradaki fark, henüz kullanılmayan ama ayrılmış olan yerdir; sona ekleme bu boşluğu kullandığı sürece ucuzdur.
Uzunluk kapasiteye ulaştığında yeniden ayırma gerekir: yeni ve daha büyük bir blok ayrılır, tüm elemanlar kopyalanır. Kopyalama, eleman sayısıyla orantılıdır — yani .
Büyütme Çarpanı
Kritik soru şudur: yeni kapasite ne kadar olmalıdır?
Sabit miktarda büyütme — her seferinde kapasiteye sabit bir sayı eklemek — kötü bir seçimdir. Kapasite birer birer artırılsaydı, eleman eklemek için yapılan toplam kopyalama şu olurdu:
Bu, demektir: bin eleman eklemek yaklaşık yarım milyon kopyalama yapar.
Katlayarak büyütme — kapasiteyi her defasında bir çarpanla (yaygın olarak iki ile) çarpmak — toplam maliyeti kökten değiştirir. Kapasite ’den başlayıp ikiye katlanıyorsa, elemana ulaşana kadar yapılan kopyalamaların toplamı:
Geometrik toplamın kendisi, son terimin iki katından küçüktür. Yani ekleme için toplam kopyalama sayısı ’den azdır; ekleme başına düşen ortalama maliyet sabittir.
Amortize Maliyet
Bu gözlem bir kavramı gerektirir. Tek tek bakıldığında sona ekleme işlemlerinin çoğu , bazıları ’dir; en kötü durum maliyeti doğrusaldır.
Ancak pahalı işlemler seyrektir ve her pahalı işlemden sonra kapasite iki katına çıktığı için bir sonraki pahalı işleme kadar iki kat daha fazla ucuz işlem yapılır. Bir işlem dizisinin toplam maliyetini işlem sayısına bölerek elde edilen değere amortize maliyet denir.
Dinamik dizide sona eklemenin amortize maliyeti ’dir. Bu, “her ekleme sabit sürer” demek değildir; “ ekleme toplamda sürer” demektir. Ayrım, gecikmeye duyarlı sistemlerde önem kazanır: tek bir eklemenin uzun sürebileceği bilinmelidir.
class DinamikDizi: """Kapasitesini ikiye katlayarak büyüyen dizi.""" def __init__(self) -> None: self._blok: list = [None] # kapasite 1 ile başlar self._uzunluk = 0 self.kopyalama = 0 # ölçüm için sayaç def __len__(self) -> int: return self._uzunluk def kapasite(self) -> int: return len(self._blok) def ekle(self, deger: int) -> None: if self._uzunluk == self.kapasite(): self._buyut() self._blok[self._uzunluk] = deger self._uzunluk += 1 def _buyut(self) -> None: yeni = [None] * (self.kapasite() * 2) for i in range(self._uzunluk): # her eleman yeni bloğa taşınır yeni[i] = self._blok[i] self.kopyalama += 1 self._blok = yeni def __getitem__(self, i: int) -> int: if not 0 <= i < self._uzunluk: raise IndexError("dizin aralık dışında") return self._blok[i] dizi = DinamikDizi() for deger in range(16): dizi.ekle(deger) print(len(dizi), dizi.kapasite()) # 16 16 print(dizi.kopyalama) # 15 — toplam kopyalama, eleman sayısından az print(dizi[3]) # 3
On altı ekleme için yapılan kopyalama sayısı ’tir. Ekleme başına ortalama bir kopyalamadan az; geometrik toplamın verdiği sınır sayımla doğrulanmıştır.
Bu sayım, amortize çözümlemenin en sade biçimidir: her ekleme kendi maliyetinin yanında ileride yapılacak kopyalama için bir pay biriktirir; yeniden ayırma geldiğinde bu birikmiş paylar maliyeti karşılar.
Çarpanın iki olması zorunlu değildir. Daha küçük bir çarpan (örneğin ) daha az boş yer bırakır ama daha sık kopyalar; daha büyük çarpan tersini yapar. Her iki durumda da amortize maliyet sabit kalır — değişen, sabit çarpanın büyüklüğüdür.
Önceden Ayırma
Eleman sayısı önceden biliniyorsa, büyüme hiç yaşanmayabilir. Çoğu dinamik dizi gerçekleştirimi, kapasiteyi baştan belirlemeye izin veren bir işlem sunar.
Kazanç iki yönlüdür. Yeniden ayırma ve kopyalama tümüyle ortadan kalkar; ve ayrılan blok tek parça olduğu için bellek parçalanması azalır. Bin elemanlık bir sonucun üretileceği biliniyorsa, kapasitenin baştan bin verilmesi on yeniden ayırmayı önler.
sonuc = [] # Kapasite baştan bilinen durumda, dilin sunduğu ön ayırma kullanılır. # Ön ayırma yoksa, sonucu doğrudan hedef boyutta üretmek aynı işi görür: hazir = [0] * 1000 # tek ayırma, kopyalama yok for i in range(1000): hazir[i] = i * 3 print(len(hazir), hazir[999]) # 1000 2997
Bu, erken eniyileme değildir: son boyutun bilindiği durumlarda ön ayırma, kodun okunabilirliğini de artırır — niyet açıkça yazılmış olur.
Küçülme ve Salınım
Elemanlar silindiğinde kapasitenin de düşürülmesi beklenir; aksi hâlde bir kez büyüyen dizi belleği bırakmaz.
Küçültme eşiği dikkatle seçilmelidir. Kapasite, uzunluk kapasitenin yarısına indiğinde yarıya düşürülürse salınım oluşur: dizi tam sınırdayken art arda yapılan bir ekleme ve bir silme, her seferinde yeniden ayırma tetikler. Her işlem olur ve amortize kazanç yok olur.
Standart çözüm, eşikleri ayırmaktır: kapasite dolduğunda ikiye katlanır, ancak yalnızca uzunluk kapasitenin dörtte birine indiğinde yarıya düşürülür. Aradaki boşluk, sınırda gidip gelen işlemlerin yeniden ayırma tetiklemesini engeller.
Gerçek Dillerdeki Karşılığı
Çoğu dilin standart “liste” veya “vektör” yapısı, bir dinamik dizidir. Sona ekleme amortize sabit, başa ekleme doğrusaldır; bu asimetri, kod yazarken hangi ucun kullanılacağını belirler.
Bir ayrıntı, elemanların ne saklandığıdır. Sabit genişlikli tipler doğrudan blokta tutulabilir; nesne referansları saklandığında ise blok işaretçiler taşır ve gerçek değerler öbekte dağınık durur. İkinci düzen esneklik verir, önbellek uyumunu düşürür. Bilgisayarlar Nasıl Çalışır kursundaki bitişik yerleşim tartışması bu ayrımı içeriyordu.
Maliyet Tablosu
| Yapı | Erişim | Arama | Başa ekleme | Sona ekleme | Ortadan silme |
|---|---|---|---|---|---|
| Dizi (sabit boyut) | * | ||||
| Dinamik dizi | amortize |
* Yer kaldığı sürece.
Özet
- Dinamik dizi, dolduğunda daha büyük bir blok ayırıp elemanları kopyalayarak sabit boyut kısıtını aşar.
- Kapasite ayrılmış yeri, uzunluk gerçek eleman sayısını gösterir.
- Sabit miktarda büyütme toplam maliyeti yapar; katlayarak büyütmede toplam kopyalama ’den azdır.
- Amortize maliyet, bir işlem dizisinin toplam maliyetinin işlem sayısına bölünmesidir; sona eklemenin amortize maliyeti sabittir, tek bir eklemeninki değildir.
- Küçültme eşiği büyütme eşiğinden ayrı seçilir; aksi hâlde sınırdaki işlemler salınım üretir.
Sonraki Adım
Dinamik dizi, sona eklemeyi ucuzlattı ama başa ve ortaya ekleme hâlâ doğrusal. Kaydırma maliyetinin kaynağı bitişik yerleşimdi. Sonraki ders, bitişikliği tümüyle terk eden bir yapıyı — bağlı listeyi — ve bu değiş tokuşun neyi kazanıp neyi kaybettirdiğini ele alacak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.