Takip et

LeetCode #1: Two Sum Problemi ve Klasik Hash Map Çözümü – Detaylı Bir Analiz

LeetCode’un ilk problemi olan “Two Sum”, algoritmik düşünmenin temel taşlarından biridir. Bu makalede, Mahdi Shamlou’nun da üzerinde durduğu gibi, pro…

LeetCode #1: Two Sum Problemi ve Klasik Hash Map Çözümü – Detaylı Bir Analiz

LeetCode’un ilk problemi olan “Two Sum”, algoritmik düşünmenin temel taşlarından biridir. Bu makalede, Mahdi Shamlou’nun da üzerinde durduğu gibi, problemin en verimli çözümü olan klasik hash map yaklaşımını detaylıca ele alacağız. Amacımız, çözümün mantığını, performansını ve önemini kapsamlı bir şekilde açıklamaktır.

LeetCode ve Algoritma Problemlerine Giriş

LeetCode Nedir ve Neden Önemlidir?

LeetCode, yazılım geliştiricilerin algoritmik problem çözme yeteneklerini geliştirmeleri, veri yapıları bilgilerini pekiştirmeleri ve teknik mülakatlara hazırlanmaları için tasarlanmış çevrimiçi bir platformdur. Binlerce farklı zorluk seviyesinde problem barındırır ve kullanıcıların farklı programlama dillerinde çözümlerini test etmelerine olanak tanır. LeetCode’da pratik yapmak, hem teorik bilgiyi pekiştirir hem de gerçek dünya mühendislik problemlerine yaklaşım yeteneğini artırır.

Algoritmik Düşünmenin Temelleri

Algoritmik düşünme, bir problemi adım adım çözme, verimli yöntemler geliştirme ve bu yöntemlerin karmaşıklığını analiz etme becerisidir. Bu, sadece kod yazmaktan öte, problemi farklı açılardan ele almayı, olası çözümleri değerlendirmeyi ve en uygun olanı seçmeyi içerir. Two Sum gibi temel problemler, bu düşünme biçimini geliştirmek için mükemmel başlangıç noktalarıdır.

İlk Adım: Two Sum Problemi

Two Sum, LeetCode’un en bilinen ve en sık karşılaşılan problemlerinden biridir. Genellikle mülakatlarda adayların temel algoritmik yeteneklerini ölçmek için kullanılır. Problem, basit bir tanıma sahip olsa da, farklı çözüm yaklaşımlarının performans üzerindeki etkisini net bir şekilde gösterir.

Two Sum Problemini Detaylı İnceleme

Problem Tanımı ve Kısıtlamalar

Two Sum problemi şu şekildedir: Size bir tam sayı dizisi (nums) ve bir hedef tam sayı (target) verilir. Dizideki hangi iki sayının toplamının hedefe eşit olduğunu bulmanız ve bu iki sayının dizideki indekslerini döndürmeniz istenir. Her girişin tam olarak bir çözümü olduğu varsayılır ve aynı elemanı iki kez kullanamazsınız.

  • nums dizisi 2 ile 104 eleman arasında olabilir.
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • Yalnızca bir geçerli çözüm vardır.

Örnek Senaryolar ve Beklenen Çıktılar

Problemi daha iyi anlamak için birkaç örnek inceleyelim:


# Örnek 1:
nums = [2, 7, 11, 15]
target = 9
# Beklenen Çıktı: [0, 1]
# Çünkü nums[0] + nums[1] == 2 + 7 == 9.

# Örnek 2:
nums = [3, 2, 4]
target = 6
# Beklenen Çıktı: [1, 2]
# Çünkü nums[1] + nums[2] == 2 + 4 == 6.

# Örnek 3:
nums = [3, 3]
target = 6
# Beklenen Çıktı: [0, 1]
# Çünkü nums[0] + nums[1] == 3 + 3 == 6.

Neden Bu Problem Temel Bir Köşe Taşıdır?

Two Sum, temel arama algoritmaları, veri yapıları (özellikle hash map'ler) ve zaman/alan karmaşıklığı analizi gibi kavramları tanıtmak için idealdir. Birçok karmaşık problemin temelinde yatan prensipleri barındırır ve farklı çözüm yaklaşımlarının performans üzerindeki dramatik etkisini açıkça gösterir.

Naif Yaklaşım: Brute Force ve Performans Dezavantajları

Brute Force Çözümünün Mantığı

Bir problemi çözmek için akla gelen ilk ve en basit yöntem genellikle "brute force" (kaba kuvvet) yaklaşımıdır. Two Sum için brute force, dizideki her sayıyı alıp, kalan tüm sayılarla toplayarak hedef toplamı bulmaya çalışmaktır. Bu, iç içe iki döngü kullanılarak gerçekleştirilir.

Zaman Karmaşıklığı Analizi (O(n^2))

Brute force çözümünde, dış döngü dizideki her eleman için bir kez çalışırken (n kez), iç döngü de her dış döngü iterasyonunda kalan elemanlar için (yaklaşık n kez) çalışır. Bu durumda toplam işlem sayısı yaklaşık n * n = n2 olur. Büyük O notasyonu ile bu, O(n2) olarak ifade edilir. Bu, dizi boyutu (n) arttıkça çözüm süresinin katlanarak artacağı anlamına gelir. Örneğin, 10.000 elemanlı bir dizi için yaklaşık 100 milyon işlem gerekebilir, bu da modern sistemlerde bile kabul edilemez derecede yavaş olabilir.

Alan Karmaşıklığı

Brute force yaklaşımında, ek bir veri yapısı kullanmadığımız için alan karmaşıklığı O(1) veya O(n) (eğer sonuç dizisi döndürme maliyetini sayarsak) düzeyindedir. Ancak, bu düşük alan maliyeti, yüksek zaman maliyeti nedeniyle genellikle tercih edilmez.

Hash Map (Karma Tablo) Veri Yapısının Gücü

Hash Map Nedir ve Nasıl Çalışır?

Hash map (karma tablo veya sözlük), anahtar-değer çiftlerini depolayan, verimli arama, ekleme ve silme işlemleri sunan bir veri yapısıdır. Bir anahtar verildiğinde, bu anahtarı bir "hash fonksiyonu" kullanarak bir dizin değerine dönüştürür ve değeri o dizinde saklar. Bu sayede, bir değeri anahtarı aracılığıyla neredeyse sabit zamanda (ortalama O(1)) bulmak mümkün olur.

Temel Operasyonlar: Ekleme, Arama, Silme

Hash map'lerin en büyük avantajı, temel operasyonlarının ortalama olarak O(1) zaman karmaşıklığına sahip olmasıdır:

  • Ekleme (Insertion): Yeni bir anahtar-değer çiftini tabloya eklemek.
  • Arama (Lookup): Bir anahtara karşılık gelen değeri bulmak.
  • Silme (Deletion): Bir anahtar-değer çiftini tablodan kaldırmak.

Bu hız, özellikle büyük veri kümelerinde arama işlemlerinin kritik olduğu durumlarda hash map'leri vazgeçilmez kılar.

Two Sum İçin Hash Map'in Avantajları

Two Sum probleminde, bir sayıyı bulduktan sonra hedefi tamamlayacak diğer sayıyı hızlıca bulmamız gerekir. Hash map'ler, bu "tamamlayıcı" sayıyı dizinin geri kalanında tekrar aramak yerine, O(1) sürede mevcut olup olmadığını kontrol etme yeteneği sunar. Bu, iç içe döngü ihtiyacını ortadan kaldırarak zaman karmaşıklığını önemli ölçüde iyileştirir.

Klasik Hash Map Çözümünün Adım Adım Uygulanması

Algoritma Mantığı ve Akış Diyagramı

Hash map çözümü, dizide tek bir geçiş yaparak çalışır. Her elemanı işlerken, hedefi tamamlayacak "tamamlayıcı" sayının daha önce görülüp görülmediğini kontrol ederiz. Eğer görülmüşse, çözüm bulunmuştur. Görülmemişse, mevcut sayıyı ve indeksini hash map'e ekleriz.

Adım 1: Hash Map'i Başlatma

Boş bir hash map (Python'da sözlük, Java'da HashMap) oluşturulur. Bu map, anahtar olarak sayıları ve değer olarak da o sayının dizideki indeksini saklayacaktır.

Adım 2: Dizide Tek Geçiş

Verilen nums dizisi üzerinde baştan sona tek bir döngü başlatılır. Her iterasyonda, mevcut sayıya (num) ve onun indeksine (i) erişilir.

Adım 3: Tamamlayıcı Sayıyı Arama

Mevcut num için, hedefi tamamlayacak olan sayıyı (complement = target - num) hesaplarız. Daha sonra bu complement sayısının hash map'te zaten mevcut olup olmadığını kontrol ederiz.

Adım 4: Eşleşme Bulunduğunda

Eğer complement hash map'te bulunuyorsa, bu demektir ki daha önce bu sayıyı görmüş ve indeksini kaydetmişiz. Bu durumda, complement'in hash map'teki indeksi ile mevcut num'un indeksi (i) aranan cevaptır. Bu iki indeksi içeren bir liste döndürülür ve algoritma sona erer.

Adım 5: Eşleşme Bulunamazsa

Eğer complement hash map'te bulunamıyorsa, bu, mevcut num'un henüz bir eşleşme bulamadığı anlamına gelir. Bu durumda, num'u ve onun indeksini (i) hash map'e ekleriz. Böylece, dizinin ilerleyen elemanları için num bir complement olarak işlev görebilir.

Kod Örneği ve Detaylı Açıklamalar

Python ile Çözüm Kodu


class Solution:
    def twoSum(self, nums: list[int], target: int) -> list[int]:
        # 1. Boş bir hash map (sözlük) oluştur
        num_map = {}  # {sayı: indeks}

        # 2. Dizide tek geçiş yap
        for i, num in enumerate(nums):
            # 3. Tamamlayıcı sayıyı hesapla
            complement = target - num

            # 4. Tamamlayıcı sayı map'te var mı kontrol et
            if complement in num_map:
                # Varsa, çözüm bulundu.
                # Tamamlayıcının indeksi ve mevcut sayının indeksi döndürülür.
                return [num_map[complement], i]
            
            # 5. Tamamlayıcı sayı bulunamazsa, mevcut sayıyı map'e ekle
            num_map[num] = i
        
        # Eğer döngü biter ve çözüm bulunamazsa (problem tanımına göre bu durum olmaz)
        return []

Kodun Satır Satır Analizi

  • num_map = {}: Boş bir Python sözlüğü (hash map) başlatılır. Anahtarlar sayılar, değerler ise o sayıların dizideki indeksleri olacaktır.
  • for i, num in enumerate(nums):: enumerate fonksiyonu, dizideki her elemanı hem indeksi (i) hem de değeri (num) ile birlikte döngüye sokar.
  • complement = target - num: Mevcut num için hedefi tamamlayacak sayıyı hesaplarız.
  • if complement in num_map:: Hesaplanan complement sayısının daha önce hash map'e eklenip eklenmediğini kontrol ederiz. Bu kontrol ortalama O(1) sürede gerçekleşir.
  • return [num_map[complement], i]: Eğer complement map'te bulunursa, çözüm bulunmuştur. num_map[complement] bize complement'in indeksini verirken, i de mevcut num'un indeksidir. Bu iki indeks döndürülür.
  • num_map[num] = i: Eğer complement bulunamazsa, mevcut num'u ve onun indeksini hash map'e ekleriz. Bu, gelecekteki elemanlar için num'un bir complement olarak bulunabilmesini sağlar.

Edge Case'ler ve Dikkat Edilmesi Gerekenler

Problem tanımına göre her zaman tam olarak bir çözüm olduğu varsayıldığı için, kodun döngüden çıkmadan bir çözüm bulması garanti edilir. Bu nedenle, döngüden sonra boş bir liste döndürme durumu pratikte gerçekleşmez, ancak iyi bir uygulama olarak eklenebilir. Aynı elemanı iki kez kullanamama kısıtlaması, hash map'e ekleme stratejimizle otomatik olarak ele alınır, çünkü mevcut elemanı ancak tamamlayıcısı bulunamazsa ekleriz. Bu, aynı elemanın kendisiyle eşleşmesini engeller.

Performans Analizi ve Optimizasyon

Zaman Karmaşıklığı (O(n))

Hash map çözümünde, dizide sadece tek bir geçiş yaparız. Her eleman için bir çıkarma işlemi (O(1)), hash map'te bir arama işlemi (ortalama O(1)) ve potansiyel olarak bir ekleme işlemi (ortalama O(1)) gerçekleştiririz. Bu nedenle, toplam zaman karmaşıklığı dizinin boyutuyla doğru orantılıdır, yani O(n) olur. Bu, brute force çözümünün O(n2) karmaşıklığına göre çok daha verimlidir.

Alan Karmaşıklığı (O(n))

Bu çözümde, en kötü durumda dizideki tüm elemanları hash map'e eklememiz gerekebilir (çözüm son elemanla bulunursa). Bu durumda hash map'in boyutu dizinin boyutuyla (n) orantılı olacaktır. Dolayısıyla, alan karmaşıklığı O(n) olarak kabul edilir. Bu, brute force çözümüne göre daha fazla bellek kullanımı anlamına gelir, ancak zaman performansındaki kazanç genellikle bu maliyeti haklı çıkarır.

Brute Force ile Karşılaştırma

Aşağıdaki tablo, iki çözüm yaklaşımının performansını özetlemektedir:

Yaklaşım Zaman Karmaşıklığı Alan Karmaşıklığı
Brute Force O(n2) O(1)
Hash Map O(n) O(n)

Görüldüğü üzere, hash map çözümü zaman karmaşıklığı açısından çok daha üstündür, özellikle büyük veri setlerinde bu fark hayati önem taşır.

Sonuç ve İleri Adımlar

Two Sum Çözümünün Önemi

Two Sum problemi, basit bir başlangıç noktası olmasına rağmen, algoritmik düşünme, veri yapıları seçimi ve performans analizi gibi temel yazılım mühendisliği kavramlarını pekiştirmek için mükemmel bir araçtır. Hash map çözümünün anlaşılması, daha karmaşık LeetCode problemlerini çözmek için sağlam bir temel oluşturur.

Öğrenilen Dersler

Bu problemden çıkarılacak en önemli ders, doğru veri yapısı seçiminin bir algoritmanın performansı üzerindeki dramatik etkisidir. Hash map'ler, hızlı arama gerektiren birçok senaryoda vazgeçilmez bir araçtır ve bu çözüm, onların gücünü açıkça ortaya koymaktadır.

Gelecek Problemlere Hazırlık

Two Sum'ı hash map ile çözmek, LeetCode yolculuğunuzda attığınız ilk önemli adımdır. Bu deneyim, dinamik programlama, grafik algoritmaları ve diğer gelişmiş veri yapıları gibi daha karmaşık konulara geçiş yaparken size rehberlik edecektir. Her zaman problemi anlamaya, farklı yaklaşımları düşünmeye ve performans analizini yapmaya odaklanın.

Sıkça Sorulan Sorular (SSS)

Two Sum için başka çözüm yolları var mı?

Evet, diziyi önce sıralayıp sonra iki işaretçi (two-pointer) tekniğiyle de çözülebilir. Ancak bu durumda, orijinal indeksleri korumak için ek adımlar gerekebilir ve sıralama işlemi O(n log n) zaman karmaşıklığına sahiptir. Hash map çözümü genellikle en optimal ve pratik yöntem olarak kabul edilir.

Hash Map yerine başka hangi veri yapıları kullanılabilir?

Eğer sayılar belirli bir aralıkta ve pozitif ise, bir boolean dizisi veya frekans dizisi (array) de kullanılabilir. Ancak bu, sayıların aralığı çok geniş olduğunda veya negatif sayılar içerdiğinde pratik değildir. Genel durumlar için hash map en esnek ve verimli çözümdür.

Boş dizi veya tek elemanlı dizi gibi özel durumlar nasıl ele alınır?

Problem kısıtlamalarına göre nums dizisi en az iki eleman içerecektir. Bu nedenle, boş veya tek elemanlı dizi gibi durumları özel olarak ele almaya gerek kalmaz. Ancak genel bir çözüm yazarken bu tür "edge case"leri düşünmek iyi bir alışkanlıktır.

Hash Map çözümünün dezavantajları nelerdir?

En büyük dezavantajı, ek bellek kullanımıdır (O(n) alan karmaşıklığı). Çok kısıtlı bellek ortamlarında bu bir sorun olabilir. Ayrıca, hash çarpışmaları (collision) nedeniyle en kötü durumda arama ve ekleme işlemleri O(n) olabilir, ancak bu durum modern hash map uygulamalarında nadiren görülür ve ortalama performans O(1)'dir.

Bu problemde neden iki geçiş yerine tek geçiş tercih edilir?

Tek geçişli çözüm, diziyi sadece bir kez tarayarak hem mevcut sayıyı işler hem de tamamlayıcısını arar. Bu, iki ayrı geçiş yapmaktan (bir geçişle tüm sayıları map'e ekleyip, ikinci geçişle tamamlayıcıları aramak) daha verimlidir çünkü her elemanı sadece bir kez ziyaret eder. İki geçiş de O(n) zaman karmaşıklığına sahip olsa da, tek geçiş pratik uygulamada daha az sabit faktörle daha hızlıdır.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Mobil sürümden çık