Command Palette

Search for a command to run...

Teknik Yazılım Mülakatlarına Hazırlık Kılavuzu

Yayınlama tarihi:
Okuma süresi:5 dk okuma

Yazılım mülakatlarınının olmazsa olmazı, CENG (Bilgisayar Mühendisliği) müfredatlarının mihenk taşı, data structure & algoritmalar bilgilerinin bir ölçülme şekli olan Leetcode sorularına nasıl yaklaşmalıyız üzerine bir kılavuz hazırladım. Bu kılavuz ile bu zorlayıcı mülakatlar daha kolayca ele alınabilir.

Aşama 0:

Soruyu iyice anlamak, soruya başlamadan önceki en temel noktadır. Bu yüzden soru sorulduktan sonraki ilk bir iki dakika içerisinde mülakatı yapan mühendise bol bol soru sorulmalıdır. Örneğin: Input array'inin boyutu en fazla ne olabilir, input array'i sorted mı, input validation yapmalı mıyım v.b. Senior adayların bu noktada, bu mülakatın bir bar raiser mülakatı olup olmadığını anlamak için, karşı tarafa bu mülakatta tek bir soru mu sorulacak diye sorması da adayı mülakata daha hazırlıklı başlatır. Buradaki anahtar kelimeler, "narrow down the scope" ve "handling edge case".

// Mülakat sorusu: "İki sayının toplamı target'a eşit olan indeksleri bulun"
// Direkt kodlamaya başlamak yerine, önce şu soruları sorun:
// 1. Input boyutu ne kadar büyük olabilir? (n <= 10^4 mü, 10^6 mı?)
// 2. Array sorted mı?
// 3. Aynı elemanı iki kez kullanabilir miyiz?
// 4. Her zaman bir çözüm var mı?
// 5. Negatif sayılar olabilir mi?
// 6. Birden fazla çözüm varsa ne döndürmeliyiz?
func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
    // Bu soruları sormadan bu fonksiyonu yazmaya başlamayın!
    // Edge case'ler: boş array, tek elemanlı array, çözüm yok durumu
    return []
}

Aşama 1 (kıdemli adaylar için):

Deneyimli mühendislerin, problemi anladıktan sonra probleme en az iki farklı çözüm önerisiyle gelmesi gerekir. *Örneğin, bu problemi brute-force ile çözebiliriz veya optimal algoritma ile çözebiliriz. Brute-force ile çözersek runtime & memory complexity'si şöyle değişir, elimizdeki vakit içerisinde bu çözümü yazabilirim *gibi noktalara değinilmelidir. Buradaki anahtar kelimeler "Getting the green light mutually", "Identifying solution alternatives and choosing the suitable one"

// Yaklaşım 1 - Brute Force: O(n²) time, O(1) space
// Her eleman çifti için kontrol et
func twoSumBruteForce(_ nums: [Int], _ target: Int) -> [Int] {
    for i in 0..<nums.count {
        for j in (i + 1)..<nums.count {
            if nums[i] + nums[j] == target {
                return [i, j]
            }
        }
    }
    return []
}

// Yaklaşım 2 - Hash Map: O(n) time, O(n) space
// Tek geçişte hash map kullanarak complement'i ara
func twoSumOptimal(_ nums: [Int], _ target: Int) -> [Int] {
    var seen: [Int: Int] = [:]
    for (i, num) in nums.enumerated() {
        let complement = target - num
        if let j = seen[complement] {
            return [j, i]
        }
        seen[num] = i
    }
    return []
}

// "Brute-force O(n²) sürer ama space kullanmaz.
//  Hash map yaklaşımı O(n) sürer, O(n) space kullanır.
//  Input boyutu 10^5'e kadar çıkabileceği için hash map
//  yaklaşımını tercih ediyorum. Sizce de uygun mu?"

Aşama 2:

Seçilen algoritmanın ufak parçalarını pseudo code ile belirlemek. Bu aşamada eğer problemin çok çetrefilli bir yönü varsa bunu önceden belirlemek, problemi çözerken alt fonksiyonlar açmak veya helper fonksiyonlar üretmek için önemlidir. Bu sayede mülakatı yapan kişi, helper fonksiyonların sadece signature'larını yazdırarak geri kalan önemli kısımlara odaklanalım diyebilir. Buradaki anahtar kelimeler "Breaking down the problem into smaller subtasks"

