Uzay Karmaşıklığı: Algoritmalar Belleği Nasıl Kullanır?
Asimptotik gösterimle uzay karmaşıklığının nasıl hesaplanacağını, özyineleme, veri yapıları ve yardımcı alan gibi bellek bileşenlerinin nasıl toplandığını ve yerinde tekniklerle alanın nasıl azaltılacağını öğrenin.
Uzay karmaşıklığı, girdi boyutu büyüdükçe algoritmaların bellek kullanımını ölçer. Bu da, kısıtlı ortamlarda kaynak yönetimi için uzay karmaşıklığını anlamayı önemli kılar.
Bu yazıda, uzay karmaşıklığının ne olduğunu, nasıl hesaplanacağını ve nasıl en aza indirileceğini ayrıntılı biçimde açıklayacağım. Ayrıca, gerçek dünya uygulamalarını ele alacak ve kuramsal temeller sunacağım.
Uzay Karmaşıklığını Anlamak
Hesaplamalara ve optimizasyonlara dalmadan önce, uzay karmaşıklığının gerçekte ne anlama geldiğini ve neden önemsemeniz gerektiğini netleştirelim.
Uzay karmaşıklığı nedir?
Uzay karmaşıklığı, algoritmanın girdi boyutuna göre ihtiyaç duyduğu toplam bellek miktarıdır. Sistem ölçeklenebilirliği için en kötü durum bellek ayak izini analiz ederken önemlidir; toplam alan girdiyi, yardımcı alanı ve ek yükü içerir; yardımcı alan ise girdi ve çıktıyı hariç tutan ekstra bellektir. Örneğin, bir diziyi sıralamayı ele alın: Toplam alan dizinin kendisini içerirken, yardımcı alan birleştime sırasında kullanılan geçici alan (merge sort’ta temp alanı) olabilir.
Uzay karmaşıklığı neden önemlidir
Modern bilişimde, alan verimliliği çoğu zaman kritiktir. Uzay karmaşıklığını optimize ederek, algoritmalarınızın sınırlı RAM’e sahip cihazlarda (IoT’de gömülü sistemler veya akıllı telefonlar gibi) ya da RAM’in her bitinin önemli olduğu, kaynak talebi yüksek işlemleri barındıran büyük sistemlerde çalışmasını sağlarsınız.
Örneğin mobil uygulamaları düşünün: uzay karmaşıklığını optimize etmek, görüntü işleme yapılırken çöküşleri önlemeye yardımcı olur. Ya da depolama maliyetlerinden tasarruf için bulut bilişimi, veriyi tam yüklemeden gerçek zamanlı analiz elde etmeyi ve akış verilerini düşünün.
Asimptotik Analiz ve Uzay için Big O Gösterimi
Bu bölümde, Big O gibi asimptotik gösterimlerin alan artışını nasıl betimlediğini açıklayacak ve bellek kullanımını sınırlamak ve karşılaştırmak için araçlar sunacağım.
Big O gösterimine giriş
Matematiksel olarak, f(n) fonksiyonu için bir c ve k sabiti varsa ve tüm k>n için 0 ≤ |f(n)| ≤ |g(n)| sağlanıyorsa, f(n) fonksiyonu g(n) tarafından baskılanır.
Bu durumda f(n)=O(g(n)) yazarız ve f(n) için g(n)’nin büyük O’su deriz.
Daha basit bir ifadeyle, bu, f(n)’in belirli bir indisten itibaren g(n)’den daha hızlı büyümediği anlamına gelir.
Big O gösterimi, alanı girdi boyutu n’in bir fonksiyonu olarak üst sınırla ifade eder; böylece büyük n için baskın terimlere odaklanabiliriz. Bu gösterim algoritmaları sınıflandırmaya yardımcı olur; örneğin O(n), giriş iki katına çıktığında alanın da iki katına çıktığı anlamına gelir.
Yeni başlayanlar arasında yaygın bir yanlış anlama, uzay için Big O’nun 2n gibi sabitleri yok saydığıdır; 2n O(n)’dir, ancak pratikte küçük n için sabitler önemlidir.
İlgili asimptotik gösterimler
Karmaşıklığı modellemek için farklı yollar ve farklı gösterimler vardır. Örneğin Big Omega alt sınır verir; bu da sıralama için en az Ω(n) alan gerektiğini kanıtlamak gibi asgari alan ihtiyaçlarını göstermekte kullanışlıdır. Bir diğer gösterim olan Big Theta, üst ve alt sınırlar örtüştüğünde sıkı sınır sunar; bu da doğru analiz için idealdir; örneğin doğrusal alan için O(n) = Θ(n).
Diğer gösterimsel gelenekler
Uzay karmaşıklığı literatüründe ilgili birkaç başka gösterimsel gelenek de vardır; örneğin polilogaritmik çarpanları yok sayan Yumuşak-O gösterimi Õ. Logaritmaların ihmal edilebilir olduğu grafik işleme gibi ileri algoritmalarda yaygındır. Akış algoritmalarında alt-doğrusal alan için yaklaşık karmaşıklıkları belirtmek üzere teorik makalelerde Õ karşımıza çıkar.
Uzay Karmaşıklığının Bileşenleri
Uzay karmaşıklığının, algoritma tasarımında birçok katkı noktası ve optimizasyon alanı vardır. Bakalım:
Girdi alanı gereksinimleri
Girdi verisi boyutla orantılı yer kaplar; örneğin bir dizi için O(n) veya genom dizileri gibi büyük bir veri kümesini analiz ederken.
Gerçek hayattaki yazılım mülakatlarında, yardımcı alan hesaplanırken girdi alanını hariç tutmak ve yalnızca algoritmanın kullandığı ekstra belleğe odaklanmak yaygındır. Ancak yarışmacı programlamada, toplam alan genellikle girdiyi içerir.
Yardımcı alan ve geçici bellek
Yardımcı alan, bir algoritmanın işlemlerini gerçekleştirmek için ihtiyaç duyduğu ekstra bellek olup doğrudan algoritma tasarımını etkiler.
Örneğin, merge sort algoritmasında temp dizi için O(n) kullanabilirsiniz. Diğer örnekler; tekrarlar için karma tablolara (O(n)), parantez eşlemede özyineleme yığınlarına (O(n)) ve daha nicelerine ihtiyaç duyulmasıdır.
Klasik yardımcı alan örnekleri arasında iki işaretçi yöntemleri (O(1)), deque kullanan kayan pencere algoritmaları (O(k)) ve ziyaret kümesi ile kuyruğu için O(V) alana ihtiyaç duyan BFS bulunur.
Aşağıdaki kod, birleştirme adımında geçici dizi üzerinde merge sort işleminin bir örneğini gösterir:
Her özyinelemeli çağrı bir yığın çerçevesi ekler (dönüş adresi + parametreler + yereller).
Öte yandan, faktöriyel ya da bağlı listede DFS gibi doğrusal özyinelemede yığın O(n)’dir.
Naif Fibonacci veya ağaç özyinelemesi gibi ikili özyinelemede, en kötü durumda O(n) olur.
Scheme, Rust ve bazen Python’da dekoratörlerle desteklenen kuyruk çağrısı optimizasyonu, O(1)’e indirir. Ancak burada asıl tehlike, yaklaşık 1000 olan Python’un varsayılan özyineleme sınırıdır; bu da derin ağaçlarda yığın taşmasına yol açar.
İşte yığın büyümesini gösteren naif özyinelemeli Fibonacci örneği:
deffibonacci(n):if n <=1:return n
return fibonacci(n-1)+ fibonacci(n-2)# O(n) stack depthAltyapı:
Veri yapısı ek yükünün güzel yanı sadeliğidir; tüm yapılar sabit/öğe başına maliyetler ekler; örneğin dizilerin en az ek yükü varken, bağlı listelerde işaretçiler iki kat alan kaplar ve daha pek çok örnek vardır.
Bununla birlikte, programlama dili büyük ölçüde etkilidir. Python listeleri, C dizilerinin aksine dinamik ek yüke sahiptir—örneğin, %75 doluluk oranına sahip karma haritalar boş kovalar için alan israf eder.
Uzay Karmaşıklığı Türleri
Sabit uzay karmaşıklığı: O(1)
Sabit uzay karmaşıklığı, bulunabildiğinde temel ve en iyi çözümdür. Bilinen birçok algoritmanın gerçekten sabit karmaşıklığı vardır; aşağıdaki örnekte olduğu gibi, Python’da tuple açılımı kullanılarak yapılan basit yerinde değiş-tokuş buna örnektir:
defswap_numbers(first, second):
first, second = second, first # O(1) — only two variablesreturn first, secondAltyapı:
Diğer bilinen sabit karmaşıklık algoritmaları, quicksort’ta Dutch flag ayrıştırması ve ağaçlar için Morris dolaşımıdır. Ancak pratik bir sınır vardır: birkaç düzine değişkenle etkili olmadan önce donanımın sınırları nedeniyle, en kötü senaryolarda gerekli bellek sistem kaynaklarını aşarsa O(1) bazen yeterli olmayabilir.
Doğrusal uzay karmaşıklığı: O(n)
Doğrusal uzay karmaşıklığı, girdiye orantılı olmasıyla tanımlanır. Örneğin kopyalama için ek bir dizi ya da benzersizler için bir karma kümesi. En yaygın kullanım alanı, listeleri filtrelemek gibi veri işleme kalıplarıdır.
Logaritmik ve alt-doğrusal uzay karmaşıklığı
Logaritmik uzay karmaşıklığı, bir algoritmanın kullandığı bellek miktarının girdi boyutu arttıkça çok yavaş büyüdüğü anlamına gelir. Özellikle, girdi boyutunun logaritmasıyla orantılıdır.
Örneğin, ikili arama özyinelemesinin yığın derinliği O(log(n))’dir; kuyruk çağrılı böl ve fethet yaklaşımı veya sonuçları O(log(n)) ya da O(sqrt(n)) alanda yaklaştıran akış algoritmaları da öyle.
Logaritmik ve alt-doğrusal uzay karmaşıklıkları nadirdir ama çok büyük veri kümeleri için son derece güçlüdür. Aşağıdaki örnek kodda görüldüğü gibi yinelemeli ikili arama (sabit alan) ile logaritmik karmaşıklığa bir örnek görebilirsiniz:
defbinary_search(numbers, target):
left =0
right =len(numbers)-1while left <= right:# O(1) space total
mid =(left + right)//2# comparison logic...Altyapı:
Kare uzay karmaşıklığı: O(n²)
Kare uzay karmaşıklığında, bellek gereksinimi girdinin karesiyle ölçeklenir. Yaygın bir örnek, O(n²) karmaşıklığına sahip iki boyutlu dinamik programlama tablosu ya da O(V²) karmaşıklığına sahip grafikler için bitişiklik (adjacency) matrisidir. Ancak bu karmaşıklığın, binlerce öğeyi aşan girdilerde pratik olmaktan çıktığını not etmek önemlidir.
Üstel ve faktöriyel uzay karmaşıklığı
İsimden de anlaşılacağı gibi üstel karmaşıklık, gerekli belleğin girdi boyutunun üsteline orantılı olarak büyüdüğü durumlardır: O(2^n). Örneğin tüm alt kümeleri ya da karar ağacı yollarını saklarken ortaya çıkar.
Benzer şekilde, faktöriyel karmaşıklıkta gerekli bellek girdi boyutunun faktörieliyle orantılıdır: O(n!). Bu da girdinin tüm permütasyonlarını listelediğinizde gerçekleşir. Ancak bu karmaşıklıklar genellikle çok pahalıdır ve çoğu sistemde n>20-30 için neredeyse kesinlikle uygulanabilir değildir. Bu durumdaysanız, budama, yaklaştırma ya da farklı bir yaklaşım gerekebilir.
Uzay Karmaşıklığı Nasıl Hesaplanır
Sistematik analiz, tüm bellek tahsislerini ve büyüme kalıplarını tanımlar:
Satır satır analiz yaklaşımı
İlk adım, kodunuzu satır satır inceleyip her değişkeni, diziyi ve nesne tahsisini belirlemektir. Sonraki adım, her tahsisin boyutunun girdi parametrelerine bağlı olup olmadığını belirlemektir. Ardından, bağımsız tahsisler için bellekleri toplayıp karşılıklı dışlayıcı dallar için maksimumu alırsınız. Son olarak, yerel değişkenler için yığın (stack) tahsislerini ve dinamik bellek için yığın (heap) tahsislerini takip edersiniz. İşte bir örnek:
deflinear_search(arr, target):
n =len(arr)# O(1) - single integerfor i inrange(n):# O(1) - loop variableif arr[i]== target:return i
return-1# Total: O(1) auxiliary spaceAltyapı:
Özyinelemeli karmaşıklık analizi
Özyineleme için uzay karmaşıklığı, maksimum özyineleme derinliğinin çağrı çerçevesi başına alan ile çarpımına eşittir; ardından özyineleme sırasında ayrılan yardımcı veri yapıları eklenir; taban durumunu belirleyin ve özyineleme derinliğinin girdi boyutuyla ilişkisini analiz edin; örneğin naif Fibonacci, çerçeve başına O(1) ile O(n) derinlik oluşturur ve toplamda O(n) yığın alanı yapar. İşte faktöriyel için alan hesabı örneği:
deffactorial(n):if n <=1:# Base casereturn1return n * factorial(n -1)# Space: O(n)Altyapı:
Dinamik programlama karmaşıklık analizi
Standart DP tabloları, alt sorun parametreleriyle eşleşen boyutlara sahiptir; dolayısıyla tek boyutlu bir problem O(n) alan gerektirirken, iki boyutlu problemler O(nxm) alana ihtiyaç duyar. Yuvarlanan diziler gibi optimizasyon teknikleri bir boyutu düşürerek örneğin O(n²)’yi O(n)’e dönüştürür. Durum sıkıştırma bağımlılıkları kullanır; eğer i satırı yalnızca i-1’e ihtiyaç duyuyorsa, yalnızca iki satır saklayın. İşte yalnızca iki değişken kullanan alan açısından optimize edilmiş Fibonacci örneği:
deffibonacci_optimized(n):
previous =0
current =1for _ inrange(2, n +1):# O(1) space
previous, current = current, previous + current
return currentAltyapı:
Uzay karmaşıklığını pratikte hesaplamak için şu adımları izleyin:
Bağlama göre girdi alanının dahil edilip edilmediğini netleştirin
Sabit boyutlu değişkenleri ve sabitleri sayın. O(1) katkı yaparlar
Diziler ve karma haritalar gibi yardımcı yapıları girdi boyutuna göre ölçün
Yığın alanı katkısı için maksimum özyineleme derinliğini hesaplayın
Tüm bileşenleri birleştirin ve en sıkı asimptotik sınırla ifade edin.
Yaygın Algoritmaların Uzay Karmaşıklığı
Sıralama algoritmalarının alan gereksinimleri
Merge sort, birleştirme sırasında geçici diziler için O(n) yardımcı alana ihtiyaç duyar. Öte yandan Quicksort ortalamada yalnızca O(log n) yığın alanı kullanır; ancak optimizasyon olmadan en kötü durumda O(n)’dir. Heapsort ise yerinde sıralama yaparak yalnızca O(1) yardımcı alana ulaşır; aşağıdaki kodda olduğu gibi:
Son olarak, counting sort, değer aralığı k ise O(k) alana ihtiyaç duyar; ancak k>>n olduğunda pratik değildir.
Graf algoritmalarında uzay karmaşıklığı
Graf gezinimi için birçok algoritma vardır. Örneğin genişlik öncelikli arama (BFS), düğümler için O(V) alan gerektiren bir kuyruk tutar; derinlik öncelikli arama (DFS) ise en kötü durumda maksimum derinlik için O(V) yığın alanı kullanır. Öte yandan Dijkstra algoritması, öncelik kuyruğu ve her düğüm için uzaklık takibi için O(V) alana ihtiyaç duyar. Grafik gösterimi önemlidir; bitişiklik matrisi O(V²), bitişiklik listesi ise O(V+E) alan kullanır.
Veri yapılarının alan özellikleri
Farklı veri yapıları vardır ve her biri farklı bellek tahsisleri gerektirir. Örneğin Diziler ve ArrayList’ler n öğeyi bitişik O(n) bellekte saklar; bağlı listeler, her düğüm için işaretçi ek yüküyle birlikte O(n) alan gerektirir; ikili ağaçlar düğüm başına 2-3 işaretçiyle birlikte O(n) alan kullanır ki bu önemli bir ek yüktür; karma tablolar, tipik olarak 1,5 ila 3 kat arasında değişen doluluk oranı ile O(n) alanı çarpar; bitişiklik listeleri ise seyrek grafikler için O(V+E) alan verimliliği sağlar.
Örnek uzay karmaşıklığı hesaplamaları
İşte uzay karmaşıklığı hesaplamasına örnekler
# O(1): Yerinde (in-place) string ters çevirme
left =0
right =len(text)-1while left < right:
text[left], text[right]= text[right], text[left]# O(1)
left +=1
right -=1# O(n): Karma kümesiyle tekrar tespiti
seen_numbers =set()# O(n)for num in array:if num in seen_numbers:returnTrue
seen_numbers.add(num)# O(log n): Özyinelemeli ikili ağacın yüksekliğideftree_height(root):ifnot root:return0return1+max(tree_height(root.left), tree_height(root.right))# O(log n) stack# O(n²): Floyd-Warshall uzaklık matrisi
distance =[[float('inf')]* vertices for _ inrange(vertices)]# O(n²)for i inrange(vertices):
distance[i][i]=0Altyapı:
Uzay Karmaşıklığı vs. Zaman Karmaşıklığı
Zaman ve alan, sistem kısıtlarına göre dengelenmesi gereken farklı kaynak boyutlarını temsil eder:
Temel farklar ve benzerlikler
İlk bakışta, uzay ve zaman karmaşıklığı benzer görünse de farklıdırlar; çünkü zaman karmaşıklığı hesaplamalı işlemleri ölçerken, uzay karmaşıklığı bellek tüketimini ölçer. İkisi de aynı asimptotik gösterimi ve matematiksel analiz çerçevelerini kullanır; ancak şunu bilmelisiniz ki algoritmalar bir boyutta eniyiyken diğerinde verimsiz olabilir. Optimizasyon sıklıkla artan alan karşılığında azalan zamanı ya da tersini takas etmeyi içerir.
Temel takası anlamak
Alanı zamanla takas ettiğinizde, yinelenen hesaplamaları ortadan kaldıran önceden hesaplanmış sonuçları depolarsınız. Öte yandan, zamanı alanla takas etmek, değerleri depolamak yerine ihtiyaç duyulduğunda yeniden hesaplamayı gerektirir. Arama tabloları, tekrarlı O(1) erişim için bir kerelik alan yatırımıyla değiş tokuş yapar. Bellek sınırları ve performans gereksinimleri gibi özel kısıtlar, hangi kaynağa öncelik verileceğini belirleyebilir.
Önbellekleme (memoization) ve dinamik programlama optimizasyonu
Önbellekleme, yeniden hesaplamak yerine değerleri kaydederek çalışır; zamanda verimlidir ama alan için kötüdür. Örneğin özyinelemeli Fibonacci’ye O(n) önbellek ekler, ancak zamanı O(2^n)’den O(n)’e indirir; yeniden hesaplamayı önleyen aşağıdaki önbelleklemeli Fibonacci örneğinde olduğu gibi:
memory ={}deffibonacci_memory(n):if n in memo:return memory[n]# O(n) space, O(n) timeif n <=1:return n
memory[n]= fibonacci_memory(n-1)+ fibonacci_memory(n-2)return memory[n]Altyapı:
Buna karşılık alttan üste dinamik programlama, özyineleme yığını ek yükünü ortadan kaldırmak için yinelemeli olarak hesaplar. Alanı optimize etmek için, mevcut hesaplama için gereken önceki durumları yalnızca gerektiği kadar tutmalısınız; örneğin düzenleme uzaklığı, yalnızca bir satır tutularak O(nxm) tablodan O(min(n,m)) alana indirilebilir.
Yerinde (in-place) algoritma teknikleri
Yerinde algoritmalar, yeni bellek yapıları ayırmak yerine girdiyi doğrudan değiştirir; bu yaklaşım yinelemeli olarak uygulandığında bellek tahsisini önemli ölçüde azaltır ve böylece uzay karmaşıklığını düşürür; aşağıdaki yerinde dizi döndürme (Tersleme algoritması) örneğinde olduğu gibi:
defrotate_array(nums, k):
n =len(nums)
k = k % n
reverse(nums,0, k -1)# all operations O(1) auxiliary
reverse(nums, k, n -1)
reverse(nums,0, n -1)Altyapı:
Diğer yerinde teknikler bellek kullanımını dramatik biçimde azaltır. Örneğin iki işaretçi tekniği dizileri O(1) yardımcı alanla işler ve döngüsel sıralama, öğeleri doğru konumlarına takas ederek permütasyon problemlerini O(1) alanda halleder. Ancak, yerinde değiştirmenin girdi verisini bozabileceğini ve yeniden kullanılabilirliği ile kod açıklığını etkileyebileceğini not etmek gerekir.
Alan verimliliği için veri yapısı seçimi
Birçok farklı veri yapısı vardır ve her biri farklı takaslar sunar; bu nedenle, örneğin boyut biliniyorsa bağlı listeler yerine dizileri tercih edin; öğe başına işaretçi ek yükünü ortadan kaldırın; bayrakları depolamak için bitleri kullanarak bayt dizilerine kıyasla 8 kat sıkıştırma elde edin. Trie yapıları, büyük dize koleksiyonları için ortak önekleri paylaşarak alan verimliliği sağlar. Seyrek grafiklerde, O(V²) yerine O(V+E) elde etmek için bitişiklik listelerini matrislere tercih edin.
Pratik Hususlar ve Uygulama Gerçekleri
Gerçek dünyadaki uzay karmaşıklığı; programlama dillerine, donanım mimarisine ve çalışma zamanlarına bağlıdır.
Dil ve çalışma zamanı etmenleri
Python ve Java gibi üst düzey diller, nesne başına meta veriler ve ek yük ekler; C ve C++ ise karmaşıklık pahasına hassas kontrol için manuel bellek yönetimi sunar. Muhtemelen bu yüzden PyTorch gibi makine öğrenimi optimizasyon çerçeveleri hesaplama için C++ kullanır.
Yığın (stack) boyutu sınırları tipik olarak 1 ila 8 MB arasında değişir; bu da maksimum özyineleme derinliğini sınırlar. 64 bit sistemlerin 8 bayt, 32 bit sistemlerin ise 4 bayt işaretçi kullandığını ve bunun işaretçi yapı boyutlarını iki katına çıkardığını unutmayın. Yüksek boyutlu verinin algoritma performansını nasıl etkilediği hakkında daha fazla bilgi için şu eğitime göz atın: Makine Öğreniminde Boyutsallık Laneti.
Bellek hiyerarşisi ve önbellek etkileri
Algoritmanın alan kullanımı ve performansı, bellek hiyerarşisine büyük ölçüde bağlıdır. RAM erişimi yaklaşık 100 nanosaniye, CPU önbellek erişimi ise 1 ila 10 nanosaniye sürer; bu da 100 kat daha hızlıdır. Ayrıca, ardışık bellek erişimi genellikle birlikte yüklenen 64 baytlık önbellek satırlarından yararlanır.
Mekânsal yerellik (spatial locality) iyi olan algoritmalar, önbellek verimliliği sayesinde daha iyi performans elde eder. Kapasiteyi aşan büyük kümelerle çalışırken ise sayfa çarpışması (thrashing) oluşur ve performans ciddi ölçüde düşer.
Çöp toplama ve bellek yönetimi
Bellek yönetimi için faydalı bir araç çöp toplamadır; artık faydalı olmayan verileri temizlemenize olanak tanır. Başka bir deyişle, çöp toplayıcılar nesne takibi ve kuşak yönetimi için bellek ek yükü ekler.
Tepe bellek kullanımı, toplama döngülerinden önce yapılan tahsisler nedeniyle canlı veri boyutunu aşar. Ek olarak, Kuşaksal GC, genç kuşakları daha küçük alanlarda tutarak kısa ömürlü nesneler için optimize eder. Ancak daha belirleyici alan kontrolü istiyorsanız manuel bellek yönetimi kullanabilirsiniz; fakat bellek sızıntıları ve parçalanma gibi risklerinin farkında olmalısınız.
Sonuç ve Öneriler
Uzay karmaşıklığı üzerine yazımı okuduğunuz için teşekkürler. Sizi birkaç son ipucuyla baş başa bırakıyorum:
Mülakatlarda ve kod incelemelerinde yanlış anlaşılmayı önlemek için uzay analizinin girdi boyutunu içerip içermediğini daima netleştirin.
Önce doğruluğa öncelik verin; ardından, profil çıkarma gerçek darboğazları ortaya koyduğunda alanı optimize edin.
Pahalı yeniden düzenlemelerden kaçınmak için sistem tasarımının erken aşamalarında donanım ve bellek kısıtlarını dikkate almaya çalışın.
Kuramsal analizi gerçek davranışa karşı doğrulamak için daima dile uygun profil çıkarma araçlarını seçin ve alan optimizasyonunu kod okunabilirliğiyle dengeleyin. Aşırı zeki optimizasyonlar bakım yapılabilirliğe de zarar verebilir.
Merkezi olmayan veriler ve dağıtık iş yükleri üzerinde bütünleşik (federated) ML işlem hatlarıyla uç zekâ sağlayan hızlandırılmış yapay zekâ sistemleri üzerinde çalışıyorum. Çalışmalarım Büyük Modeller, Konuşma İşleme, Bilgisayarlı Görü, Pekiştirmeli Öğrenme ve ileri ML topolojilerine odaklanmaktadır.
SSS
Eğer seyrek veriler ayrılan O(n) alanın yalnızca %10’unu kullanıyorsa, karmaşıklık yine de O(n) midir?⌄
Evet. Uzay karmaşıklığı en kötü durumu varsayar. Seyrek veri garantileri, Big O analizinde yansıtılmayan farklı bir problem örneğidir.
Uzay karmaşıklığı, zaman karmaşıklığından daha iyi olabilir mi?⌄
Evet. Doğrusal arama O(1) alana ama O(n) zamana sahiptir. Hiçbir şey depolamadan yineleme yaparsınız.
Neden önbellekleme (memoization), zamanı hızlandırmasına rağmen uzay karmaşıklığını kötüleştirir?⌄
Zamanı alanla takas eder. Fibonacci, zamanı O(2^n)’den O(n)’e iyileştirir ama önbellekleme için O(n) alan ekler.
Kuyruk özyinelemesi (tail recursion) eliminasyonu, uzay karmaşıklığını azaltır mı?⌄
Gerçekten de evet. Özyinelemeyi döngülere dönüştürerek yığın çerçevelerini ortadan kaldırır. Faktöriyel, optimize edildiğinde O(n)’den O(1) alana iner.
Karma tablolar doluluk oranı nedeniyle 3× bellek kullanıyorsa, karmaşıklık O(3n) mi yoksa O(n) mi?⌄
O(n). Big O sabit çarpanları yok sayar. 3x çarpanı, girdi boyutundan bağımsız bir ek yüktür.
Algoritmalar, sistem tasarımı ve davranışsal senaryoları kapsayan bu temel sorularla teknik mülakat sürecine hakim olun. Uzman cevapları, kod örnekleri ve kanıtlanmış hazırlık stratejileri edinin.
Git deponuzu temiz tutmak için .gitignore’u nasıl kullanacağınızı öğrenin. Bu eğitim; temelleri, yaygın kullanım durumlarını ve başlamanıza yardımcı olacak pratik örnekleri kapsar!
Uzay karmaşıklığı, girdi boyutu büyüdükçe algoritmaların bellek kullanımını ölçer. Bu da, kısıtlı ortamlarda kaynak yönetimi için uzay karmaşıklığını anlamayı önemli kılar.
Bu yazıda, uzay karmaşıklığının ne olduğunu, nasıl hesaplanacağını ve nasıl en aza indirileceğini ayrıntılı biçimde açıklayacağım. Ayrıca, gerçek dünya uygulamalarını ele alacak ve kuramsal temeller sunacağım.
Uzay Karmaşıklığını Anlamak
Hesaplamalara ve optimizasyonlara dalmadan önce, uzay karmaşıklığının gerçekte ne anlama geldiğini ve neden önemsemeniz gerektiğini netleştirelim.
Uzay karmaşıklığı nedir?
Uzay karmaşıklığı, algoritmanın girdi boyutuna göre ihtiyaç duyduğu toplam bellek miktarıdır. Sistem ölçeklenebilirliği için en kötü durum bellek ayak izini analiz ederken önemlidir; toplam alan girdiyi, yardımcı alanı ve ek yükü içerir; yardımcı alan ise girdi ve çıktıyı hariç tutan ekstra bellektir. Örneğin, bir diziyi sıralamayı ele alın: Toplam alan dizinin kendisini içerirken, yardımcı alan birleştime sırasında kullanılan geçici alan (merge sort’ta temp alanı) olabilir.
Uzay karmaşıklığı neden önemlidir
Modern bilişimde, alan verimliliği çoğu zaman kritiktir. Uzay karmaşıklığını optimize ederek, algoritmalarınızın sınırlı RAM’e sahip cihazlarda (IoT’de gömülü sistemler veya akıllı telefonlar gibi) ya da RAM’in her bitinin önemli olduğu, kaynak talebi yüksek işlemleri barındıran büyük sistemlerde çalışmasını sağlarsınız.
Örneğin mobil uygulamaları düşünün: uzay karmaşıklığını optimize etmek, görüntü işleme yapılırken çöküşleri önlemeye yardımcı olur. Ya da depolama maliyetlerinden tasarruf için bulut bilişimi, veriyi tam yüklemeden gerçek zamanlı analiz elde etmeyi ve akış verilerini düşünün.
Asimptotik Analiz ve Uzay için Big O Gösterimi
Bu bölümde, Big O gibi asimptotik gösterimlerin alan artışını nasıl betimlediğini açıklayacak ve bellek kullanımını sınırlamak ve karşılaştırmak için araçlar sunacağım.
Big O gösterimine giriş
Matematiksel olarak, f(n) fonksiyonu için bir c ve k sabiti varsa ve tüm k>n için 0 ≤ |f(n)| ≤ |g(n)| sağlanıyorsa, f(n) fonksiyonu g(n) tarafından baskılanır.
Bu durumda f(n)=O(g(n)) yazarız ve f(n) için g(n)’nin büyük O’su deriz.
Daha basit bir ifadeyle, bu, f(n)’in belirli bir indisten itibaren g(n)’den daha hızlı büyümediği anlamına gelir.
Big O gösterimi, alanı girdi boyutu n’in bir fonksiyonu olarak üst sınırla ifade eder; böylece büyük n için baskın terimlere odaklanabiliriz. Bu gösterim algoritmaları sınıflandırmaya yardımcı olur; örneğin O(n), giriş iki katına çıktığında alanın da iki katına çıktığı anlamına gelir.
Yeni başlayanlar arasında yaygın bir yanlış anlama, uzay için Big O’nun 2n gibi sabitleri yok saydığıdır; 2n O(n)’dir, ancak pratikte küçük n için sabitler önemlidir.
İlgili asimptotik gösterimler
Karmaşıklığı modellemek için farklı yollar ve farklı gösterimler vardır. Örneğin Big Omega alt sınır verir; bu da sıralama için en az Ω(n) alan gerektiğini kanıtlamak gibi asgari alan ihtiyaçlarını göstermekte kullanışlıdır. Bir diğer gösterim olan Big Theta, üst ve alt sınırlar örtüştüğünde sıkı sınır sunar; bu da doğru analiz için idealdir; örneğin doğrusal alan için O(n) = Θ(n).
Diğer gösterimsel gelenekler
Uzay karmaşıklığı literatüründe ilgili birkaç başka gösterimsel gelenek de vardır; örneğin polilogaritmik çarpanları yok sayan Yumuşak-O gösterimi Õ. Logaritmaların ihmal edilebilir olduğu grafik işleme gibi ileri algoritmalarda yaygındır. Akış algoritmalarında alt-doğrusal alan için yaklaşık karmaşıklıkları belirtmek üzere teorik makalelerde Õ karşımıza çıkar.
Uzay Karmaşıklığının Bileşenleri
Uzay karmaşıklığının, algoritma tasarımında birçok katkı noktası ve optimizasyon alanı vardır. Bakalım:
Girdi alanı gereksinimleri
Girdi verisi boyutla orantılı yer kaplar; örneğin bir dizi için O(n) veya genom dizileri gibi büyük bir veri kümesini analiz ederken.
Gerçek hayattaki yazılım mülakatlarında, yardımcı alan hesaplanırken girdi alanını hariç tutmak ve yalnızca algoritmanın kullandığı ekstra belleğe odaklanmak yaygındır. Ancak yarışmacı programlamada, toplam alan genellikle girdiyi içerir.
Yardımcı alan ve geçici bellek
Yardımcı alan, bir algoritmanın işlemlerini gerçekleştirmek için ihtiyaç duyduğu ekstra bellek olup doğrudan algoritma tasarımını etkiler.
Örneğin, merge sort algoritmasında temp dizi için O(n) kullanabilirsiniz. Diğer örnekler; tekrarlar için karma tablolara (O(n)), parantez eşlemede özyineleme yığınlarına (O(n)) ve daha nicelerine ihtiyaç duyulmasıdır.
Klasik yardımcı alan örnekleri arasında iki işaretçi yöntemleri (O(1)), deque kullanan kayan pencere algoritmaları (O(k)) ve ziyaret kümesi ile kuyruğu için O(V) alana ihtiyaç duyan BFS bulunur.
Aşağıdaki kod, birleştirme adımında geçici dizi üzerinde merge sort işleminin bir örneğini gösterir:
Çağrı yığını ve özyineleme alanı
Her özyinelemeli çağrı bir yığın çerçevesi ekler (dönüş adresi + parametreler + yereller).
Öte yandan, faktöriyel ya da bağlı listede DFS gibi doğrusal özyinelemede yığın O(n)’dir.
Naif Fibonacci veya ağaç özyinelemesi gibi ikili özyinelemede, en kötü durumda O(n) olur.
Scheme, Rust ve bazen Python’da dekoratörlerle desteklenen kuyruk çağrısı optimizasyonu, O(1)’e indirir. Ancak burada asıl tehlike, yaklaşık 1000 olan Python’un varsayılan özyineleme sınırıdır; bu da derin ağaçlarda yığın taşmasına yol açar.
İşte yığın büyümesini gösteren naif özyinelemeli Fibonacci örneği:
Python’da karmaşıklık analizine dair daha fazla bilgi için Python ile Kodun Karmaşıklığını Analiz Etme blog yazımıza göz atın.
Veri yapısı ek yükü
Veri yapısı ek yükünün güzel yanı sadeliğidir; tüm yapılar sabit/öğe başına maliyetler ekler; örneğin dizilerin en az ek yükü varken, bağlı listelerde işaretçiler iki kat alan kaplar ve daha pek çok örnek vardır.
Bununla birlikte, programlama dili büyük ölçüde etkilidir. Python listeleri, C dizilerinin aksine dinamik ek yüke sahiptir—örneğin, %75 doluluk oranına sahip karma haritalar boş kovalar için alan israf eder.
Uzay Karmaşıklığı Türleri
Sabit uzay karmaşıklığı: O(1)
Sabit uzay karmaşıklığı, bulunabildiğinde temel ve en iyi çözümdür. Bilinen birçok algoritmanın gerçekten sabit karmaşıklığı vardır; aşağıdaki örnekte olduğu gibi, Python’da tuple açılımı kullanılarak yapılan basit yerinde değiş-tokuş buna örnektir:
Diğer bilinen sabit karmaşıklık algoritmaları, quicksort’ta Dutch flag ayrıştırması ve ağaçlar için Morris dolaşımıdır. Ancak pratik bir sınır vardır: birkaç düzine değişkenle etkili olmadan önce donanımın sınırları nedeniyle, en kötü senaryolarda gerekli bellek sistem kaynaklarını aşarsa O(1) bazen yeterli olmayabilir.
Doğrusal uzay karmaşıklığı: O(n)
Doğrusal uzay karmaşıklığı, girdiye orantılı olmasıyla tanımlanır. Örneğin kopyalama için ek bir dizi ya da benzersizler için bir karma kümesi. En yaygın kullanım alanı, listeleri filtrelemek gibi veri işleme kalıplarıdır.
Logaritmik ve alt-doğrusal uzay karmaşıklığı
Logaritmik uzay karmaşıklığı, bir algoritmanın kullandığı bellek miktarının girdi boyutu arttıkça çok yavaş büyüdüğü anlamına gelir. Özellikle, girdi boyutunun logaritmasıyla orantılıdır.
Örneğin, ikili arama özyinelemesinin yığın derinliği O(log(n))’dir; kuyruk çağrılı böl ve fethet yaklaşımı veya sonuçları O(log(n)) ya da O(sqrt(n)) alanda yaklaştıran akış algoritmaları da öyle.
Logaritmik ve alt-doğrusal uzay karmaşıklıkları nadirdir ama çok büyük veri kümeleri için son derece güçlüdür. Aşağıdaki örnek kodda görüldüğü gibi yinelemeli ikili arama (sabit alan) ile logaritmik karmaşıklığa bir örnek görebilirsiniz:
Kare uzay karmaşıklığı: O(n²)
Kare uzay karmaşıklığında, bellek gereksinimi girdinin karesiyle ölçeklenir. Yaygın bir örnek, O(n²) karmaşıklığına sahip iki boyutlu dinamik programlama tablosu ya da O(V²) karmaşıklığına sahip grafikler için bitişiklik (adjacency) matrisidir. Ancak bu karmaşıklığın, binlerce öğeyi aşan girdilerde pratik olmaktan çıktığını not etmek önemlidir.
Üstel ve faktöriyel uzay karmaşıklığı
İsimden de anlaşılacağı gibi üstel karmaşıklık, gerekli belleğin girdi boyutunun üsteline orantılı olarak büyüdüğü durumlardır: O(2^n). Örneğin tüm alt kümeleri ya da karar ağacı yollarını saklarken ortaya çıkar.
Benzer şekilde, faktöriyel karmaşıklıkta gerekli bellek girdi boyutunun faktörieliyle orantılıdır: O(n!). Bu da girdinin tüm permütasyonlarını listelediğinizde gerçekleşir. Ancak bu karmaşıklıklar genellikle çok pahalıdır ve çoğu sistemde n>20-30 için neredeyse kesinlikle uygulanabilir değildir. Bu durumdaysanız, budama, yaklaştırma ya da farklı bir yaklaşım gerekebilir.
Uzay Karmaşıklığı Nasıl Hesaplanır
Sistematik analiz, tüm bellek tahsislerini ve büyüme kalıplarını tanımlar:
Satır satır analiz yaklaşımı
İlk adım, kodunuzu satır satır inceleyip her değişkeni, diziyi ve nesne tahsisini belirlemektir. Sonraki adım, her tahsisin boyutunun girdi parametrelerine bağlı olup olmadığını belirlemektir. Ardından, bağımsız tahsisler için bellekleri toplayıp karşılıklı dışlayıcı dallar için maksimumu alırsınız. Son olarak, yerel değişkenler için yığın (stack) tahsislerini ve dinamik bellek için yığın (heap) tahsislerini takip edersiniz. İşte bir örnek:
Özyinelemeli karmaşıklık analizi
Özyineleme için uzay karmaşıklığı, maksimum özyineleme derinliğinin çağrı çerçevesi başına alan ile çarpımına eşittir; ardından özyineleme sırasında ayrılan yardımcı veri yapıları eklenir; taban durumunu belirleyin ve özyineleme derinliğinin girdi boyutuyla ilişkisini analiz edin; örneğin naif Fibonacci, çerçeve başına O(1) ile O(n) derinlik oluşturur ve toplamda O(n) yığın alanı yapar. İşte faktöriyel için alan hesabı örneği:
Dinamik programlama karmaşıklık analizi
Standart DP tabloları, alt sorun parametreleriyle eşleşen boyutlara sahiptir; dolayısıyla tek boyutlu bir problem O(n) alan gerektirirken, iki boyutlu problemler O(nxm) alana ihtiyaç duyar. Yuvarlanan diziler gibi optimizasyon teknikleri bir boyutu düşürerek örneğin O(n²)’yi O(n)’e dönüştürür. Durum sıkıştırma bağımlılıkları kullanır; eğer i satırı yalnızca i-1’e ihtiyaç duyuyorsa, yalnızca iki satır saklayın. İşte yalnızca iki değişken kullanan alan açısından optimize edilmiş Fibonacci örneği:
Boyutluluk ve indirgenmesi hakkında meraklıysanız, şu eğitimlerimize göz atmanızı öneririm: Yavaş Değişen Boyutlarda (SCD) Ustalık ve Kardinaliteyi Anlamak: Veri Yoğun İş Akışları için Zorluklar ve Çözümler.
Pratikte uzay karmaşıklığını hesaplama adımları
Uzay karmaşıklığını pratikte hesaplamak için şu adımları izleyin:
Yaygın Algoritmaların Uzay Karmaşıklığı
Sıralama algoritmalarının alan gereksinimleri
Merge sort, birleştirme sırasında geçici diziler için O(n) yardımcı alana ihtiyaç duyar. Öte yandan Quicksort ortalamada yalnızca O(log n) yığın alanı kullanır; ancak optimizasyon olmadan en kötü durumda O(n)’dir. Heapsort ise yerinde sıralama yaparak yalnızca O(1) yardımcı alana ulaşır; aşağıdaki kodda olduğu gibi:
Son olarak, counting sort, değer aralığı k ise O(k) alana ihtiyaç duyar; ancak k>>n olduğunda pratik değildir.
Graf algoritmalarında uzay karmaşıklığı
Graf gezinimi için birçok algoritma vardır. Örneğin genişlik öncelikli arama (BFS), düğümler için O(V) alan gerektiren bir kuyruk tutar; derinlik öncelikli arama (DFS) ise en kötü durumda maksimum derinlik için O(V) yığın alanı kullanır. Öte yandan Dijkstra algoritması, öncelik kuyruğu ve her düğüm için uzaklık takibi için O(V) alana ihtiyaç duyar. Grafik gösterimi önemlidir; bitişiklik matrisi O(V²), bitişiklik listesi ise O(V+E) alan kullanır.
Veri yapılarının alan özellikleri
Farklı veri yapıları vardır ve her biri farklı bellek tahsisleri gerektirir. Örneğin Diziler ve ArrayList’ler n öğeyi bitişik O(n) bellekte saklar; bağlı listeler, her düğüm için işaretçi ek yüküyle birlikte O(n) alan gerektirir; ikili ağaçlar düğüm başına 2-3 işaretçiyle birlikte O(n) alan kullanır ki bu önemli bir ek yüktür; karma tablolar, tipik olarak 1,5 ila 3 kat arasında değişen doluluk oranı ile O(n) alanı çarpar; bitişiklik listeleri ise seyrek grafikler için O(V+E) alan verimliliği sağlar.
Örnek uzay karmaşıklığı hesaplamaları
İşte uzay karmaşıklığı hesaplamasına örnekler
Uzay Karmaşıklığı vs. Zaman Karmaşıklığı
Zaman ve alan, sistem kısıtlarına göre dengelenmesi gereken farklı kaynak boyutlarını temsil eder:
Temel farklar ve benzerlikler
İlk bakışta, uzay ve zaman karmaşıklığı benzer görünse de farklıdırlar; çünkü zaman karmaşıklığı hesaplamalı işlemleri ölçerken, uzay karmaşıklığı bellek tüketimini ölçer. İkisi de aynı asimptotik gösterimi ve matematiksel analiz çerçevelerini kullanır; ancak şunu bilmelisiniz ki algoritmalar bir boyutta eniyiyken diğerinde verimsiz olabilir. Optimizasyon sıklıkla artan alan karşılığında azalan zamanı ya da tersini takas etmeyi içerir.
Temel takası anlamak
Alanı zamanla takas ettiğinizde, yinelenen hesaplamaları ortadan kaldıran önceden hesaplanmış sonuçları depolarsınız. Öte yandan, zamanı alanla takas etmek, değerleri depolamak yerine ihtiyaç duyulduğunda yeniden hesaplamayı gerektirir. Arama tabloları, tekrarlı O(1) erişim için bir kerelik alan yatırımıyla değiş tokuş yapar. Bellek sınırları ve performans gereksinimleri gibi özel kısıtlar, hangi kaynağa öncelik verileceğini belirleyebilir.
Önbellekleme (memoization) ve dinamik programlama optimizasyonu
Önbellekleme, yeniden hesaplamak yerine değerleri kaydederek çalışır; zamanda verimlidir ama alan için kötüdür. Örneğin özyinelemeli Fibonacci’ye O(n) önbellek ekler, ancak zamanı O(2^n)’den O(n)’e indirir; yeniden hesaplamayı önleyen aşağıdaki önbelleklemeli Fibonacci örneğinde olduğu gibi:
Buna karşılık alttan üste dinamik programlama, özyineleme yığını ek yükünü ortadan kaldırmak için yinelemeli olarak hesaplar. Alanı optimize etmek için, mevcut hesaplama için gereken önceki durumları yalnızca gerektiği kadar tutmalısınız; örneğin düzenleme uzaklığı, yalnızca bir satır tutularak O(nxm) tablodan O(min(n,m)) alana indirilebilir.
Yerinde (in-place) algoritma teknikleri
Yerinde algoritmalar, yeni bellek yapıları ayırmak yerine girdiyi doğrudan değiştirir; bu yaklaşım yinelemeli olarak uygulandığında bellek tahsisini önemli ölçüde azaltır ve böylece uzay karmaşıklığını düşürür; aşağıdaki yerinde dizi döndürme (Tersleme algoritması) örneğinde olduğu gibi:
Diğer yerinde teknikler bellek kullanımını dramatik biçimde azaltır. Örneğin iki işaretçi tekniği dizileri O(1) yardımcı alanla işler ve döngüsel sıralama, öğeleri doğru konumlarına takas ederek permütasyon problemlerini O(1) alanda halleder. Ancak, yerinde değiştirmenin girdi verisini bozabileceğini ve yeniden kullanılabilirliği ile kod açıklığını etkileyebileceğini not etmek gerekir.
Alan verimliliği için veri yapısı seçimi
Birçok farklı veri yapısı vardır ve her biri farklı takaslar sunar; bu nedenle, örneğin boyut biliniyorsa bağlı listeler yerine dizileri tercih edin; öğe başına işaretçi ek yükünü ortadan kaldırın; bayrakları depolamak için bitleri kullanarak bayt dizilerine kıyasla 8 kat sıkıştırma elde edin. Trie yapıları, büyük dize koleksiyonları için ortak önekleri paylaşarak alan verimliliği sağlar. Seyrek grafiklerde, O(V²) yerine O(V+E) elde etmek için bitişiklik listelerini matrislere tercih edin.
Pratik Hususlar ve Uygulama Gerçekleri
Gerçek dünyadaki uzay karmaşıklığı; programlama dillerine, donanım mimarisine ve çalışma zamanlarına bağlıdır.
Dil ve çalışma zamanı etmenleri
Python ve Java gibi üst düzey diller, nesne başına meta veriler ve ek yük ekler; C ve C++ ise karmaşıklık pahasına hassas kontrol için manuel bellek yönetimi sunar. Muhtemelen bu yüzden PyTorch gibi makine öğrenimi optimizasyon çerçeveleri hesaplama için C++ kullanır.
Yığın (stack) boyutu sınırları tipik olarak 1 ila 8 MB arasında değişir; bu da maksimum özyineleme derinliğini sınırlar. 64 bit sistemlerin 8 bayt, 32 bit sistemlerin ise 4 bayt işaretçi kullandığını ve bunun işaretçi yapı boyutlarını iki katına çıkardığını unutmayın. Yüksek boyutlu verinin algoritma performansını nasıl etkilediği hakkında daha fazla bilgi için şu eğitime göz atın: Makine Öğreniminde Boyutsallık Laneti.
Bellek hiyerarşisi ve önbellek etkileri
Algoritmanın alan kullanımı ve performansı, bellek hiyerarşisine büyük ölçüde bağlıdır. RAM erişimi yaklaşık 100 nanosaniye, CPU önbellek erişimi ise 1 ila 10 nanosaniye sürer; bu da 100 kat daha hızlıdır. Ayrıca, ardışık bellek erişimi genellikle birlikte yüklenen 64 baytlık önbellek satırlarından yararlanır.
Mekânsal yerellik (spatial locality) iyi olan algoritmalar, önbellek verimliliği sayesinde daha iyi performans elde eder. Kapasiteyi aşan büyük kümelerle çalışırken ise sayfa çarpışması (thrashing) oluşur ve performans ciddi ölçüde düşer.
Çöp toplama ve bellek yönetimi
Bellek yönetimi için faydalı bir araç çöp toplamadır; artık faydalı olmayan verileri temizlemenize olanak tanır. Başka bir deyişle, çöp toplayıcılar nesne takibi ve kuşak yönetimi için bellek ek yükü ekler.
Tepe bellek kullanımı, toplama döngülerinden önce yapılan tahsisler nedeniyle canlı veri boyutunu aşar. Ek olarak, Kuşaksal GC, genç kuşakları daha küçük alanlarda tutarak kısa ömürlü nesneler için optimize eder. Ancak daha belirleyici alan kontrolü istiyorsanız manuel bellek yönetimi kullanabilirsiniz; fakat bellek sızıntıları ve parçalanma gibi risklerinin farkında olmalısınız.
Sonuç ve Öneriler
Uzay karmaşıklığı üzerine yazımı okuduğunuz için teşekkürler. Sizi birkaç son ipucuyla baş başa bırakıyorum: