Big O, bir kodun kesin olarak kaç saniyede çalışacağını değil, veri miktarı büyüdükçe işlem sayısının nasıl arttığını anlamaya yarar. Küçük bir listede hızlı görünen kod, veri sayısı arttığında her kaydı tekrar tekrar kontrol ediyorsa ciddi bir darboğaza dönüşebilir.
Yavaşlayan kodu teşhis ederken ilk soru “Bu işlem benim bilgisayarımda kaç saniye sürdü?” değil, “Veri büyüdüğünde bu işlem kaç kez tekrarlanıyor?” olmalıdır. Bu bakış açısı, sorunu donanım değiştirmeden veya rastgele optimizasyon yapmadan önce doğru yerde aramanızı sağlar.
Kod Veri Büyüdükçe Neden Yavaşlar?
Bir programın çalışma maliyeti, işlediği veri miktarıyla aynı oranda artmıyorsa küçük verilerde fark edilmeyen performans sorunu büyük verilerde görünür hâle gelir. Örneğin 10 kayıt üzerinde çalışan bir arama işlemi neredeyse anında tamamlanabilir. Ancak aynı kod 100 bin kayıt üzerinde her kaydı başka bir listeyle karşılaştırıyorsa yapılacak kontrollerin sayısı yalnızca biraz değil, çok daha hızlı artabilir.
Buradaki temel kavram girdi büyüklüğüdür. Big O anlatımında bu büyüklük çoğunlukla n harfiyle gösterilir. Buradaki n; bir listedeki eleman sayısı, veritabanındaki kayıt sayısı, işlenecek dosya miktarı veya bir metindeki karakter sayısı olabilir. Yani n, programın üzerinde çalıştığı verinin ne kadar büyük olduğunu temsil eden sade bir ölçüdür.
İşlem sayısı veriyle nasıl büyür?
Bir listeyi yalnızca bir kez dolaşan bir kodda n eleman varsa yaklaşık n kontrol yapılır. Liste iki katına çıktığında yapılacak kontrol sayısı da kabaca iki katına çıkar. Bu davranış genellikle O(n) olarak ifade edilir.
Buna karşılık her elemanı diğer listedeki tüm elemanlarla karşılaştıran bir kod düşünelim. İlk listede n, ikinci listede de n eleman varsa yaklaşık n × n karşılaştırma yapılır. Veri sayısı arttıkça maliyet çok daha hızlı büyür ve bu davranış O(n²) olarak ifade edilir.
ogrenciler = ["Ayşe", "Mehmet", "Zeynep"]
notlar = [80, 90, 75]
for ogrenci in ogrenciler:
for not_degeri in notlar:
print(ogrenci, not_degeri)
Bu örnekte iki döngü bulunduğu için kaba bir bakışla O(n²) denebilir; ancak burada iki farklı veri kümesinin boyutlarını ayrı düşünmek daha doğrudur. Öğrenci sayısı n, not sayısı m ise işlem maliyeti O(n × m) olur. Not listesi her öğrenci için sabit kalıyorsa, pratikte büyüyen yalnızca öğrenci sayısı olabilir. Bu nedenle iç içe döngü görmek önemli bir işarettir fakat tek başına kesin karmaşıklık sonucu değildir.
Big O kesin çalışma süresi değildir
Big O gösterimi “Bu kod tam olarak 2,4 saniyede çalışır” anlamına gelmez. Aynı karmaşıklığa sahip iki kod, gerçek hayatta farklı sürelerde çalışabilir. Bunun birkaç nedeni vardır:
- İşlem başına düşen sabit maliyetler farklı olabilir.
- Kullanılan bilgisayarın işlemcisi, belleği ve depolama hızı sonucu değiştirebilir.
- Python yorumlayıcısının yaptığı ek işlemler çalışma süresini etkileyebilir.
- Verilerin dağılımı, aranan değerin listenin başında veya sonunda bulunması gibi durumlar sonucu değiştirebilir.
- Dosya okuma, ağ bağlantısı veya veritabanı sorgusu gibi dış etkenler kodun toplam süresini artırabilir.
Örneğin bir listedeki değeri arayan döngü, aranan elemanı ilk sırada bulursa hemen durabilir. Eleman listenin sonunda bulunuyorsa veya hiç bulunmuyorsa listenin tamamını dolaşması gerekir. Bu iki çalıştırma farklı süreler üretebilir; fakat büyük veri karşısında yapılabilecek maksimum kontrol sayısının nasıl büyüdüğünü incelemek yine de daha genel bir fikir verir.
Bu yüzden Big O, belirli bir bilgisayardaki kesin süreyi ölçmek yerine algoritmanın ölçeklenme davranışını anlatır. Gerçek süreyi görmek için zaman ölçümü ve profil oluşturma araçları kullanılabilir; ancak hangi kodun veri büyüdüğünde sorun çıkarabileceğini anlamak için önce işlem sayısının büyüme biçimine bakılır.
Küçük veride neden sorun fark edilmez?
Bir kodun 20 kayıt üzerinde hızlı çalışması, iyi ölçeklendiğini kanıtlamaz. Küçük girdilerde O(n), O(n²) ve hatta bazı daha pahalı işlemler arasındaki fark günlük kullanımda hissedilmeyebilir. Çünkü yapılacak toplam işlem sayısı henüz düşüktür.
Örneğin 10 elemanlı iki listeyi birbirleriyle karşılaştırmak yaklaşık 100 kontrol anlamına gelebilir. Bu sayı kullanıcı açısından fark edilmeyecek kadar kısa sürede tamamlanabilir. Listeler 10 bin elemana çıktığında aynı yaklaşım yaklaşık 100 milyon karşılaştırmaya dönüşebilir. Donanım ve kodun ayrıntıları gerçek süreyi değiştirse de, veri büyüdükçe ortaya çıkan ölçek farkı artık göz ardı edilemez.
Algoritma değerlendirmesi konusunda temel kavramlarınızı sınamak, hangi işlemlerin veri büyüdükçe daha pahalı hâle geldiğini görmek için algoritma bilgi testi gibi uygulamalı bir değerlendirmeden yararlanabilirsiniz.
Bu bölümden çıkarılacak pratik çerçeve şudur: Önce toplam süreyi değil, veri büyüdüğünde aynı işlemin kaç kez tekrarlandığını sorun. Kodun içinde gereksiz tekrarlar varsa, sonraki adım bu tekrarların neden oluştuğunu ve daha uygun bir veri yapısıyla azaltılıp azaltılamayacağını incelemektir.
O(1), O(log n), O(n) ve O(n²) Nasıl Karşılaştırılır?

Big O sınıflarını anlamanın en kolay yolu, veri miktarı arttığında işlem sayısının nasıl davrandığını karşılaştırmaktır. O(1) veri ne kadar büyürse büyüsün aynı sayıda temel işlem yapmaya çalışır. O(n) veriyi baştan sona dolaşır. O(log n) her adımda arama alanını küçültür. O(n²) ise çoğunlukla verilerin birbiriyle tekrar tekrar karşılaştırıldığı durumlarda ortaya çıkar.
| Karmaşıklık | Veri büyüdüğünde işlem davranışı | Basit örnek | Dikkat edilmesi gereken nokta |
|---|---|---|---|
| O(1) | Veri miktarından bağımsız olarak yaklaşık sabit sayıda işlem yapılır. | Bir listenin belirli indeksindeki elemana erişmek | İşlemin gerçekten sabit olup olmadığı kullanılan veri yapısına bağlıdır. |
| O(log n) | Her adımda aranacak alan küçültülür; veri büyüse de artış yavaştır. | Sıralı listede ikili arama fikri | İkili arama için verinin sıralı olması veya aramaya uygun bir düzende bulunması gerekir. |
| O(n) | Veri yaklaşık bir kez dolaşılır; veri iki katına çıkarsa işlem sayısı da kabaca iki katına çıkar. | Listedeki sayıların toplamını hesaplamak | Tek döngü görmek çoğu zaman O(n) fikrini destekler; fakat döngü içindeki işlemler ayrıca incelenmelidir. |
| O(n²) | Veri büyüdükçe karşılaştırma sayısı çok daha hızlı artar. | Her elemanı diğer tüm elemanlarla karşılaştırmak | İki veri boyutu farklıysa sonuç O(n × m) olabilir; her iç içe döngü otomatik olarak O(n²) değildir. |
O(1): Veri büyüse de aynı noktaya erişmek
O(1), sabit karmaşıklık anlamına gelir. Bir listenin belirli bir indeksindeki elemana erişmek buna örnek olarak verilebilir. Liste 10 elemanlı da olsa 10 milyon elemanlı da olsa, indeks bilgisi hazır olduğu sürece program belirli konuma erişmek için yaklaşık aynı türden işlemi yapar.
Günlük hayatta bunu bir dolabın numarası bilinen çekmecesini açmaya benzetebilirsiniz. Dolapta kaç çekmece olduğu önemli değildir; doğrudan 8 numaralı çekmeceye gidersiniz. Ancak listenin içinde belirli bir değeri arıyorsanız durum değişir. Değerin hangi konumda olduğunu bilmiyorsanız elemanları tek tek kontrol etmeniz gerekebilir.
Küçük bir listede hem doğrudan indeks erişimi hem de sırayla arama hızlı görünebilir. Liste büyüdüğünde doğrudan erişim benzer davranışı korurken, sırayla arama daha fazla elemanı kontrol etmeye başlar.
O(log n): Her adımda arama alanını küçültmek
O(log n) davranışında program her adımda tüm veriyi incelemek yerine arama alanını küçültür. En bilinen sezgisel örnek, sıralı bir listede ikili aramadır. Program ortadaki elemana bakar. Aranan değer ortadaki değerden küçükse sağ tarafı, büyükse sol tarafı eler. Böylece her adımda aranacak bölümün önemli bir kısmı devre dışı kalır.
Bu yaklaşımın çalışması için listenin sıralı olması gerekir. Sıralama yoksa ortadaki elemana bakarak hangi tarafın elenebileceğini güvenilir biçimde anlayamazsınız. Ayrıca veriyi önce sıralamanın maliyeti de toplam çözüm değerlendirilirken hesaba katılmalıdır.
Günlük hayatta sözlükte bir kelime aramak O(log n) fikrini anlamaya yardımcı olur. Sözlüğü ilk sayfadan başlayarak tek tek okumazsınız; yaklaşık ortasından açar, kelimenin alfabetik olarak önce mi sonra mı geldiğine bakar ve arama alanını daraltırsınız.
Küçük bir listede sırayla arama ile ikili arama arasındaki fark belirgin olmayabilir. Büyük ve sıralı bir veri kümesinde ise her seferinde büyük bir bölümü elemek, tek tek kontrol etmeye göre çok daha az adım gerektirebilir. Burada dikkat edilmesi gereken nokta, O(log n) yaklaşımının sihirli bir hızlandırma olmadığı; sıralama, uygun veri düzeni ve doğru arama koşulları gerektirdiğidir.
O(n): Veriyi yaklaşık bir kez dolaşmak
O(n) davranışında işlem sayısı veri miktarıyla birlikte yaklaşık doğrusal artar. Listedeki her sayıyı toplamak, her kaydı bir koşula göre filtrelemek veya bir metindeki karakterleri sırayla kontrol etmek bu sınıfa yakın örneklerdir.
Bir markette alışveriş listesindeki ürünleri tek tek kontrol ettiğinizi düşünün. Liste 10 üründen 20 ürüne çıktığında kontrol etmeniz gereken ürün sayısı da yaklaşık iki katına çıkar. Bu artış yönetilebilir olabilir; özellikle her eleman üzerinde yapılan işlem basitse O(n), çoğu temel tarama için makul bir başlangıç noktasıdır.
Yine de “tek döngü var, kesinlikle O(n)” demek doğru değildir. Döngünün içinde başka bir listeyi baştan sona dolaşan arama, pahalı bir fonksiyon çağrısı veya veri tabanına yapılan tekrar tekrar sorgular varsa toplam maliyet büyüyebilir. Karmaşıklığı belirlemek için yalnızca döngü sayısına değil, her döngü adımında gerçekte ne yapıldığına bakılmalıdır.
O(n²): Verilerin birbirleriyle tekrar tekrar karşılaştırılması
O(n²), çoğunlukla her elemanın diğer elemanlarla karşılaştırıldığı durumlarda görülür. Örneğin bir listedeki tekrar eden değerleri bulmak için her elemanı listenin geri kalanıyla karşılaştırmak, veri sayısı arttıkça çok sayıda kontrol oluşturabilir.
Bir sınıftaki öğrencilerin her biriyle diğer tüm öğrencileri tanıştırmaya çalıştığınızı düşünün. Öğrenci sayısı az olduğunda süreç kolay görünür. Ancak kişi sayısı büyüdükçe yalnızca öğrenci sayısının değil, olası eşleşmelerin de artması gerekir. Kodda da iki döngü aynı veri boyutuna bağlı olarak ilerliyorsa benzer bir büyüme ortaya çıkabilir.
Küçük girdilerde O(n) ile O(n²) kodu aynı hızda çalışıyor gibi görünebilir. Fakat veri miktarı arttığında doğrusal yaklaşımda işlem sayısı yaklaşık veri miktarı kadar artarken, karesel yaklaşımda her yeni eleman çok daha fazla karşılaştırma doğurabilir.
Burada önemli bir sınır vardır: İç içe döngüler her zaman O(n²) değildir. Dış döngü n kez, iç döngü sabit sayıda çalışıyorsa toplam maliyet O(n) olabilir. İç döngü farklı bir veri kümesinin m elemanını dolaşıyorsa O(n × m) söz konusu olabilir. Bir döngünün her turunda arama alanı yarıya iniyorsa, davranış O(n log n) gibi başka bir sınıfa yaklaşabilir.
Bu dört sınıfı karşılaştırırken akılda tutulması gereken ana fikir şudur: Küçük veriler, algoritmalar arasındaki farkı gizleyebilir; büyük veriler ise kodun ölçeklenme biçimini ortaya çıkarır. Bu nedenle yavaşlık teşhisinde önce verinin boyutunu, ardından her veri elemanının kaç kez işleme alındığını ve aramalarda hangi veri yapısının kullanıldığını incelemek gerekir.
Yavaşlık Kalıplarını Kodun İçinde Nasıl Tanırsınız?
Veri büyüdüğünde yavaşlayan bir kodu incelerken ilk soru, “Bu kod kaç saniyede çalışıyor?” değil, girdi büyüdükçe hangi işlem kaç kez tekrarlanıyor? olmalıdır. Big O analizi kesin çalışma süresini göstermez; kodun işlem sayısının veri boyutuna göre nasıl arttığını anlamaya yardımcı olur.
Pratik bir teşhis için her kod parçasında şu üç soruyu sırayla sorun:
- Dış döngü veya ana işlem ne kadar çalışıyor?
- İçerideki işlem her turda kaç kez tekrarlanıyor?
- Arama ya da üyelik kontrolü hangi veri yapısında yapılıyor?
Bu üç soruya verilen cevaplar, yavaşlığın döngüden mi, aramadan mı, yanlış veri yapısından mı yoksa gereksiz tekrar hesaplamadan mı kaynaklandığını çoğu zaman ortaya çıkarır.
İç içe döngüler: Her zaman O(n²) değildir
İç içe iki döngü gördüğünüzde kodu hemen O(n²) olarak etiketlemek sık yapılan bir hatadır. İki döngünün iç içe olması yalnızca işlemlerin birbirinin içinde çalıştığını gösterir. Karmaşıklığı belirlemek için her döngünün hangi veri boyutuna bağlı olduğunu ayrıca incelemek gerekir.
for urun in urunler:
for kategori in kategoriler:
kontrol_et(urun, kategori)
Burada dış döngü urunler listesindeki m eleman için, iç döngü ise kategoriler listesindeki n eleman için çalışıyorsa toplam işlem sayısı yaklaşık m × n olur. Bu nedenle karmaşıklık O(m·n) şeklinde ifade edilir.
Eğer iki listenin de boyutu birlikte n olarak düşünülüyorsa bu yapı pratikte O(n²) gibi görünebilir. Ancak ürün sayısı büyürken kategori sayısı sabit kalıyorsa sonuç farklıdır:
for urun in urunler:
for kategori in sabit_kategoriler:
kontrol_et(urun, kategori)
sabit_kategoriler her zaman örneğin 5 eleman içeriyorsa iç döngü 5 kez çalışır. Dış döngü n kez çalıştığı için toplam işlem sayısı yaklaşık 5n olur. Sabit katsayılar Big O gösteriminde göz ardı edildiğinden bu yapı O(n) kabul edilir; otomatik olarak O(n²) değildir.
Benzer şekilde, iç döngü dış döngüden bağımsız olarak sabit sayıda çalışıyorsa yine O(n²) sonucuna varılmaz. Teşhis sırasında “iki döngü var” demek yerine şu hesabı yapın:
- Dış döngü kaç kez çalışıyor?
- İç döngü her dış turda kaç kez çalışıyor?
- İç döngünün tekrar sayısı dış verinin boyutuna bağlı mı?
Aynı listeyi farklı bölümlerde tekrar tekrar taramak
Bazen kodda iç içe döngü bulunmaz; ancak aynı veri kümesi farklı işlemler için baştan sona tekrar taranır. Bu durum da veri büyüdüğünde beklenenden fazla işlem yapılmasına neden olabilir.
aktif_sayisi = sum(1 for kisi in kisiler if kisi["aktif"])
yas_sayisi = sum(1 for kisi in kisiler if kisi["yas"] >= 18)
email_sayisi = sum(1 for kisi in kisiler if kisi["email"])
Bu örnekte üç ayrı filtreleme işlemi vardır. Her işlem kisiler listesinin tamamını bir kez tarar. Liste n elemanlıysa toplam işlem yaklaşık 3n olur ve Big O açısından O(n) kabul edilir. Yani liste üç kez taranmasına rağmen bu yapı otomatik olarak O(n²) değildir.
Burada önemli ayrım, tarama sayısının sabit mi yoksa veriyle birlikte mi arttığıdır. Üç filtre sabit sayıda kaldığı sürece işlem sayısı doğrusal büyür. Fakat her kişi için listenin tamamını yeniden tarayan bir yapı oluşturursanız durum değişir:
for kisi in kisiler:
benzer_sayisi = 0
for diger in kisiler:
if kisi["sehir"] == diger["sehir"]:
benzer_sayisi += 1
Bu kodda dış döngü yaklaşık n kez, iç döngü de her dış turda yaklaşık n kez çalışır. Böylece karşılaştırma işlemi yaklaşık n × n kez yapılır ve yapı O(n²) olur. Aynı listeyi ayrı ayrı üç kez taramakla, her eleman için aynı listeyi tekrar taramak aynı şey değildir.
Döngü içinde liste araması yapmak
Yavaş kodun en sık görülen kalıplarından biri, bir listenin elemanlarını gezerken başka bir listede in ile üyelik kontrolü yapmaktır.
eslesenler = []
for siparis in siparisler:
if siparis["urun_id"] in stok_urunleri:
eslesenler.append(siparis)
stok_urunleri bir listeyse, Python eşleşen değeri bulmak için listeyi baştan sona tarayabilir. Bu arama tek seferde O(n) sürebilir. Dış döngü de siparisler listesindeki m eleman için çalışıyorsa toplam maliyet yaklaşık O(m·n) olur.
Özellikle iki listenin boyutu birbirine yakınsa bu yapı pratikte O(n²) davranışı gösterebilir. Sorun yalnızca dış döngüde değildir; her turun içinde yapılan üyelik kontrolünün de kendi maliyeti vardır. Bu nedenle bir döngüyü incelerken gövdesindeki işlemleri tek tek saymak gerekir.
Liste içinde arama yapan bazı kodlar küçük veri kümelerinde yeterince hızlı görünebilir. Ancak veri büyüdüğünde her turda baştan sona tarama yapılması belirginleşir. Bu tür durumlarda “döngü kaç kez dönüyor?” sorusuna ek olarak “döngünün içinde arama kaç adım sürebilir?” sorusunu da sorun.
Amaca uygun olmayan veri yapısını fark etmek
Bir veri yapısı seçerken yalnızca veriyi saklayıp saklayamadığına bakmak yeterli değildir. Veriye nasıl erişeceğiniz, tekrar eden değerlerin önemli olup olmadığı ve üyelik kontrolünün ne kadar sık yapılacağı da kararı etkiler.
- Liste: Sıralı veriyi korumak ve elemanları sırayla gezmek için kullanışlıdır. Belirli bir değerin üyeliğini sık sık kontrol etmek için her zaman ideal olmayabilir.
- Set: Benzersiz değerleri tutmak ve üyelik kontrolünü doğrudan ifade etmek için uygundur.
- Sözlük: Bir anahtardan ilişkili değere erişmek gerektiğinde daha anlamlıdır.
- Tuple: Değişmeyecek küçük veri gruplarını temsil etmek için tercih edilebilir; ancak tek başına her arama sorununu çözmez.
Örneğin kodun amacı “Bu kimlik daha önce görüldü mü?” sorusunu çok kez sormaksa, veriyi yalnızca sırayla gezmek için tasarlanmış bir listede tutmak yerine üyelik kontrolüne daha uygun bir yapı düşünülmelidir. Burada dikkat edilmesi gereken nokta, Big O değerinin veri yapısından bağımsız olmadığıdır. Aynı algoritma, kullandığı yapıya göre farklı davranabilir.
Sıralama ve tekrar eden hesaplamaları birlikte değerlendirmek
Döngü içinde sıralama yapmak, çoğu zaman gözden kaçan başka bir maliyet oluşturur.
for grup in gruplar:
sirali_notlar = sorted(grup["notlar"])
en_yuksek = sirali_notlar[-1]
Bu kodda her grubun notları ayrı ayrı sıralanır. Dış döngü grup sayısı kadar çalışır; iç maliyet ise her grubun not sayısına bağlıdır. Grupların eleman sayıları farklıysa tek bir n kullanarak düşünmek yanıltıcı olabilir. Toplam maliyet, her grubun kendi sıralama maliyetlerinin toplamıdır.
Ayrıca bir değer değişmediği hâlde döngü içinde tekrar hesaplanıyorsa gereksiz işlem yapılabilir:
for siparis in siparisler:
vergi_orani = vergi_orani_getir()
toplam = siparis["tutar"] * (1 + vergi_orani)
vergi_orani_getir() sonucu bütün siparişler için aynıysa bu çağrı döngünün dışına alınabilir. Bu değişiklik her zaman karmaşıklık sınıfını değiştirmeyebilir; fakat gereksiz tekrarları azaltarak gerçek çalışma süresini düşürebilir. Big O analizi ile mikro düzeydeki iyileştirmeleri birbirine karıştırmayın: biri büyüme biçimini, diğeri mevcut girdideki gereksiz maliyeti inceler.
Bu bölümdeki mini teşhis şablonunu her kod parçasına uygulayın:
- Dış döngü ne kadar çalışıyor?
- İç işlem veya arama her turda ne kadar çalışıyor?
- Bu tekrar sayısı aynı veri boyutuna mı, farklı bir boyuta mı bağlı?
- Üyelik kontrolü liste, set veya sözlük üzerinde mi yapılıyor?
- Sıralama ya da aynı hesap döngünün her turunda gereksiz yere tekrarlanıyor mu?
Yavaş Kodu İncelemek İçin Adım Adım Teşhis Kontrol Listesi