// Problem: Bir binary tree'de en uzun path'i bulun (Diameter of Binary Tree)

// Pseudo code ile problemi parçalara ayırma:
// Adım 1: Her node için sol ve sağ derinliği hesapla
// Adım 2: Her node'da diameter = solDerinlik + sağDerinlik
// Adım 3: Tüm node'lar arasındaki en büyük diameter'ı döndür

class TreeNode {
    var val: Int
    var left: TreeNode?
    var right: TreeNode?
    init(_ val: Int) { self.val = val }
}

// Helper fonksiyon signature'ları önce yazılır:
func depth(_ node: TreeNode?) -> Int {
    // TODO: implement
    return 0
}

func diameterOfBinaryTree(_ root: TreeNode?) -> Int {
    // Ana mantık burada olacak, depth helper'ını kullanarak
    // TODO: implement
    return 0
}

Aşama 3:

Seçilen algoritmanın en önemli kısmını implement etmek. Mülakat boyunca kısıtlı bir vakit olduğu için, problemin asıl çözümünün yer alacağı kod bloğunu ilk yazmak, bu fonksiyonun çağrılacağı yeri yazmayı da kolaylaştırabilir. Arkasından vakit elverdiğince problemin tamamını çözerek, bilgisayara veya IDE'ye aktarıldığında 90% ölçüde çalışacak bir kod yazmak hedeflenir. Kimi şirketler kodun çalıştırılarak çıktının görülmesini bekleyebilir. Buradaki anahtar kelimeler "Coming up with a working solution"

// Önce core logic implement edilir:
var maxDiameter = 0

func depth(_ node: TreeNode?) -> Int {
    guard let node = node else { return 0 }
    let leftDepth = depth(node.left)
    let rightDepth = depth(node.right)
    // Her node'da diameter güncellenir
    maxDiameter = max(maxDiameter, leftDepth + rightDepth)
    return max(leftDepth, rightDepth) + 1
}

func diameterOfBinaryTree(_ root: TreeNode?) -> Int {
    maxDiameter = 0
    _ = depth(root)
    return maxDiameter
}

// Hızlı bir test ile doğrulama:
//       1
//      / \
//     2   3
//    / \
//   4   5
// Beklenen çıktı: 3 (path: 4 -> 2 -> 1 -> 3)

Aşama 4:

Üretilen çözüm üzerine tekrar düşünmek. Mülakatı yapan kişi adaya sormadan, adayın kendi kendisine çözümün runtime ve memory complexity'sini hesaplaması, hızlı bir şekilde örnek bir input üzerinden kodun analizini yapması ve çalıştığından emin olması adayı bir sonraki aşamaya daha rahat geçirebilecek bir noktadır. Kodun üzerine sakince düşünebilmek için SOLID principle'lara uygun bir kod yazılması da adayı rahatlatabilir. Buradaki anahtar kelimeler "Identifies improvement points", "Reflects on the code"

// Çözümü gözden geçirme - dry run ile analiz:

// Input: nums = [2, 7, 11, 15], target = 9
// ┌─────────┬─────┬────────────┬──────────────┬────────┐
// │  Adım   │ num │ complement │     seen     │ Sonuç  │
// ├─────────┼─────┼────────────┼──────────────┼────────┤
// │  i = 0  │  2  │     7      │ {2: 0}       │   -    │
// │  i = 1  │  7  │     2      │ seen[2] = 0  │ [0, 1] │
// └─────────┴─────┴────────────┴──────────────┴────────┘

// Complexity analizi:
// Time:  O(n) - array üzerinde tek geçiş
// Space: O(n) - hash map en fazla n eleman tutar

// Potansiyel iyileştirme noktaları:
// 1. Global değişken yerine tuple döndürerek state'i kaldırabiliriz
// 2. Edge case: nums.count < 2 ise erken dönüş eklenebilir
// 3. Aynı indeksi iki kez kullanmadığımızdan emin olalım

