Big O Notation (Algoritma Karmaşıklığı)¶
Genel Bakış¶
Big O Notation, bir algoritmanın performansını veya karmaşıklığını tanımlamak için kullanılan matematiksel bir gösterimdir. Giriş boyutu arttıkça bir algoritmanın çalışma süresinin (zaman karmaşıklığı) veya bellek kullanımının (alan karmaşıklığı) nasıl büyüdüğünü ifade eder.
Big O, algoritmanın en kötü senaryodaki davranışını temsil eder ve sabit katsayılar ile alt mertebeden terimler göz ardı edilir. Bu sayede farklı algoritmalar arasında apples-to-apples karşılaştırması yapılabilir.
Temel Karmaşıklık Sınıfları (Hızlıdan Yavaşa)¶
| Notation | İsim | Örnek |
|---|---|---|
| O(1) | Sabit | Dizi elemanına index ile erişim |
| O(log n) | Logaritmik | Binary search |
| O(n) | Lineer | Doğrusal arama |
| O(n log n) | Lineer-logaritmik | Merge sort, Quick sort (ort.) |
| O(n²) | Karesel | Bubble sort, Selection sort |
| O(2^n) | Üstel | Fibonacci (naive recursive) |
| O(n!) | Faktöriyel | Travelling Salesman (brute) |
Zaman Karmaşıklığı vs Alan Karmaşıklığı¶
- Zaman Karmaşıklığı (Time Complexity): Giriş boyutuna bağlı olarak algoritmanın kaç işlem yaptığını ölçer.
- Alan Karmaşıklığı (Space Complexity): Algoritmanın çalışması için gereken ekstra bellek miktarını ölçer.
Zaman ve alan karmaşıklığı arasında genellikle bir denge (trade-off) vardır: daha hızlı bir algoritma çoğunlukla daha fazla bellek kullanır.
Best Case, Average Case, Worst Case¶
- Best Case (En İyi Durum): Algoritmanın en hızlı çalıştığı senaryo. Omega (Ω) notasyonu ile gösterilir.
- Average Case (Ortalama Durum): Rastgele giriş için beklenen ortalama performans. Theta (Θ) notasyonu ile gösterilir.
- Worst Case (En Kötü Durum): Algoritmanın en yavaş çalıştığı senaryo. Big O (O) notasyonu ile gösterilir.
Mülakat Soruları ve Cevapları¶
1. Soru¶
Big O Notation nedir ve neden önemlidir? O(1), O(n) ve O(n²) arasındaki farkı bir C# örneğiyle açıklayın.
Cevap:
Big O Notation, bir algoritmanın ölçeklenebilirliğini ölçmek için kullanılan evrensel bir standarttır. Farklı makine hızlarından bağımsız olarak algoritmaları karşılaştırmamıza olanak tanır. Üç temel karmaşıklık:
- O(1) - Sabit: Giriş boyutundan bağımsız olarak her zaman aynı sürede çalışır.
- O(n) - Lineer: Giriş boyutuyla doğru orantılı büyür.
- O(n²) - Karesel: İç içe döngülerde ortaya çıkar; giriş iki katına çıkarsa çalışma süresi dört katına çıkar.
Örnek Kod:
using System;
using System.Collections.Generic;
public class BigOExamples
{
// O(1) - Sabit Karmaşıklık
// Giriş boyutundan bağımsız olarak her zaman tek bir işlem yapar.
public static int GetFirstElement(int[] array)
{
return array[0]; // Her zaman 1 adım, O(1)
}
// O(1) - Dictionary erişimi de sabit zamanlıdır
public static bool ContainsKey(Dictionary<string, int> dict, string key)
{
return dict.ContainsKey(key); // Hash tablosu nedeniyle O(1) ortalama
}
// O(n) - Lineer Karmaşıklık
// n eleman için n adım gerekir.
public static int FindMax(int[] array)
{
int max = array[0];
foreach (int item in array) // n kez döner
{
if (item > max)
max = item;
}
return max;
}
// O(n²) - Karesel Karmaşıklık
// İç içe iki döngü: n * n = n² adım
public static void BubbleSort(int[] array)
{
int n = array.Length;
for (int i = 0; i < n - 1; i++) // n kez döner
{
for (int j = 0; j < n - i - 1; j++) // n kez döner
{
if (array[j] > array[j + 1])
{
// Swap
(array[j], array[j + 1]) = (array[j + 1], array[j]);
}
}
}
}
public static void Demo()
{
int[] data = { 64, 34, 25, 12, 22, 11, 90 };
Console.WriteLine($"İlk eleman: {GetFirstElement(data)}"); // O(1)
Console.WriteLine($"Maksimum: {FindMax(data)}"); // O(n)
BubbleSort(data); // O(n²)
Console.WriteLine($"Sıralı: {string.Join(", ", data)}");
}
}
2. Soru¶
O(log n) karmaşıklığı neden bu kadar verimlidir? Binary Search algoritmasını C# ile implement edin.
Cevap:
O(log n), her adımda problem boyutunu yarıya indiren algoritmalarda ortaya çıkar. Bu, n = 1.000.000 eleman için yalnızca yaklaşık 20 adım anlamına gelir (log₂(1.000.000) ≈ 20). Binary Search, sıralı bir dizide çalışır ve her karşılaştırmada arama alanını yarıya böler.
Örnek Kod:
using System;
public class BinarySearchExample
{
// O(log n) - İteratif Binary Search
public static int BinarySearch(int[] sortedArray, int target)
{
int left = 0;
int right = sortedArray.Length - 1;
while (left <= right)
{
int mid = left + (right - left) / 2; // Overflow'u önlemek için
if (sortedArray[mid] == target)
return mid; // Bulundu: O(1) en iyi durum
if (sortedArray[mid] < target)
left = mid + 1; // Sağ yarıya bak
else
right = mid - 1; // Sol yarıya bak
}
return -1; // Bulunamadı
}
// O(log n) - Recursive Binary Search
public static int BinarySearchRecursive(int[] sortedArray, int target, int left, int right)
{
if (left > right)
return -1;
int mid = left + (right - left) / 2;
if (sortedArray[mid] == target)
return mid;
if (sortedArray[mid] < target)
return BinarySearchRecursive(sortedArray, target, mid + 1, right);
else
return BinarySearchRecursive(sortedArray, target, left, mid - 1);
}
public static void Demo()
{
int[] sorted = { 2, 5, 8, 12, 16, 23, 38, 56, 72, 91 };
int target = 23;
int index = BinarySearch(sorted, target);
Console.WriteLine($"{target} değeri index {index}'te bulundu."); // index 5
// Karşılaştırma: 10 elemanlı dizide Binary Search en fazla 4 adım atar
// Linear Search ise en kötü durumda 10 adım atar
}
}
3. Soru¶
.NET koleksiyonlarının (List\<T>, Dictionary\<T,V>, HashSet\<T>, LinkedList\<T>) zaman karmaşıklıklarını karşılaştırın. Hangi durumda hangisini kullanırsınız?
Cevap:
Her koleksiyonun farklı operasyonlar için farklı karmaşıklıkları vardır. Doğru veri yapısını seçmek performans açısından kritiktir.
| Operasyon | List\<T> | Dictionary\<K,V> | HashSet\<T> | LinkedList\<T> | SortedSet\<T> |
|---|---|---|---|---|---|
| Erişim (index) | O(1) | O(1)* | - | O(n) | O(log n) |
| Arama | O(n) | O(1)* | O(1)* | O(n) | O(log n) |
| Ekleme (sona) | O(1)* | O(1)* | O(1)* | O(1) | O(log n) |
| Ekleme (başa) | O(n) | - | - | O(1) | O(log n) |
| Silme | O(n) | O(1)* | O(1)* | O(1)** | O(log n) |
| İçerme kontrolü | O(n) | O(1)* | O(1)* | O(n) | O(log n) |
*Ortalama durum (amortized veya hash çakışması olmadığında) **Node referansı bilindiğinde
Örnek Kod:
using System;
using System.Collections.Generic;
using System.Diagnostics;
public class CollectionComplexity
{
public static void CompareCollections()
{
int n = 1_000_000;
var list = new List<int>();
var hashSet = new HashSet<int>();
var dictionary = new Dictionary<int, string>();
var linkedList = new LinkedList<int>();
var sortedSet = new SortedSet<int>();
// Veri doldurma
for (int i = 0; i < n; i++)
{
list.Add(i);
hashSet.Add(i);
dictionary[i] = $"value_{i}";
linkedList.AddLast(i);
sortedSet.Add(i);
}
int searchTarget = 999_999;
var sw = Stopwatch.StartNew();
// List<T> - O(n) arama
sw.Restart();
bool listResult = list.Contains(searchTarget);
sw.Stop();
Console.WriteLine($"List<T> Contains: {sw.ElapsedMicroseconds} µs (O(n))");
// HashSet<T> - O(1) ortalama arama
sw.Restart();
bool hashSetResult = hashSet.Contains(searchTarget);
sw.Stop();
Console.WriteLine($"HashSet<T> Contains: {sw.ElapsedMicroseconds} µs (O(1) avg)");
// Dictionary<K,V> - O(1) ortalama arama
sw.Restart();
bool dictResult = dictionary.ContainsKey(searchTarget);
sw.Stop();
Console.WriteLine($"Dictionary ContainsKey:{sw.ElapsedMicroseconds} µs (O(1) avg)");
// SortedSet<T> - O(log n) arama
sw.Restart();
bool sortedResult = sortedSet.Contains(searchTarget);
sw.Stop();
Console.WriteLine($"SortedSet<T> Contains: {sw.ElapsedMicroseconds} µs (O(log n))");
}
// Doğru koleksiyonu seçme rehberi
public static void DataStructureSelectionGuide()
{
// Senaryo 1: Sık sık "içeriyor mu?" sorgusu yapılacaksa → HashSet<T>
var visitedUrls = new HashSet<string>();
visitedUrls.Add("https://example.com");
bool alreadyVisited = visitedUrls.Contains("https://example.com"); // O(1)
// Senaryo 2: Key-value eşleştirmesi gerekiyorsa → Dictionary<K,V>
var userCache = new Dictionary<int, string>();
userCache[1] = "Ahmet";
string userName = userCache[1]; // O(1)
// Senaryo 3: Sıralı veri ve hızlı arama gerekiyorsa → SortedSet<T>
var leaderboard = new SortedSet<int> { 100, 250, 75, 300 };
// Otomatik sıralı: 75, 100, 250, 300
// Senaryo 4: Başa/sona sık ekleme/silme yapılacaksa → LinkedList<T>
var recentItems = new LinkedList<string>();
recentItems.AddFirst("en yeni"); // O(1)
recentItems.AddLast("en eski"); // O(1)
recentItems.RemoveFirst(); // O(1)
// Senaryo 5: Index ile erişim ve sona ekleme gerekiyorsa → List<T>
var items = new List<string>();
items.Add("item"); // O(1) amortized
string first = items[0]; // O(1)
}
}
4. Soru¶
LINQ operasyonlarının zaman karmaşıklıkları nedir? Where, OrderBy, First, Contains gibi metotları karşılaştırın.
Cevap:
LINQ metotları ertelenmiş (deferred) veya anında (immediate) çalışabilir. Karmaşıklıkları, kullandıkları iç algoritmaya bağlıdır.
| LINQ Metodu | Karmaşıklık | Açıklama |
|---|---|---|
Where |
O(n) | Her elemanı bir kez tarar |
Select |
O(n) | Her elemanı bir kez dönüştürür |
First/FirstOrDefault |
O(n) | En kötü durumda tüm koleksiyonu tarar |
OrderBy |
O(n log n) | Quicksort/Mergesort tabanlı |
Contains (IEnumerable) |
O(n) | Doğrusal arama |
Contains (HashSet) |
O(1) | Hash tablosu kullanır |
GroupBy |
O(n) | Hash tablosu ile gruplar |
Distinct |
O(n) | HashSet kullanır |
Count |
O(1) veya O(n) | ICollection ise O(1), değilse O(n) |
ToList/ToArray |
O(n) | Tüm koleksiyonu materializes eder |
Örnek Kod:
using System;
using System.Collections.Generic;
using System.Linq;
public class LinqComplexity
{
public static void LinqExamples()
{
var numbers = Enumerable.Range(1, 1_000_000).ToList();
// O(n) - Where: tüm koleksiyonu tarar
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// O(n log n) - OrderBy: sıralama algoritması kullanır
var sorted = numbers.OrderBy(n => n).ToList();
// O(n) - First ile O(1) farkı:
// İlk eleman için First() O(1) pratik olarak, ama worst case O(n)
var firstEven = numbers.First(n => n % 2 == 0); // Pratik O(1)
// UYARI: Bu pattern O(n log n) + O(n) = O(n log n)
var topFive = numbers
.OrderByDescending(n => n) // O(n log n)
.Take(5) // O(1)
.ToList();
// Daha verimli alternatif - eğer sadece max n değer lazımsa:
// PriorityQueue veya özel algoritma kullanın
}
// LINQ'da performans tuzakları
public static void LinqPerformancePitfalls()
{
var users = new List<User>
{
new User { Id = 1, Name = "Ahmet" },
new User { Id = 2, Name = "Mehmet" },
// ... binlerce kullanıcı
};
// KÖTÜ: O(n) her Contains çağrısı için, toplam O(n²)
var targetIds = new List<int> { 1, 2, 3, 4, 5 };
var slowResult = users.Where(u => targetIds.Contains(u.Id)).ToList();
// targetIds.Contains() her u için O(n) çalışır → O(n * m)
// İYİ: HashSet kullanarak O(n + m)
var targetIdSet = new HashSet<int>(targetIds); // O(m)
var fastResult = users.Where(u => targetIdSet.Contains(u.Id)).ToList(); // O(n)
// KÖTÜ: Defalarca Count() çağırmak
var query = users.Where(u => u.Id > 0);
if (query.Count() > 0) // O(n) - tüm koleksiyonu sayar
{
var first = query.First(); // O(n) - tekrar tarar!
}
// İYİ: Any() kullanmak - ilk eşleşmede durur
if (query.Any()) // O(1) en iyi durumda
{
var first = query.First();
}
}
private class User { public int Id { get; set; } public string Name { get; set; } }
}
5. Soru¶
İç içe döngüler ve recursive fonksiyonların karmaşıklığını nasıl analiz edersiniz? Fibonacci örneği üzerinden açıklayın.
Cevap:
Karmaşıklık analizi için temel kurallar: - Sıralı işlemler: Karmaşıklıkları toplanır, büyük olan alınır → O(n) + O(n²) = O(n²) - İç içe döngüler: Karmaşıklıklar çarpılır → O(n) * O(n) = O(n²) - Recursive fonksiyonlar: Recurrence relation analizi yapılır
Fibonacci örneği, üstel ve lineer karmaşıklık farkını dramatik biçimde gösterir.
Örnek Kod:
using System;
using System.Collections.Generic;
public class ComplexityAnalysis
{
// O(2^n) - Naive recursive Fibonacci
// Her çağrı iki yeni çağrı yaratır: 2^n toplam çağrı
public static long FibonacciNaive(int n)
{
if (n <= 1) return n;
return FibonacciNaive(n - 1) + FibonacciNaive(n - 2);
// F(5) için: F(4)+F(3) → F(3)+F(2)+F(2)+F(1) → ...
// Çakışan alt problemler tekrar hesaplanır!
}
// O(n) - Memoization ile Fibonacci (Top-Down Dynamic Programming)
// Her alt problem yalnızca bir kez hesaplanır
private static readonly Dictionary<int, long> _memo = new();
public static long FibonacciMemo(int n)
{
if (n <= 1) return n;
if (_memo.ContainsKey(n)) return _memo[n]; // O(1) cache hit
_memo[n] = FibonacciMemo(n - 1) + FibonacciMemo(n - 2);
return _memo[n];
}
// O(n) - Iteratif Fibonacci (Bottom-Up DP)
// Space complexity: O(1) - sadece 2 değişken tutulur
public static long FibonacciIterative(int n)
{
if (n <= 1) return n;
long prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++)
{
long current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
// İç içe döngü analizi
public static void NestedLoopExamples()
{
int n = 100;
// O(n²) - İki iç içe n döngü
for (int i = 0; i < n; i++) // n kez
for (int j = 0; j < n; j++) // n kez
Console.Write("."); // Toplam: n²
// O(n²/2) = O(n²) - Üçgen şekli
for (int i = 0; i < n; i++)
for (int j = i; j < n; j++) // i'den başlar, ama hala O(n²)
Console.Write(".");
// O(n³) - Üç iç içe döngü
for (int i = 0; i < n; i++) // n kez
for (int j = 0; j < n; j++) // n kez
for (int k = 0; k < n; k++) // n kez
Console.Write("."); // Toplam: n³
// O(n log n) - Dış döngü lineer, iç döngü logaritmik
for (int i = 0; i < n; i++) // n kez
for (int j = 1; j < n; j = j * 2) // log n kez (j ikiye katlanır)
Console.Write("."); // Toplam: n log n
}
public static void PerformanceComparison()
{
Console.WriteLine("Fibonacci karşılaştırması:");
Console.WriteLine($"F(10) Naive: {FibonacciNaive(10)}");
Console.WriteLine($"F(10) Memo: {FibonacciMemo(10)}");
Console.WriteLine($"F(10) Iterative: {FibonacciIterative(10)}");
// F(50) için Naive yaklaşık 2^50 = 1 quadrilyon işlem gerektirir!
// Memo ve Iteratif sadece 50 adımda tamamlar.
Console.WriteLine($"F(50) Memo: {FibonacciMemo(50)}");
Console.WriteLine($"F(50) Iterative: {FibonacciIterative(50)}");
}
}
6. Soru¶
Amortized complexity (itfa edilmiş karmaşıklık) nedir? List\<T>'nin dinamik boyutlanmasını örnek verin.
Cevap:
Amortized complexity, bir dizi işlemin ortalama maliyetini ifade eder. Bazı operasyonlar nadiren pahalı olsa da, genel ortalama hala düşük kalır. List<T> dinamik yeniden boyutlanması buna klasik bir örnektir.
Örnek Kod:
using System;
using System.Collections.Generic;
public class AmortizedComplexity
{
// List<T>'nin iç davranışını simüle eden sınıf
public class SimpleList<T>
{
private T[] _array;
private int _count;
private int _resizeCount; // Kaç kez yeniden boyutlandırıldı
private long _totalCopyOperations; // Toplam kopyalama işlemi
public SimpleList(int initialCapacity = 4)
{
_array = new T[initialCapacity];
_count = 0;
}
// Add metodu: Genellikle O(1), bazen O(n) (resize sırasında)
// Amortized: O(1)
public void Add(T item)
{
if (_count == _array.Length)
{
Resize(); // O(n) ama nadiren çağrılır
}
_array[_count++] = item; // O(1) - çoğu zaman bu adım
}
private void Resize()
{
int newCapacity = _array.Length * 2; // Kapasiteyi iki katına çıkar
T[] newArray = new T[newCapacity];
// Mevcut elemanları kopyala: O(n)
for (int i = 0; i < _count; i++)
{
newArray[i] = _array[i];
_totalCopyOperations++;
}
_array = newArray;
_resizeCount++;
Console.WriteLine($" Resize! Yeni kapasite: {newCapacity}");
}
public void PrintStats(int totalAdded)
{
Console.WriteLine($"Toplam eklenen: {totalAdded}");
Console.WriteLine($"Resize sayısı: {_resizeCount}");
Console.WriteLine($"Toplam kopya işlemi: {_totalCopyOperations}");
Console.WriteLine($"Kopya / Ekleme oranı: {(double)_totalCopyOperations / totalAdded:F2}");
Console.WriteLine($"Amortized maliyet: O(1) (oran 2.0'dan az)");
}
}
public static void Demo()
{
Console.WriteLine("=== List<T> Amortized Complexity Demo ===");
var list = new SimpleList<int>(initialCapacity: 4);
int n = 32;
for (int i = 0; i < n; i++)
{
list.Add(i);
}
list.PrintStats(n);
/*
Çıktı analizi (kapasite 4 ile başlayıp, her seferinde 2 katına çıkar):
- Kapasite 4→8: 4 eleman kopyalanır
- Kapasite 8→16: 8 eleman kopyalanır
- Kapasite 16→32: 16 eleman kopyalanır
Toplam kopya: 4+8+16 = 28 işlem, 32 ekleme için
Ortalama: 28/32 = 0.875 kopya/ekleme → O(1) amortized
Matematiksel ispat: n eleman için toplam kopya = n + n/2 + n/4 + ... ≈ 2n = O(n)
Her ekleme için ortalama maliyet = O(n)/n = O(1)
*/
}
// Gerçek List<T> kapasite davranışı
public static void RealListCapacity()
{
var list = new List<int>();
Console.WriteLine("Eleman Sayısı | Kapasite");
for (int i = 0; i < 20; i++)
{
list.Add(i);
Console.WriteLine($"{list.Count,13} | {list.Capacity}");
}
// List başlangıç kapasitesi bilinen durumda belirtmek daha verimli:
var optimizedList = new List<int>(capacity: 1000); // Resize olmaz
for (int i = 0; i < 1000; i++)
optimizedList.Add(i); // Her zaman O(1), hiç resize yok
}
}
7. Soru¶
Queue\<T> ve Stack\<T> koleksiyonlarının karmaşıklıklarını açıklayın. BFS ve DFS algoritmalarında nasıl kullanılırlar?
Cevap:
Queue<T> (Kuyruk) FIFO (First In, First Out) yapısıdır ve BFS (Breadth-First Search) için idealdir. Stack<T> ise LIFO (Last In, First Out) yapısıdır ve DFS (Depth-First Search) için kullanılır. Her ikisinin temel operasyonları O(1) amortized karmaşıklığa sahiptir.
| Operasyon | Queue\<T> | Stack\<T> |
|---|---|---|
| Enqueue/Push | O(1) amortized | O(1) amortized |
| Dequeue/Pop | O(1) | O(1) |
| Peek | O(1) | O(1) |
| Contains | O(n) | O(n) |
Örnek Kod:
using System;
using System.Collections.Generic;
public class GraphTraversal
{
// BFS - Queue kullanır - O(V + E) karmaşıklık
// V: düğüm sayısı, E: kenar sayısı
public static List<int> BreadthFirstSearch(
Dictionary<int, List<int>> graph, int start)
{
var visited = new HashSet<int>(); // O(1) lookup için
var queue = new Queue<int>(); // FIFO yapısı
var order = new List<int>();
queue.Enqueue(start); // O(1)
visited.Add(start); // O(1)
while (queue.Count > 0)
{
int node = queue.Dequeue(); // O(1)
order.Add(node);
foreach (int neighbor in graph[node])
{
if (!visited.Contains(neighbor)) // O(1) HashSet lookup
{
visited.Add(neighbor); // O(1)
queue.Enqueue(neighbor); // O(1)
}
}
}
return order;
}
// DFS - Stack kullanır - O(V + E) karmaşıklık
public static List<int> DepthFirstSearch(
Dictionary<int, List<int>> graph, int start)
{
var visited = new HashSet<int>();
var stack = new Stack<int>(); // LIFO yapısı
var order = new List<int>();
stack.Push(start); // O(1)
while (stack.Count > 0)
{
int node = stack.Pop(); // O(1)
if (!visited.Contains(node))
{
visited.Add(node);
order.Add(node);
foreach (int neighbor in graph[node])
{
if (!visited.Contains(neighbor))
stack.Push(neighbor); // O(1)
}
}
}
return order;
}
public static void Demo()
{
// Graf: 0-1, 0-2, 1-3, 1-4, 2-5
var graph = new Dictionary<int, List<int>>
{
{ 0, new List<int> { 1, 2 } },
{ 1, new List<int> { 0, 3, 4 } },
{ 2, new List<int> { 0, 5 } },
{ 3, new List<int> { 1 } },
{ 4, new List<int> { 1 } },
{ 5, new List<int> { 2 } }
};
var bfsOrder = BreadthFirstSearch(graph, 0);
Console.WriteLine($"BFS: {string.Join(" → ", bfsOrder)}"); // 0 → 1 → 2 → 3 → 4 → 5
var dfsOrder = DepthFirstSearch(graph, 0);
Console.WriteLine($"DFS: {string.Join(" → ", dfsOrder)}"); // 0 → 2 → 5 → 1 → 4 → 3
}
}
8. Soru¶
Doğru veri yapısını seçmek uygulama performansını nasıl etkiler? Gerçek dünya senaryosu üzerinden O(n²) yerine O(n) çözüm üretin.
Cevap:
Yanlış veri yapısı seçimi, teoride çalışan ama pratikte ölçeklenemeyen kod üretir. Aşağıdaki örnek, "iki listede ortak elemanları bul" problemini üç farklı karmaşıklıkla gösterir.
Örnek Kod:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
public class RealWorldOptimization
{
// SENARYO: E-ticaret sitesinde kullanıcı sepetindeki ürünlerin
// indirimli ürünler listesinde olup olmadığını kontrol et.
// YAKLAŞIM 1: O(n * m) - İki iç içe döngü
public static List<int> FindDiscountedItemsNaive(
List<int> cartItems, List<int> discountedItems)
{
var result = new List<int>();
foreach (int cartItem in cartItems) // n kez
{
foreach (int discItem in discountedItems) // m kez
{
if (cartItem == discItem)
{
result.Add(cartItem);
break;
}
}
}
return result; // Toplam: O(n * m)
}
// YAKLAŞIM 2: O(n + m) - HashSet kullanarak
public static List<int> FindDiscountedItemsOptimized(
List<int> cartItems, List<int> discountedItems)
{
// İndirimli ürünleri HashSet'e koy: O(m)
var discountSet = new HashSet<int>(discountedItems);
// Her sepet ürünü için O(1) lookup
return cartItems.Where(item => discountSet.Contains(item)).ToList(); // O(n)
// Toplam: O(n + m)
}
// YAKLAŞIM 3: LINQ Intersect - HashSet tabanlı, O(n + m)
public static List<int> FindDiscountedItemsLinq(
List<int> cartItems, List<int> discountedItems)
{
return cartItems.Intersect(discountedItems).ToList(); // O(n + m)
}
// SENARYO 2: Log dosyasından benzersiz IP adreslerini bul
public static void FindUniqueIPs()
{
// Simüle edilmiş log
var logEntries = new List<string>
{
"192.168.1.1 GET /api/users",
"10.0.0.1 POST /api/orders",
"192.168.1.1 GET /api/products",
"10.0.0.2 GET /api/users",
"10.0.0.1 DELETE /api/cart"
};
// KÖTÜ: List.Contains() ile O(n²)
var uniqueIpsSlowList = new List<string>();
foreach (var entry in logEntries)
{
string ip = entry.Split(' ')[0];
if (!uniqueIpsSlowList.Contains(ip)) // O(n) her seferinde!
uniqueIpsSlowList.Add(ip);
}
// İYİ: HashSet ile O(n)
var uniqueIpsFast = new HashSet<string>();
foreach (var entry in logEntries)
{
string ip = entry.Split(' ')[0];
uniqueIpsFast.Add(ip); // Duplicate'ları otomatik ignore eder, O(1)
}
Console.WriteLine($"Benzersiz IP sayısı: {uniqueIpsFast.Count}");
}
public static void BenchmarkComparison()
{
int n = 10_000;
int m = 10_000;
var cartItems = Enumerable.Range(1, n).ToList();
var discountedItems = Enumerable.Range(5_000, m).ToList();
var sw = Stopwatch.StartNew();
sw.Restart();
var naiveResult = FindDiscountedItemsNaive(cartItems, discountedItems);
sw.Stop();
Console.WriteLine($"O(n*m) Naive: {sw.ElapsedMilliseconds} ms, {naiveResult.Count} eşleşme");
sw.Restart();
var optimizedResult = FindDiscountedItemsOptimized(cartItems, discountedItems);
sw.Stop();
Console.WriteLine($"O(n+m) HashSet: {sw.ElapsedMilliseconds} ms, {optimizedResult.Count} eşleşme");
sw.Restart();
var linqResult = FindDiscountedItemsLinq(cartItems, discountedItems);
sw.Stop();
Console.WriteLine($"O(n+m) Intersect: {sw.ElapsedMilliseconds} ms, {linqResult.Count} eşleşme");
/*
Tipik sonuçlar (n=m=10.000):
O(n*m) Naive: ~500 ms
O(n+m) HashSet: ~2 ms
O(n+m) Intersect: ~3 ms
n=m=100.000 için Naive yaklaşık 50.000 ms (50 saniye!) sürer
HashSet yaklaşımı hala ~20 ms sürer
*/
}
}
Özet: Big O Karmaşıklık Kılavuzu¶
Hangi Yapıyı Ne Zaman Seçmeli?¶
// 1. Hızlı arama (O(1)) gerekiyorsa:
var set = new HashSet<T>(); // Benzersiz elemanlar
var dict = new Dictionary<K, V>(); // Key-value çiftleri
// 2. Sıralı veri ve O(log n) arama gerekiyorsa:
var sortedSet = new SortedSet<T>();
var sortedDict = new SortedDictionary<K, V>();
// veya sıralı dizide Array.BinarySearch()
// 3. Sıraya göre işleme (FIFO) gerekiyorsa:
var queue = new Queue<T>(); // BFS, işlem kuyruğu
// 4. Ters sıraya işleme (LIFO) gerekiyorsa:
var stack = new Stack<T>(); // DFS, geri alma işlemleri
// 5. Başa/sona sık ekleme/silme gerekiyorsa:
var linkedList = new LinkedList<T>(); // O(1) başa ekleme
// 6. Index erişimi ve sona ekleme gerekiyorsa:
var list = new List<T>(); // Genel amaçlı, O(1) index erişim
Karmaşıklık Analizi İpuçları¶
- Sabit faktörleri görmezden gel: O(2n) = O(n), O(500) = O(1)
- Alt mertebe terimleri at: O(n² + n) = O(n²)
- İç içe döngülerde çarp: İki O(n) döngü → O(n²)
- Sıralı bloklarda topla, büyüğünü al: O(n) + O(n²) = O(n²)
- Recursive fonksiyonlarda recurrence ilişkisi kur: T(n) = 2T(n/2) + O(n) → O(n log n)
- LINQ'da dikkatli ol:
OrderByO(n log n),ContainsO(n) (liste üzerinde) - Amortized analizi unut:
List<T>.Add()O(1) amortized, ama bazen O(n)