Kendi kodunuzda yavaşlık ararken rastgele satırları değiştirmek yerine sabit bir sıra izlemek daha güvenlidir. Önce büyüyen veri boyutunu belirleyin, sonra tekrar sayılarını hesaplayın, ardından veri yapısını ve gerçek çalışma süresini değerlendirin.
- n olarak hangi veri boyutunu takip ettiğinizi belirleyin.
Koddaki “girdi” tek bir sayı olmayabilir. Kullanıcı sayısı, sipariş sayısı, dosya satırı, ağ isteği, listedeki eleman sayısı veya bir metnin karakter uzunluğu farklı veri boyutlarıdır. Önce hangi boyut büyüdüğünde yavaşlığın ortaya çıktığını yazın. İki farklı liste birlikte büyüyorsa bunları tek bir
nkabul etmek yerinemvenolarak ayırmanız daha doğru olabilir. - Dış döngüleri sayın.
Her dış döngünün yaklaşık kaç kez çalıştığını belirleyin. Bir listeyi bir kez dolaşmak genellikle veri boyutuyla doğrusal ilişkilidir. Ancak döngünün gerçekten listenin tamamını mı, yoksa bir koşul sağlanana kadar yalnızca bir bölümünü mü gezdiğini de kontrol edin.
- İç içe döngülerde her boyutun bağımlılığını ayırın.
İç döngü dış döngünün her turunda aynı veri boyutunu mu tarıyor? Yoksa sabit uzunlukta bir yapı mı kullanıyor? Dış döngü
m, iç döngünkadar çalışıyorsa sonuç O(m·n) olabilir. İç döngü sabit sayıda çalışıyorsa dış döngüyle birlikte otomatik olarak O(n²) sonucu çıkarılmaz. - Döngü içinde arama, üyelik kontrolü veya sıralama olup olmadığını kontrol edin.
in,index,find,sortedve benzeri işlemleri özellikle işaretleyin. Bu işlemler tek başına küçük görünse de bir döngünün içinde tekrarlandıklarında toplam maliyeti büyütebilir. Aramanın liste, set veya sözlük üzerinde yapıldığını ayrıca not edin. - Aynı hesabın tekrar yapılıp yapılmadığını bulun.
Döngü içinde değişmeyen bir değer tekrar tekrar hesaplanıyor olabilir. Sabit bir dosya okuma, aynı yapılandırma değerini getirme, aynı dönüşümü yapma veya aynı listeyi sıralama buna örnektir. Hesap gerçekten her turda değişiyor mu, yoksa bir kez yapılıp sonucu kullanılabilir mi diye sorun.
- Kullanılan veri yapısının erişim ve üyelik davranışını sorgulayın.
Veri yapısını yalnızca alışkanlıkla seçmeyin. Sıralı dolaşma mı, hızlı üyelik kontrolü mü, anahtarla değer bulma mı gerekiyor? Gereksinim üyelik kontrolüyse liste seçiminin maliyetini, anahtar-değer erişimiyse sözlük kullanımının anlamlı olup olmadığını değerlendirin.
- Küçük ve daha büyük girdilerle gerçek süreyi ölçün.
Aynı kodu küçük bir veri kümesiyle ve belirgin biçimde daha büyük bir veri kümesiyle çalıştırın. Ölçümde yalnızca tek çalıştırmaya bakmak yerine benzer koşullarda karşılaştırma yapmaya çalışın. Hangi bölümün ne kadar sürdüğünü görmek için profil oluşturma veya zaman ölçme araçlarından yararlanabilirsiniz; ancak belirli bir aracın güncel davranışını varsaymadan kullandığınız yöntemin dokümantasyonunu kontrol edin.
- Bir değişikliği uygulayıp sonucu tekrar karşılaştırın.
Aynı anda birçok satırı değiştirmek teşhisi zorlaştırır. Önce yalnızca veri yapısını değiştirin, yalnızca tekrar eden hesabı dışarı alın veya yalnızca gereksiz aramayı azaltın. Daha sonra aynı girdilerle yeniden ölçün. İyileşme olduysa hangi değişikliğin etkili olduğunu anlayabilir, beklenmedik sonuçta önceki adıma geri dönebilirsiniz.
Bu kontrol listesinde ölçüm son sırada yer alır; çünkü süreyi ölçmeden önce kodun hangi işlemleri neden tekrarladığını anlamak gerekir. Yine de ölçüm önemlidir. Big O ve profiling farklı sorulara cevap verir: Big O, veri büyüdükçe işlem sayısının nasıl arttığını; profiling veya zaman ölçümü ise mevcut çalıştırmada zamanın hangi bölümde harcandığını gösterir.
Örneğin bir kod O(n) olabilir fakat döngü içinde pahalı bir dosya işlemi yaptığı için küçük girdilerde bile yavaş çalışabilir. Başka bir kod O(n²) olabilir ancak veri çok küçükken hızlı görünebilir. Bu nedenle önce büyüme biçimini analiz edin, sonra gerçek darboğazı ölçümle doğrulayın. En güvenilir teşhis, bu iki bakış açısının birlikte kullanılmasıdır.
O(n²) Aramayı Set Kullanarak O(n) Yaklaşıma Dönüştürme
Bir listenin her elemanının başka bir listede bulunup bulunmadığını kontrol etmek, veri miktarı büyüdüğünde sık karşılaşılan bir yavaşlık nedenidir. İlk listedeki her eleman için ikinci listeyi baştan sona tararsanız, iki listenin boyutlarına bağlı olarak yaklaşık O(n × m) işlem oluşur. Listelerin boyutları birbirine yakınsa bu yapı pratikte O(n²) gibi davranır.
Örneğin aşağıdaki yaklaşımda Python, ilk listedeki her değer için ikinci listedeki değerleri sırayla kontrol eder:
def ortak_naif(birinci, ikinci):
ortak = []
for deger in birinci:
if deger in ikinci:
ortak.append(deger)
return ortak
birinci = [3, 8, 12, 15]
ikinci = [1, 4, 8, 12, 20]
print(ortak_naif(birinci, ikinci))
Bu kodun çıktısı [8, 12] olur. Sonuç doğrudur; ancak deger in ikinci ifadesi ikinci veri yapısı bir liste olduğu için listedeki elemanları sırayla inceleyebilir. İlk listede n, ikinci listede m eleman varsa toplam maliyet yaklaşık O(n × m) olarak değerlendirilir.
Bu yaklaşım küçük listelerde fark edilmeyebilir. Fakat örneğin ilk listede 100.000, ikinci listede de 100.000 değer olduğunda, aynı ikinci listeyi tekrar tekrar taramak ciddi bir işlem yükü oluşturur. Buradaki sorun çoğu zaman döngünün varlığı değil, aynı aramanın her turda yeniden yapılmasıdır.
Set kullanımı arama maliyetini nasıl düşürür?
Üyelik kontrolünün temel amacı bir değerin veri kümesinde bulunup bulunmadığını hızlı biçimde öğrenmekse, ikinci listeyi bir sete dönüştürmek daha uygun olabilir. Set, tekrar eden değerleri tekilleştirir ve üyelik kontrolü için tasarlanmış bir veri yapısıdır. Ortalama durumda bir değerin sette bulunup bulunmadığını kontrol etmek O(1) davranışına yakındır.
Böylece ikinci listeyi bir kez dolaşıp set oluşturur, ardından ilk listeyi bir kez dolaşarak üyelik kontrolü yaparız:
def ortak_set_ile(birinci, ikinci):
ikinci_set = set(ikinci)
ortak = []
for deger in birinci:
if deger in ikinci_set:
ortak.append(deger)
return ortak
birinci = [3, 8, 12, 15]
ikinci = [1, 4, 8, 12, 20]
print(ortak_set_ile(birinci, ikinci))
Bu kodun beklenen çıktısı yine [8, 12] olur. İlk liste bir kez dolaşılır, ikinci liste ise set oluşturulurken bir kez taranır. Bu nedenle yaklaşımın toplam maliyeti yaklaşık O(n + m) olur. Önceki yöntemdeki gibi ikinci listeyi her eleman için baştan sona aramak yerine, üyelik kontrolüne uygun bir veri yapısından yararlanılır.
Bu dönüşüm, özellikle “Bu değer diğer listede var mı?” sorusunun çok kez sorulduğu durumlarda etkilidir. Örneğin kullanıcı kimliklerini izinli kullanıcılarla karşılaştırmak, işlenmiş kayıtları kontrol etmek veya bir ürün listesindeki kodların başka bir listede bulunup bulunmadığını aramak gibi işlemlerde aynı düşünce kullanılabilir.
Python temellerini bu tür veri yapısı ve algoritma kararlarıyla birlikte yapılandırarak ilerletmek isteyenler, birebir Python dersleri kapsamında üyelik kontrolleri, listeler, setler ve performans düşüncesi üzerine uygulamalı çalışma yapabilir.
Set dönüşümünün ödünleşimleri nelerdir?
Set kullanımı her durumda ücretsiz bir iyileştirme değildir. İkinci listenin set'e çevrilmesi ek bellek kullanır. Büyük verilerle çalışırken, oluşturulan set'in bellekte tutulması gereken alanı hesaba katılmalıdır.
Ayrıca set, tekrarları korumaz. İkinci listede aynı değer birkaç kez bulunuyorsa, bu tekrarlar set oluşturulduğunda tek bir değere dönüşür. Bu durum yalnızca “var mı, yok mu?” kontrolü yapan işlemler için genellikle sorun değildir; fakat tekrar sayısının önemli olduğu bir iş akışında farklı bir veri yapısı veya sayaç yaklaşımı gerekebilir.
Set'in kendi eleman sıralaması da liste gibi bir sonuç sırası anlamına gelmez. Yukarıdaki örnekte sonuç sırasını ilk listeyi dolaşma sırasına bağlı olarak koruyoruz; çünkü ortak değerleri ilk listeden seçip ortak listesine ekliyoruz. Ancak set'i doğrudan sonuç olarak kullanırsanız, sıralı çıktı beklentiniz karşılanmayabilir.
Bu nedenle karar verirken şu soruları sorun:
- Amacım yalnızca üyelik kontrolü yapmak mı?
- Tekrar eden değerleri korumam gerekiyor mu?
- Sonuç belirli bir sırada mı dönmeli?
- Ek bir set oluşturacak kadar belleğim var mı?
- Aynı aramayı kaç kez yapıyorum?
Karmaşıklığı Azaltma Stratejileri: Hangi İyileştirmeyi Seçmelisiniz?
Yavaş bir kodu hızlandırırken ilk akla gelen işlemi rastgele değiştirmek yerine, darboğazın neden oluştuğunu belirlemek gerekir. Her performans sorunu yeni bir veri yapısıyla çözülmez. Bazen aynı hesaplamayı tekrar tekrar yapmak, bazen gereksiz kayıtları işlemek, bazen de ihtiyaç duyulmayan alanları okumak asıl maliyeti oluşturur.
Pratik bir karar çerçevesi olarak önce döngülerin kaç kez çalıştığına, sonra aramaların nasıl yapıldığına, ardından kullanılan veri yapısına bakabilirsiniz. Daha sonra aynı işin tekrar edilip edilmediğini ve işlem başlamadan önce gereksiz verilerin elenip elenemeyeceğini inceleyin.
1. Veri yapısı seçimini gözden geçirin
Veri yapısı, yalnızca veriyi saklama biçimi değildir; veriye hangi hızda ve hangi kurallarla erişebileceğinizi de belirler. Python'da liste, set ve sözlük benzer görünen ihtiyaçlarda farklı kararlar gerektirir.
| İhtiyaç | Uygun yapı | Dikkat edilmesi gereken nokta |
|---|---|---|
| Sıralı veriyi korumak ve konuma göre erişmek | list | Üyelik kontrolü büyük veride tekrar tekrar yapılırsa tarama maliyeti artabilir. |
| Bir değerin bulunup bulunmadığını kontrol etmek | set | Tekrarlar korunmaz; ek bellek kullanılır. |
| Bir anahtardan ilişkili değere ulaşmak | dict | Anahtarların anlamlı ve benzersiz olması gerekir. |
Örneğin, “bu kullanıcı daha önce işlendi mi?” sorusu için bir listeyi her defasında taramak yerine set kullanılabilir. “Bu ürün koduna ait fiyat nedir?” sorusunda ise sözlük daha doğrudan bir seçimdir. Buna karşılık verilerin kullanıcıya belirli bir sırada gösterilmesi gerekiyorsa listeyi tamamen ortadan kaldırmak doğru olmayabilir.
2. Önceden hesaplayıp sonucu saklayın
Bir döngünün içinde her turda aynı sonucu üreten bir işlem varsa, bu işlemi döngünün dışına taşıyabilirsiniz. Böylece aynı hesaplama yalnızca bir kez yapılır ve sonuç sonraki adımlarda tekrar kullanılır.
Örneğin her kaydı işlerken değişmeyen bir dosya yolunu, sabit bir dönüşüm tablosunu veya filtreleme için kullanılan bir kümeyi yeniden oluşturmak gereksizdir. Bu değerleri döngüden önce hazırlayıp bir değişkende saklamak, hem işlem sayısını azaltır hem de kodun niyetini daha görünür kılar.
Burada önemli ayrım, gerçekten her turda değişen değerlerle sabit kalan değerleri ayırt etmektir. Bir hesaplamayı dışarı taşımak sonucu değiştirecekse bu optimizasyon doğru değildir. Performans kazancı uğruna programın doğruluğu bozulmamalıdır.
3. Tekrarları kaldırın
Aynı kaydı veya aynı işi birden fazla kez işlemek, veri büyüdükçe doğrudan maliyet üretir. Aynı dosya adı, kullanıcı kimliği, ürün kodu ya da istek parametresi tekrar ediyorsa, bu tekrarların iş akışında gerçekten gerekli olup olmadığını kontrol edin.
Tekrarları kaldırmak için ihtiyaca göre set kullanabilir, daha ayrıntılı tekrar sayılarına ihtiyaç varsa sözlükte sayaç tutabilir veya daha önce hesaplanan sonuçları anahtarlarıyla birlikte saklayabilirsiniz. Örneğin aynı parametrelerle yapılan bir hesaplamanın sonucu değişmiyorsa, her çağrıda işlemi baştan yapmak yerine önce daha önce üretilmiş bir sonuç olup olmadığı kontrol edilebilir.
Ancak tekrarları kaldırmadan önce tekrarın anlamını incelemek gerekir. Bir listenin aynı değeri iki kez içermesi bazen hatadır, bazen de iki ayrı işlemi temsil eder. Bu nedenle “tekrar eden her şeyi silmek” yerine, iş kuralının tekrarları nasıl yorumladığını belirleyin.
4. Gereksiz işlemleri azaltın
Bazen algoritmanın temel karmaşıklığını değiştirmeden de önemli miktarda gereksiz iş azaltılabilir. Bunun için mümkün olan filtreleri erken uygulayın. İşlenmesi gerekmeyen kayıtları döngünün ilerleyen adımlarına taşımayın.
- Koşulu sağlamayan kayıtları mümkün olduğunca erken eleyin.
- Döngü başlamadan önce sabit kalan koşulları değerlendirin.
- Bir kaydın yalnızca gerekli alanlarını okuyun veya dönüştürün.
- Sonucu kesinleşmiş işlemlerde döngüyü erken bitirmeyi değerlendirin.
- Aynı biçimlendirme veya dönüşüm işlemini gereksiz yere tekrarlamayın.
Örneğin yalnızca aktif kullanıcıları işlemek gerekiyorsa, pasif kayıtları tüm hesaplama adımlarından geçirdikten sonra elemek yerine başta filtrelemek daha anlamlıdır. Benzer şekilde, bir sonuç bulunduğunda aramaya devam etmek gerekmiyorsa döngüyü sürdürmek gereksiz işlem üretir.
Hangi stratejiyi seçeceğinizi nasıl belirlemelisiniz?
İyileştirme seçimini şu kısa kontrol listesiyle yapabilirsiniz:
- Veri yapısını kontrol edin: Sık üyelik kontrolü varsa set, anahtarla erişim varsa dict, sıra ve konum önemliyse list kullanmayı değerlendirin.
- Tekrarlanan hesabı arayın: Döngü içinde değişmeyen bir sonuç üretiliyorsa hesaplamayı önceye taşıyın.
- Tekrarlanan işi inceleyin: Aynı kayıt veya aynı parametre birden fazla işleniyorsa sonuçları saklamayı ya da tekrarları ayıklamayı değerlendirin.
- Gereksiz veriyi eleyin: Filtreleri erkene alın ve yalnızca ihtiyaç duyulan alanları işleyin.
- Doğruluğu yeniden kontrol edin: Sıra, tekrar, bellek ve çıktı biçimi gibi gereksinimlerin korunup korunmadığını test edin.
En hızlı görünen değişikliği değil, darboğazın karmaşıklığını düşüren ve kodun doğruluğunu koruyan değişikliği seçmek gerekir. O(n²) bir aramayı O(n + m) yaklaşımına çevirmek çoğu zaman güçlü bir adımdır; fakat yalnızca küçük bir döngü içindeki sabit ifadeyi taşımak, asıl darboğaz başka yerdeyse sınırlı etki yaratabilir.
Sık Sorulan Sorular
Big O kesin çalışma süresini saniye cinsinden gösterir mi?
Hayır. Big O, bir algoritmanın belirli bir bilgisayarda kaç saniyede çalışacağını kesin olarak söylemez. Girdi büyüklüğü arttıkça işlem sayısının hangi hızda arttığını açıklamak için kullanılır. Gerçek süre; donanım, Python sürümü, veri düzeni, uygulama ayrıntıları ve kullanılan kütüphaneler gibi etkenlere bağlı olarak değişebilir.
Her iç içe döngü mutlaka O(n²) midir?
Hayır. İç içe döngü görmek tek başına O(n²) sonucu vermez. İç döngü her dış döngü turunda aynı veri boyutunu tarıyorsa O(n²) olabilir; ancak iç döngü daha küçük bir veri üzerinde çalışabilir, her turda küçülebilir, sabit sayıda tekrar yapabilir veya iki farklı veri boyutuna bağlı olabilir. Karmaşıklığı belirlemek için döngülerin kaç kez ve hangi veri üzerinde çalıştığı incelenmelidir.
Python'da liste yerine set kullanmak her zaman daha mı hızlıdır?
Hayır. Set, özellikle üyelik kontrolünün çok tekrarlandığı durumlarda avantaj sağlar; ancak sıra korunması, tekrarların önemli olması veya konuma göre erişim gerektiğinde liste daha uygun olabilir. Set ayrıca ek bellek kullanır ve tekrar eden değerleri tekilleştirir. Doğru seçim, yapılacak işlemin gereksinimlerine göre belirlenmelidir.
Big O analizinden sonra kodun gerçek performansını nasıl ölçebilirim?
Önce farklı boyutlarda gerçekçi test verileri hazırlayın ve kodun çalışma süresini ölçün. Aynı kodu birden fazla kez çalıştırarak tek bir ölçümün oluşturabileceği dalgalanmayı azaltın. Ayrıca yalnızca toplam süreye değil, hangi fonksiyonun veya satırın zaman harcadığına da bakın. Big O büyüme eğilimini açıklar; ölçüm ise mevcut uygulamanın gerçek çalışma davranışını gösterir.
Yavaş kodu iyileştirmenin en sağlam yolu, önce tekrar eden işi ve veri erişim biçimini teşhis etmek, ardından sonucu ve bellek kullanımını yeniden kontrol etmektir.