Mülakatın önceki aşamalarında eğer herhangi bir improvement point belirlendiyse, bu aşamada bu iyileştirmelerin yapılması çok önemlidir. Eğer aday kendisi bir iyileştirme noktası bulamazsa mülakatı yapan kişi bir follow up sorusu sorarak adaydan problemi farklı bir input ile düşünmesini bekleyebilir. Bu noktada adayın çözümünü yeni bilgi ışığında değiştirebilmesi çok önemlidir. Buradaki anahtar kelimeler "Able to answer follow up questions"

// Follow-up: "Ya input array sorted olsaydı?"
// Yeni bilgi ile daha optimal bir çözüm üretilebilir:

// Sorted array için Two Pointer yaklaşımı: O(n) time, O(1) space
func twoSumSorted(_ nums: [Int], _ target: Int) -> [Int] {
    var left = 0
    var right = nums.count - 1

    while left < right {
        let sum = nums[left] + nums[right]
        if sum == target {
            return [left, right]
        } else if sum < target {
            left += 1    // Toplamı artırmak için sol pointer'ı ilerlet
        } else {
            right -= 1   // Toplamı azaltmak için sağ pointer'ı geri çek
        }
    }
    return []
}

// Önceki çözüm: O(n) time, O(n) space  (hash map)
// Yeni çözüm:   O(n) time, O(1) space  (two pointer)
// → Sorted input bilgisi sayesinde space complexity'yi O(1)'e düşürdük

Aşama 6:

Nihai çözümün runtime ve memory karmaşıklığının hesaplanması mülakatın başarılı olması için bir sonraki adımdır. Adayın burada ezberden konuşmaması, aksine yazdığı kodun en çok kaynak tüketen yerlerini belirleyerek, yazdığı kodun üzerine sesli düşünerek karmaşıklık hesabı yapabiliyor olması gerekir. Buradaki anahtar kelimeler "Able to estimate complexity"

// Complexity hesabını kodun üzerine sesli düşünerek yapma:
func twoSumOptimal(_ nums: [Int], _ target: Int) -> [Int] {
    var seen: [Int: Int] = [:]          // Space: O(n) - en kötü durumda n eleman
    for (i, num) in nums.enumerated() { // Time: O(n) - n elemanlı döngü
        let complement = target - num   // Time: O(1) - aritmetik işlem
        if let j = seen[complement] {   // Time: O(1) amortized - hash lookup
            return [j, i]               // Best case: ilk iki eleman cevap
        }
        seen[num] = i                   // Time: O(1) amortized - hash insert
    }
    return []
}

// Toplam Time Complexity:  O(n) — döngü n kez döner, her iterasyon O(1)
// Toplam Space Complexity: O(n) — hash map en fazla n key tutar
//
// En iyi durum:  O(1) time — cevap ilk iki elemanda bulunursa
// En kötü durum: O(n) time — cevap son elemanda veya yok
// Ortalama:      O(n) time — hash collision'lar ihmal edilirse

Aşama 7:

Mülakat sona ermeden önce adayın mutlaka karşı tarafa pair programming için nazikçe teşekkür etmesi ve varsa adayın sorularını sorması çok önemlidir. Mülakatı yapan kişinin, adayın da şirketi objektif olarak değerlendirdiğini anlaması karşılıklı bir güven oluşturacağı için, adayın önceden bu mülakatı yapan kişilerin rollerine uygun sorular hazırlayarak mülakata girmesi önemlidir.

// Rol bazlı soru hazırlığı — mülakat öncesi bir checklist:

// Mülakatçı: Senior Software Engineer ise
//   → "Ekibinizde code review süreci nasıl işliyor?"
//   → "Teknik borç yönetimini nasıl yapıyorsunuz?"
//   → "Yeni bir feature'ın architecture'ını kim belirliyor?"

// Mülakatçı: Engineering Manager ise
//   → "Ekip içi kariyer gelişimini nasıl destekliyorsunuz?"
//   → "Sprint planning ve estimation süreçleriniz nasıl?"
//   → "On-call rotation var mı, nasıl yönetiliyor?"

// Mülakatçı: Staff/Principal Engineer ise
//   → "Şirketin teknik vizyonu önümüzdeki 1-2 yıl nasıl şekilleniyor?"
//   → "Cross-team teknik kararlar nasıl alınıyor?"
//   → "Büyük ölçekli migration'lar nasıl planlanıyor?"

