Zaman damgalı anahtar-değer deposu
Problem
Her anahtar için değerin geçmişini saklayan bir depo tasarla. İki işlem var:
set(key, value, timestamp): anahtarın bu zaman damgasındaki değerini kaydeder.get(key, timestamp): anahtar için, zaman damgası verilen değerden küçük ya da eşit olan kayıtlar içinden en geç olanın değerini döndürür. Böyle bir kayıt yoksa boş string döner.
Aynı anahtar için set çağrılarının zaman damgaları kesin artan sırada gelir.
set("kur", "32.10", 10)
set("kur", "32.45", 20)
get("kur", 5) -> "" (10'dan önce kayıt yok)
get("kur", 15) -> "32.10"
get("kur", 25) -> "32.45"
get("yok", 10) -> ""
Netleştirme soruları
- Zaman damgaları anahtar başına artan sırada mı garanti? (Bu, sıralı ekleme ve binary search'ün temeli.)
- Aynı zaman damgasıyla iki
setgelirse? (Garanti dışı; gelirse son yazan mı kazanmalı?) getvesetoranı ne? Hangisi daha sık? (Veri yapısı seçimini etkiler.)- Thread-safe olmalı mı?
- Eski versiyonlar silinebilir mi (retention), yoksa sonsuza kadar mı tutulacak?
İpuçları
İpucu 1
Her anahtarın geçmişini ayrı bir listede tut: Map<String, List<(timestamp, value)>>. Zaman damgaları artan geldiği için liste kendiliğinden sıralı kalır.
İpucu 2
get için sıralı listede "zaman damgası ≤ t olan en son eleman" aranıyor. Bu, binary search'ün klasik bir varyasyonu.
İpucu 3
"t'den büyük olan ilk eleman"ı (upper bound) bul; cevap onun hemen solundaki eleman. Sol yoksa boş string. Alternatif: TreeMap.floorEntry(t).
Yaklaşım
Brute force: Her anahtar için listeyi sondan başa tara, timestamp <= t olan ilk kaydı döndür. set O(1), get O(m) (m: anahtarın versiyon sayısı). Yazma ağırlıklı ve az versiyonlu bir iş yükünde bu yeterli bile olabilir; mülakatta söylemeye değer.
Optimal, HashMap + sıralı liste + binary search: Problemin garantisi sayesinde her anahtarın listesi set ile sona ekleyerek sıralı kalır, set amortize O(1). get'te upper bound: [lo, hi) yarı açık aralıkla timestamp <= t olanları sola at; döngü bitince lo, t'den büyük ilk index'tir. Cevap lo - 1; lo == 0 ise hiç uygun kayıt yoktur.
Alternatif, TreeMap<Integer, String>: Her anahtar için bir TreeMap tut, get için floorEntry(t) kullan. Hem set hem get O(log m) ve sıralı gelme garantisine ihtiyaç duymaz; damgalar karışık sırada gelse de çalışır. Bedeli: girdi başına daha fazla nesne ve bellek (kırmızı-siyah ağaç düğümleri). Mülakatta en temiz cevap genelde şu: "Garanti varsa liste + binary search; yoksa TreeMap."
Versiyon değerini record Version(int timestamp, String value) ile modellemek, paralel iki liste tutmaktan (List<Integer> + List<String>) daha okunaklıdır; paralel listelerin senkron kalmaması klasik bir hata kaynağıdır.
Java çözümü
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class TimeMap {
private record Version(int timestamp, String value) { }
private final Map<String, List<Version>> versionsByKey = new HashMap<>();
// Varsayım (problemin garantisi): aynı key için timestamp'ler artan sırada gelir
public void set(String key, String value, int timestamp) {
versionsByKey.computeIfAbsent(key, k -> new ArrayList<>()).add(new Version(timestamp, value));
}
public String get(String key, int timestamp) {
List<Version> versions = versionsByKey.get(key);
if (versions == null) {
return "";
}
// timestamp'i timestamp'ten BÜYÜK olan ilk versiyonu bul; cevap onun bir solu
int lo = 0, hi = versions.size(); // yarı açık [lo, hi)
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (versions.get(mid).timestamp() <= timestamp) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo == 0 ? "" : versions.get(lo - 1).value();
}
}
Kendi binary search'ünü yazmak yerine Collections.binarySearch da kullanılabilir, ama tam eşleşme yoksa -(insertionPoint) - 1 döndürdüğü için dönüşüm hatası yapmak kolaydır; mülakatta elle yazılan net bir upper bound daha güvenlidir.
Karmaşıklık
set: amortize O(1) (map erişimi + listeye ekleme). TreeMap versiyonunda O(log m).get: O(log m); map erişimi O(1) ortalama + binary search.- Bellek: O(toplam
setsayısı).
Kenar durumlar ve testler
main içinde derlenip çalıştırıldı; ayrıca 300 rastgele senaryoda (her biri 60 karışık set/get, 3 anahtar) her anahtar için TreeMap.floorEntry kullanan referans uygulamayla karşılaştırıldı.
| Durum | Çağrı | Beklenen | Neyi yakalar |
|---|---|---|---|
| İlk kayıttan önce | get("kur", 5) | "" | lo == 0 |
| Tam ilk damga | get("kur", 10) | "32.10" | <= (eşitlik dahil) |
| Arada | get("kur", 15) | "32.10" | Floor mantığı |
| Tam son damga | get("kur", 20) | "32.45" | |
| Sondan sonra | get("kur", 1000) | "32.45" | lo == size |
| Bilinmeyen anahtar | get("yok", 10) | "" | null liste |
| Boş değer saklanmış | set("x", "", 1), get("x", 2) | "" | Boş değer ile "yok" ayrımı |
Son satır bir tasarım uyarısıdır: boş string hem "değer boş" hem "kayıt yok" anlamına geliyor. Gerçek bir API'de Optional<String> döndürmek bu belirsizliği kaldırır.
Mülakatçı için
Bu soru bir veri yapısı tasarımı + binary search karışımıdır ve backend dünyasına çok yakındır: MVCC (PostgreSQL'in satır versiyonları), event sourcing'de "t anındaki durum", konfigürasyon geçmişi, döviz kuru geçmişi. Güçlü aday, problemin "artan damga" garantisinin neden önemli olduğunu ve garanti olmasaydı ne değişeceğini kendiliğinden söyler.
İpucu zamanlaması: Aday get'te lineer tarama yaparsa kabul et ve "her anahtar için milyonlarca versiyon olursa?" diye sor. Upper bound'da sınır hatası yaparsa (lo - 1 yerine lo) "tam damga" ve "arada" örneklerini yürütmesini iste. TreeMap ile başlarsa çok iyi; garanti varken listenin neden daha hafif olduğunu sor.
- Zaman damgalarının artan geldiği garantisini sordu ve bundan yararlandı.
- Anahtar başına versiyon listesi ya da TreeMap ile mantıklı bir veri modeli kurdu.
get'i O(log m)'de çözdü; floor/upper bound sınırlarını doğru yazdı.- Liste + binary search ile TreeMap arasındaki trade-off'u (sıra garantisi, bellek, ekleme maliyeti) açıkladı.
- Bilinmeyen anahtar ve ilk kayıttan önceki sorguyu ele aldı.
- Thread-safety ve bellek büyümesi (retention) gibi production sorularına değindi.
Takip soruları:
- Bu yapıyı thread-safe yap; okumalar yazmalardan 100 kat fazla. (Beklenen:
ConcurrentHashMap+ anahtar başına kilit ya daConcurrentSkipListMap; copy-on-write liste okuma ağırlıklı yükte uygun ama yazma maliyeti O(m).) - Bellek sınırsız büyüyor; 30 günden eski versiyonları sil. (Beklenen: anahtar başına baştan kırpma, TreeMap'te
headMap(t).clear(); arka planda periyodik temizlik ya da yazma sırasında lazy temizlik.) - Bunu bir veritabanında modellesen? (Beklenen:
(key, ts)birleşik index veWHERE key = ? AND ts <= ? ORDER BY ts DESC LIMIT 1; index sayesinde aynı floor araması.)
Benzer problemler
- Snapshot Array (LeetCode 1146): aynı versiyonlama fikri, index bazlı.
- Stock Price Fluctuation (LeetCode 2034): veri yapısı tasarımı + sıralı yapı.
- LRU Cache (LeetCode 146): klasik veri yapısı tasarımı.
- Koko Eating Bananas (LeetCode 875): binary search sınır disiplini (bu sette:
koko-eating-bananas).