Peki bütün bu aşamalar nasıl pratik yapılabilir?

A4 üzerinde, autocompletion sağlamayan bir text editor üzerinde bol bol LeetCode pratiği yapmak adayı coding mülakatlarına güzel bir şekilde hazırlar. Kağıttan bilgisayara aktarıldığında çok fazla değişiklik yapma ihtiyacı olmadan çalışan kodlar hedeflenerek hazırlık yapılabilir. Code buddy'ler ile karşılıklı mock mülakatların yapılması da mülakat heyecanını yenmede çok önemli bir rol oynar.

Daha Fazla Okuma İçin

Kitaplar

  • Cracking the Coding Interview — Gayle Laakmann McDowell (Google, Facebook, Bloomberg mülakatlarının ortak önerisi)
  • Introduction to Algorithms — Cormen, Leiserson, Rivest, Stein (algoritmalar için temel referans)
  • Programming Interviews Exposed: Secrets to Landing Your Next Job — Mongan, Giguere, Suojanen, Kindler
  • Programming Pearls — Jon Bentley
  • Elements of Programming Interviews — Adnan Aziz, Tsung-Hsien Lee, Amit Prakash

Pratik Platformları

Mülakat Araçları ve Ortamları

  • https://coderpad.io — Birçok şirketin (Facebook, Spotify) tercih ettiği canlı kodlama ortamı
  • https://mural.co — Spotify gibi şirketlerin system design mülakatlarında kullandığı sanal whiteboard
  • https://excalidraw.com — System design çizimleri için ücretsiz whiteboard aracı

Big-O Complexity Referansları

  • https://www.bigocheatsheet.com/ — Veri yapıları ve sıralama algoritmalarının complexity tablosu
  • Cracking the Coding Skills — Tek sayfalık algoritma referans kağıdı

Bu kılavuzu kendi mülakat deneyimlerimden ve yıllar içerisinde gözlemlediğim ortak hatalardan yola çıkarak oluşturdum. Coding mülakatları ilk bakışta sadece algoritma bilgisi ölçüyormuş gibi görünse de, aslında ölçülen şey adayın bir problemi nasıl ele aldığı, belirsizlik karşısında nasıl hareket ettiği ve düşünce sürecini karşı tarafa ne kadar net aktarabildiğidir. Soruyu anlamadan koda atlamak, birden fazla çözüm alternatifi sunmamak veya yazdığı kodun üzerine düşünmemek gibi hatalar teknik bilgisi yeterli olan adayları bile zor durumda bırakabilir.

Benim bu konudaki en temel tavsiyem, mülakat hazırlığını bir maraton gibi ele almak. Kısa sürede yüzlerce LeetCode sorusu çözmek yerine, her gün düzenli olarak birkaç soru üzerinde derinlemesine çalışmak çok daha verimli sonuçlar veriyor. Özellikle bir code buddy ile karşılıklı mock mülakat yapmak, hem sesli düşünme pratiği kazandırıyor hem de gerçek mülakat ortamındaki baskıya alışmayı sağlıyor. Kağıt üzerinde veya autocompletion olmadan kod yazmak özellikle yapay zeka çağında kulağa eski moda gelebilir ama bu pratik, herhangi bir IDE'ye bağımlı olmadan düşünebilme becerisini kazandırması açısından hala en etkili yöntemlerden biri.

Son olarak, unutulmaması gereken bir nokta var: mülakat iki taraflı bir süreç. Şirket de sizi değerlendirirken siz de şirketi değerlendirebilirsiniz. Bu yüzden mülakatın sonunda sorular sormak sadece bir nezaket değil, aynı zamanda o ekipte ve o şirkette mutlu olup olmayacağınızı anlamanın en doğrudan yolu. Hazırlıklı gitmek, sakin olmak ve her mülakatı bir öğrenme fırsatı olarak görmek çok fayda sağlayabilir. Sonuç ne olursa olsun, her mülakat sizi bir sonrakine daha hazır hale getirebilir. Kapak fotoğrafı: Unsplash - Safar Safarov

Paylaş:

A
Yazan
Software Engineer

Tartışma

Henüz yorum yok

İlk yorumu siz yapın!