F-lab Java 1์ฃผ์ฐจ / Phase 6 / Unit 6.5 ๋ณธ๊ฒฉ ํ์ต ์๋ฃ โ Phase 6 ๋ง๋ฌด๋ฆฌ!
9-์น์ ๋ง์คํฐ ํ๋กฌํํธ ํ์์ผ๋ก ๊น์ด ํํค์น๋ค.์ ์ ์ง์: Phase 4-5, Unit 6.1-6.4 (ํนํ HashMap, ArrayList/LinkedList)
๋ค์ ํ์ต: Phase 7 โ I/O & NIO (์์ )์ด Unit์ ์๋ฏธ: ์์๊ฐ ์๋ Map ์ ๋ ๊ฐ์ง ๋ต.
TreeMap = ์ ๋ ฌ ์์ (๋ฒ์ ์ฟผ๋ฆฌ). LinkedHashMap = ์ฝ์ /์ ๊ทผ ์์ (LRU ์บ์).
๋ฉด์ ("TreeMap ์ธ์ ์ฐ๋?") + ์ค๋ฌด (LRU ์บ์ 6์ค ๊ตฌํ) ๋ ๋ง๋ฆฌ.
์ธ ๊ฐ์ง Map ์ ์ผ์ ๋น์ ๋ก ํ์ด๋ณด๊ฒ ์ต๋๋ค.
[A]
Apple โ ์ฌ๊ณผ
Apricot โ ์ด๊ตฌ
[B]
Banana โ ๋ฐ๋๋
Berry โ ๋ฒ ๋ฆฌ
[C]
Cat โ ๊ณ ์์ด
Cherry โ ์ฒด๋ฆฌ
...
ํน์ง:
ํต์ฌ: ์ ๋ ฌ๋ ์ํ ์ ์ง + ๋ฒ์ ๊ฒ์.
2024-12-01 14:30 Alice
2024-12-02 09:15 Bob
2024-12-02 11:45 Charlie
2024-12-03 16:20 David
ํน์ง:
ํต์ฌ: ์ฝ์ ์์ ๋๋ ์ ๊ทผ ์์ ์ ์ง.
"HashMap ์ ์์ ์์ (๋น ๋ฆ), TreeMap ์ ์ ๋ ฌ ์์ (๋ฒ์), LinkedHashMap ์ ์ฝ์ /์ ๊ทผ ์์ (LRU)."
๋น์ ์ ๋ฆฌ:
| ๋น์ | ์๋ฐ ํด๋์ค | ์์ | ํน์ง |
|---|---|---|---|
| ์ฌ๋ฌผํจ | HashMap | ์์ | ๊ฐ์ฅ ๋น ๋ฆ |
| ์์ด ์ฌ์ | TreeMap | ์ ๋ ฌ | ๋ฒ์ ์ฟผ๋ฆฌ |
| ๋ฐฉ๋ช ๋ก | LinkedHashMap | ์ฝ์ /์ ๊ทผ | LRU ์บ์ |
HashMap = ์์ ๋ฐฐ์น ์ฑ ๊ฝ์ด:
TreeMap = ๋ถ๋ฅ ์ ๋ฆฌ๋ ์ฑ ๊ฝ์ด:
LinkedHashMap = ๋์ฐฉ ์์ ์ฑ ๊ฝ์ด:
HashMap ์ ๋น ๋ฅด์ง๋ง ์์ ์ ๋ณด๊ฐ ์์.
Map<Integer, Customer> customers = new HashMap<>();
customers.put(20, alice);
customers.put(35, bob);
customers.put(40, charlie);
customers.put(55, david);
// 30~50๋ ๊ณ ๊ฐ๋ง ๊ฐ์ ธ์ค๊ธฐ?
// HashMap ์ผ๋ก๋ โ ์ ์ฒด ์ํ + ํํฐ๋ง โ O(n)
โ ์์๊ฐ ์๋ค๋ฉด O(log n + k) ๋ก ๊ฐ๋ฅ.
// LRU ์บ์ ์๋๋ฆฌ์ค
// ๊ฐ์ฅ ์ค๋ ์ ์ด ํญ๋ชฉ ์ ๊ฑฐ
Map<String, Cache> cache = new HashMap<>();
cache.put("A", a); // 1๋ฒ์งธ
cache.put("B", b); // 2๋ฒ์งธ
cache.put("C", c); // 3๋ฒ์งธ
// "๊ฐ์ฅ ์ค๋๋ ํญ๋ชฉ" โ ์ด๋ป๊ฒ?
// HashMap ์ ์์ ์์ โ ์ถ์ ๋ถ๊ฐ
โ ์ฝ์ ์์๊ฐ ์ ์ง๋๋ค๋ฉด ์ฆ์ ์ฒ๋ฆฌ ๊ฐ๋ฅ.
Map<String, Integer> wordCount = new HashMap<>();
wordCount.put("apple", 5);
wordCount.put("banana", 3);
wordCount.put("cherry", 8);
// ์ํ๋ฒณ ์์๋ก ์ถ๋ ฅ?
// HashMap ์ผ๋ก๋ โ keys โ ์ ๋ ฌ โ ๋งค๋ฒ O(n log n)
โ ํญ์ ์ ๋ ฌ๋์ด ์๋ค๋ฉด O(n) ์ผ๋ก ๋.
// Java 1.2 (1998)
public class TreeMap<K, V> implements NavigableMap<K, V> {
private final Comparator<? super K> comparator;
private transient Entry<K,V> root; // Red-Black Tree
private transient int size;
}
ํต์ฌ:
subMap, headMap, tailMap, floorKey, ceilingKey)// Java 1.4 (2002)
public class LinkedHashMap<K, V> extends HashMap<K, V> {
transient LinkedHashMap.Entry<K,V> head; // ์ฒซ ์์
transient LinkedHashMap.Entry<K,V> tail; // ๋ง์ง๋ง ์์
final boolean accessOrder; // false = ์ฝ์
์์, true = ์ ๊ทผ ์์
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // ์๋ฐฉํฅ ์ฐ๊ฒฐ
}
}
ํต์ฌ:
accessOrder=false (๊ธฐ๋ณธ): ์ฝ์
์์ ์ ์งaccessOrder=true: ์ ๊ทผ ์์ ์ ์ง (LRU ๊ตฌํ ๊ฐ๋ฅ)| ์ฐ์ฐ | HashMap | TreeMap | LinkedHashMap |
|---|---|---|---|
get | O(1) โญ | O(log n) | O(1) โญ |
put | O(1) โญ | O(log n) | O(1) โญ |
remove | O(1) โญ | O(log n) | O(1) โญ |
containsKey | O(1) โญ | O(log n) | O(1) โญ |
| firstKey/lastKey | X | O(log n) โญ | O(1) (์ฒซ/๋ง์ง๋ง) |
| subMap (๋ฒ์) | X | O(log n + k) โญ | X |
| ์ํ (์์) | ๋ณด์ฅ X | ์ ๋ ฌ ์์ โญ | ์ฝ์ /์ ๊ทผ ์์ โญ |
โ TreeMap ์ ์ ๋ ฌ ๋ฉ์๋, LinkedHashMap ์ ์์ + ๊ฐ์ ์ฑ๋ฅ.
| ํญ๋ชฉ | HashMap | TreeMap | LinkedHashMap |
|---|---|---|---|
| Node ํฌ๊ธฐ | 32 ๋ฐ์ดํธ | 40 ๋ฐ์ดํธ (parent ์ถ๊ฐ) | 48 ๋ฐ์ดํธ (before/after ์ถ๊ฐ) |
| ๋ฉ๋ชจ๋ฆฌ/์์ | 1๋ฐฐ | 1.25๋ฐฐ | 1.5๋ฐฐ |
โ LinkedHashMap ๊ฐ ๋ฉ๋ชจ๋ฆฌ ๊ฐ์ฅ ํผ, ๊ทธ๋ฌ๋ ์์ ์ ์ง์ ๊ฐ์น ๊ฐ ์์ ๋๋ง.
"๊ฐ์ Map ์ธํฐํ์ด์ค, ์ธ ๊ฐ์ง ๋ค๋ฅธ trade-off."
์๋๋ง ํ์ํ๋ฉด HashMap, ์ ๋ ฌ๊ณผ ๋ฒ์๊ฐ ํ์ํ๋ฉด TreeMap, ์ฝ์ ์์๋ LRU ๊ฐ ํ์ํ๋ฉด LinkedHashMap. ์ํฉ์ ๋ง๋ ์ ํ ์ด ์๋์ด ์๋ฐ ๊ฐ๋ฐ์์ ์ญ๋.
@Service
public class CargoSearchService {
// โ HashMap ์ผ๋ก ๊ฐ๊ฒฉ๋ ๊ฒ์
private Map<Integer, List<Cargo>> cargosByFare = new HashMap<>();
public List<Cargo> findByFareRange(int minFare, int maxFare) {
List<Cargo> result = new ArrayList<>();
for (Map.Entry<Integer, List<Cargo>> entry : cargosByFare.entrySet()) {
// โ ๏ธ ๋ชจ๋ ๊ฐ๊ฒฉ๋ ์ํ โ O(n)
if (entry.getKey() >= minFare && entry.getKey() <= maxFare) {
result.addAll(entry.getValue());
}
}
return result;
}
}
๋ฌธ์ :
ํด๊ฒฐ: TreeMap
private NavigableMap<Integer, List<Cargo>> cargosByFare = new TreeMap<>();
public List<Cargo> findByFareRange(int minFare, int maxFare) {
// O(log n + k) โ ์ ๋ ฌ๋ ํธ๋ฆฌ์์ ๋ฒ์ ๊ฒ์
return cargosByFare.subMap(minFare, true, maxFare, true)
.values().stream()
.flatMap(List::stream)
.collect(Collectors.toList());
}
// โ HashMap + List ๋ก LRU ๊ตฌํ ์๋
public class BadLRUCache<K, V> {
private Map<K, V> map = new HashMap<>();
private List<K> accessOrder = new ArrayList<>();
private int capacity;
public V get(K key) {
V value = map.get(key);
if (value != null) {
accessOrder.remove(key); // โ ๏ธ O(n)
accessOrder.add(key);
}
return value;
}
public void put(K key, V value) {
if (map.size() >= capacity && !map.containsKey(key)) {
K oldest = accessOrder.remove(0); // โ ๏ธ O(n)
map.remove(oldest);
}
map.put(key, value);
accessOrder.remove(key); // O(n)
accessOrder.add(key);
}
}
๋ฌธ์ :
ํด๊ฒฐ: LinkedHashMap
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // accessOrder = true!
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
// 6 ์ค๋ก LRU ์บ์ ์์ฑ! ๋ชจ๋ ์์
O(1)
// โ HashMap โ ์์ ๋ณด์กด X
Map<LocalDateTime, Activity> activities = new HashMap<>();
activities.put(time1, a1);
activities.put(time2, a2);
activities.put(time3, a3);
// ์๊ฐ์ ์ถ๋ ฅ?
for (Activity a : activities.values()) {
System.out.println(a); // ์์ ๋ณด์ฅ X!
}
ํด๊ฒฐ 1: TreeMap (์๊ฐ์ ์ ๋ ฌ)
NavigableMap<LocalDateTime, Activity> activities = new TreeMap<>();
// ์๋์ผ๋ก ์๊ฐ์
for (Activity a : activities.values()) { ... } // ์๊ฐ์ ์ถ๋ ฅ
ํด๊ฒฐ 2: LinkedHashMap (์ฝ์ ์์)
Map<LocalDateTime, Activity> activities = new LinkedHashMap<>();
// ์ฝ์
์์ ์ ์ง (์๊ฐ์ ์ฝ์
ํ๋ค๋ฉด ์๊ฐ์)
Q: "TreeMap ์ ์ธ์ ์ฐ๋์?"
A: "์... HashMap ๋ณด๋ค ์ ๋ ฌ๋๋ ๊ฑฐ์์" โ ํ๋ฉด์
์๋์ด ๋ต๋ณ:
1. ๋ฒ์ ์ฟผ๋ฆฌ ๊ฐ ํ์ํ ๋ (subMap, headMap, tailMap)
2. floorKey/ceilingKey ๊ฐ์ ๊ทผ์ ํค ๊ฒ์
3. ์ ๋ ฌ๋ ์ถ๋ ฅ ์ด ์์ฃผ ํ์ํ ๋
4. NavigableMap ์ ๊ฐ๋ ฅํ ๋ฉ์๋๋ค
5. trade-off: O(log n) ๋น์ฉ
// โ ๋งค๋ฒ ์ ๋ ฌ
Map<Integer, Integer> hourlyCount = new HashMap<>();
// ... ๋ฐ์ดํฐ ์ถ๊ฐ ...
// ์๊ฐ์ ์ถ๋ ฅ ์
List<Integer> sortedHours = new ArrayList<>(hourlyCount.keySet());
Collections.sort(sortedHours); // ๋งค๋ฒ O(n log n)
for (int hour : sortedHours) {
System.out.println(hour + ": " + hourlyCount.get(hour));
}
ํด๊ฒฐ: TreeMap
Map<Integer, Integer> hourlyCount = new TreeMap<>();
// ์๋ ์ ๋ ฌ
for (Map.Entry<Integer, Integer> e : hourlyCount.entrySet()) {
System.out.println(e.getKey() + ": " + e.getValue());
}
// JSON ์๋ต์์ ํค ์์๊ฐ ์ค์ํ ๊ฒฝ์ฐ
@RestController
public class ReportController {
@GetMapping("/report")
public Map<String, Object> getReport() {
Map<String, Object> result = new HashMap<>();
result.put("totalCount", 1000);
result.put("avgFare", 50000);
result.put("maxWeight", 5000);
return result;
// JSON ์์๊ฐ ๋ฌด์์!
// {"avgFare":50000, "maxWeight":5000, "totalCount":1000}
}
// โ LinkedHashMap โ ์ฝ์
์์ ์ ์ง
@GetMapping("/report-ordered")
public Map<String, Object> getReportOrdered() {
Map<String, Object> result = new LinkedHashMap<>();
result.put("totalCount", 1000);
result.put("avgFare", 50000);
result.put("maxWeight", 5000);
return result;
// JSON: {"totalCount":1000, "avgFare":50000, "maxWeight":5000}
}
}
| ์๋๋ฆฌ์ค | ์๋ชป๋ ์ ํ | ์ฌ๋ฐ๋ฅธ ์ ํ |
|---|---|---|
| ๊ฐ๊ฒฉ๋ ๋ฒ์ ๊ฒ์ | HashMap O(n) | TreeMap O(log n + k) |
| LRU ์บ์ | ์ง์ ๊ตฌํ O(n) | LinkedHashMap O(1) |
| ์๊ฐ์ ์ถ๋ ฅ | HashMap + ์ ๋ ฌ ๋งค๋ฒ | TreeMap ๋๋ LinkedHashMap |
| ๋ฉด์ ๋ต๋ณ | ํ๋ฉด์ | ์๋์ด |
| API ์๋ต ์์ | HashMap (๋ฌด์์) | LinkedHashMap |
โ ์ ์ ํ Map ์ ํ์ ์๋์ด์ ํต์ฌ ์ญ๋.
TreeMap<String, Integer> map = new TreeMap<>();
map.put("Charlie", 3);
map.put("Alice", 1);
map.put("Bob", 2);
// ์ํ โ ์ ๋ ฌ๋ ์์!
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + ": " + e.getValue());
}
// ์ถ๋ ฅ:
// Alice: 1
// Bob: 2
// Charlie: 3
NavigableMap<Integer, String> map = new TreeMap<>();
map.put(10, "A");
map.put(20, "B");
map.put(30, "C");
map.put(40, "D");
map.put(50, "E");
// === ๋ฒ์ ๊ฒ์ ===
map.subMap(20, 40); // {20=B, 30=C} (40 ๋ฏธํฌํจ)
map.subMap(20, true, 40, true); // {20=B, 30=C, 40=D}
map.headMap(30); // {10=A, 20=B}
map.tailMap(30); // {30=C, 40=D, 50=E}
// === ๊ทผ์ ํค ๊ฒ์ ===
map.floorKey(25); // 25 ์ดํ ์ค ๊ฐ์ฅ ํฐ ํค โ 20
map.ceilingKey(25); // 25 ์ด์ ์ค ๊ฐ์ฅ ์์ ํค โ 30
map.lowerKey(20); // 20 ๋ฏธ๋ง ์ค ๊ฐ์ฅ ํฐ ํค โ 10
map.higherKey(20); // 20 ์ด๊ณผ ์ค ๊ฐ์ฅ ์์ ํค โ 30
// === ์ฒซ / ๋ง์ง๋ง ===
map.firstKey(); // 10
map.lastKey(); // 50
map.firstEntry(); // (10, A)
map.lastEntry(); // (50, E)
map.pollFirstEntry(); // (10, A) ๋ฐํ + ์ ๊ฑฐ
map.pollLastEntry(); // (50, E) ๋ฐํ + ์ ๊ฑฐ
// === ์ญ์ ===
map.descendingMap(); // ์ญ์ Map
map.descendingKeySet(); // ์ญ์ ํค
// 1. ์์ฐ ์์ (Comparable)
TreeMap<String, Integer> ascMap = new TreeMap<>(); // A, B, C...
// 2. ์ญ์
TreeMap<String, Integer> descMap = new TreeMap<>(Comparator.reverseOrder());
// 3. ์ปค์คํ
Comparator
TreeMap<Customer, Order> byAge = new TreeMap<>(
Comparator.comparingInt(Customer::getAge)
);
// 4. ๋ค์ค ์ ๋ ฌ
TreeMap<Customer, Order> byGradeThenName = new TreeMap<>(
Comparator.comparing(Customer::getGrade)
.thenComparing(Customer::getName)
);
Map<String, Integer> map = new LinkedHashMap<>();
map.put("Charlie", 3);
map.put("Alice", 1);
map.put("Bob", 2);
// ์ํ โ ์ฝ์
์์!
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey());
}
// ์ถ๋ ฅ:
// Charlie (๋จผ์ ์ฝ์
)
// Alice
// Bob
// accessOrder = true
LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// ํ์ฌ ์์: A โ B โ C (์ฝ์
์์)
map.get("A"); // A ์ ๊ทผ
// ์์ ๋ณ๊ฒฝ: B โ C โ A
// (A ๊ฐ ๊ฐ์ฅ ์ต๊ทผ ์ ๊ทผ โ ๋งจ ๋ค๋ก ์ด๋)
ํต์ฌ: get() ํธ์ถ ์ ๊ทธ ํญ๋ชฉ์ด ๋งจ ๋ค๋ก ์ด๋.
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // accessOrder = true
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // capacity ์ด๊ณผ ์ ๊ฐ์ฅ ์ค๋๋ ์ ๊ฑฐ
}
}
// ์ฌ์ฉ
LRUCache<String, Customer> cache = new LRUCache<>(100);
cache.put("alice", aliceCustomer);
cache.get("alice"); // alice ๊ฐ ๊ฐ์ฅ ์ต๊ทผ
// 100 ๊ฐ ์ด๊ณผ ์ ์๋์ผ๋ก ๊ฐ์ฅ ์ค๋ ์ ์ด ํญ๋ชฉ ์ ๊ฑฐ
ํต์ฌ ๋ฉ์ปค๋์ฆ:
accessOrder = true โ get ์ ๋งจ ๋ค๋ก ์ด๋removeEldestEntry ์ค๋ฒ๋ผ์ด๋ โ ์๋ ์ ๊ฑฐ ์ ์ฑ
| ํญ๋ชฉ | HashMap | TreeMap | LinkedHashMap |
|---|---|---|---|
| ์์ | ์์ | ์ ๋ ฌ | ์ฝ์ /์ ๊ทผ |
| ์๊ฐ ๋ณต์ก๋ | O(1) | O(log n) | O(1) |
| null key | 1๊ฐ | โ | 1๊ฐ |
| ๋ฉ๋ชจ๋ฆฌ/์์ | 1๋ฐฐ | 1.25๋ฐฐ | 1.5๋ฐฐ |
| ๋ฒ์ ์ฟผ๋ฆฌ | X | โ โญ | X |
| firstKey/lastKey | X | O(log n) | O(1) |
| LRU ๊ตฌํ | ์ด๋ ค์ | ์ด๋ ค์ | 6์ค โญ |
| ์ฌ์ฉ ์๊ธฐ | ๊ธฐ๋ณธ | ์ ๋ ฌ/๋ฒ์ | ์์/LRU |
Map ์ด ํ์ํ๋ค
โ
์์๊ฐ ํ์ํ๊ฐ?
โโโ ๋ฌด๊ด โ HashMap (๋๋ ConcurrentHashMap)
โโโ ์ ๋ ฌ ์์ / ๋ฒ์ ์ฟผ๋ฆฌ โ TreeMap โญ
โโโ ์ฝ์
/ ์ ๊ทผ ์์
โโโ ์ฝ์
์์๋ง โ LinkedHashMap (๊ธฐ๋ณธ)
โโโ LRU ์บ์ โ LinkedHashMap(accessOrder=true) โญ
// ๋จ์ด ๋น๋ โ ์ํ๋ฒณ์ ์ถ๋ ฅ
TreeMap<String, Integer> wordCount = new TreeMap<>();
words.forEach(w -> wordCount.merge(w, 1, Integer::sum));
wordCount.forEach((word, count) ->
System.out.println(word + ": " + count)
);
// ์๋์ผ๋ก ์ํ๋ฒณ์!
// ๊ฐ๊ฒฉ๋๋ณ ํ๋ฌผ
NavigableMap<Integer, List<Cargo>> byFare = new TreeMap<>();
// 5,000์~10,000์ ๋ฒ์
Map<Integer, List<Cargo>> mid = byFare.subMap(5000, 10001);
Map<String, Data> cache = new LinkedHashMap<>(100, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
return size() > 100;
}
};
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", "success");
response.put("data", ...);
response.put("timestamp", ...);
// JSON ์ถ๋ ฅ ์์ ๋ณด์ฅ
public class TreeMap<K, V> implements NavigableMap<K, V> {
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
private transient int size;
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left; // ์ผ์ชฝ ์์
Entry<K,V> right; // ์ค๋ฅธ์ชฝ ์์
Entry<K,V> parent; // ๋ถ๋ชจ
boolean color = BLACK; // Red ๋๋ Black
}
}
Red-Black Tree ๊ท์น:
1. ๋
ธ๋๋ Red ๋๋ Black
2. Root ๋ Black
3. Red ๋
ธ๋์ ์์์ Black
4. ๋ชจ๋ leaf ๊น์ง์ Black ๋
ธ๋ ์๋ ๊ฐ์
5. โ ๊ท ํ ๋ณด์ฅ: ์ต์
๊น์ด โค 2 ร log(n+1)
public V put(K key, V value) {
Entry<K,V> t = root;
// 1. ๋ฃจํธ๊ฐ ์์ผ๋ฉด root ๋ก
if (t == null) {
compare(key, key); // null/type check
root = new Entry<>(key, value, null);
size = 1;
return null;
}
// 2. ๋น๊ตํ๋ฉฐ ์์น ์ฐพ๊ธฐ
Comparator<? super K> cpr = comparator;
int cmp;
Entry<K,V> parent;
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0) t = t.left;
else if (cmp > 0) t = t.right;
else return t.setValue(value); // ๊ฐ์ ํค โ ๊ฐ ๊ต์ฒด
} while (t != null);
// 3. ์ ๋
ธ๋ ์ถ๊ฐ
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0) parent.left = e;
else parent.right = e;
// 4. Red-Black ๊ท ํ ์กฐ์
fixAfterInsertion(e);
size++;
return null;
}
O(log n): ํธ๋ฆฌ ๊น์ด๋งํผ ํ์.
// map.subMap(20, 40)
// โ 20 โค key < 40 ์ธ ๋ชจ๋ entry
์๊ณ ๋ฆฌ์ฆ:
1. ์์ ํค (20) ์ฐพ๊ธฐ: O(log n)
2. ๋ ํค (40) ์ฐพ๊ธฐ: O(log n)
3. ์ฌ์ด์ ๋ชจ๋ entry ์ํ: O(k)
4. ์ด: O(log n + k)
์ค์: subMap ์ view โ ์๋ณธ ๋ณ๊ฒฝ ์ ๋ฐ์๋จ.
| ์์ | ์๊ฐ |
|---|---|
| ์ฝ์ | O(log n) |
| ๊ฒ์ | O(log n) |
| ์ญ์ | O(log n) |
| ์ต์/์ต๋ | O(log n) |
| ๋ฒ์ ๊ฒ์ | O(log n + k) |
์๋ฐ 8+ HashMap ์ Treeification ๋ ๊ฐ์ Red-Black Tree.
public class LinkedHashMap<K,V> extends HashMap<K,V> {
transient Entry<K,V> head; // ์ฒซ ๋ฒ์งธ (๊ฐ์ฅ ์ค๋๋)
transient Entry<K,V> tail; // ๋ง์ง๋ง (๊ฐ์ฅ ์ต๊ทผ)
final boolean accessOrder; // false=์ฝ์
์์, true=์ ๊ทผ์์
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // ์๋ฐฉํฅ ์ฐ๊ฒฐ
}
}
ํต์ฌ:
HashMap ๋ถ๋ถ (table ๋ฐฐ์ด):
table[0]: ...
table[3]: Node(B) โ Node(D)
table[5]: Node(A)
table[7]: Node(C)
...
LinkedHashMap ์ ์๋ฐฉํฅ ๋ฆฌ์คํธ:
head โ A โ B โ C โ D โ tail
โ โ
๊ฐ์ฅ ์ค๋๋ ๊ฐ์ฅ ์ต๊ทผ
๊ฐ Node ๋ next (HashMap) ์ before/after (LinkedHashMap) ๋ชจ๋ ๋ณด์
// LinkedHashMap.newNode()
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<>(hash, key, value, e);
linkNodeLast(p); // โ
์๋ฐฉํฅ ๋ฆฌ์คํธ์ ๋์ ์ถ๊ฐ
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
ํจ๊ณผ:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e); // โ
๋งจ ๋ค๋ก ์ด๋
return e.value;
}
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
// e ๋ฅผ ํ์ฌ ์์น์์ ๋นผ๊ณ tail ๋ก ์ด๋
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;
LinkedHashMap.Entry<K,V> b = p.before, a = p.after;
p.after = null;
if (b == null) head = a;
else b.after = a;
if (a != null) a.before = b;
else last = b;
if (last == null) head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
}
}
ํต์ฌ:
// LinkedHashMap ์ ๋ฉ์๋
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false; // ๊ธฐ๋ณธ: ์ ๊ฑฐ ์ ํจ
}
// HashMap.afterNodeInsertion ์์ ํธ์ถ
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
LRU ์บ์์ ๋์:
1. put ํธ์ถ โ ์ entry ์ถ๊ฐ
2. afterNodeInsertion ํธ์ถ
3. removeEldestEntry ์ค๋ฒ๋ผ์ด๋ ๊ฐ true ๋ฐํ ์
4. head (๊ฐ์ฅ ์ค๋๋ ํญ๋ชฉ) ์ ๊ฑฐ
๋ชจ๋ ๋น๊ต:
// 1. ์ฝ์
์์ (accessOrder = false, ๊ธฐ๋ณธ)
Map<String, Integer> insertion = new LinkedHashMap<>();
insertion.put("A", 1);
insertion.put("B", 2);
insertion.put("C", 3);
insertion.get("A"); // ์์ ๋ณ๊ฒฝ X
// ์ํ: A, B, C (์ฝ์
์์)
// 2. ์ ๊ทผ ์์ (accessOrder = true)
Map<String, Integer> access = new LinkedHashMap<>(16, 0.75f, true);
access.put("A", 1);
access.put("B", 2);
access.put("C", 3);
access.get("A"); // A ๊ฐ ๋งจ ๋ค๋ก!
// ์ํ: B, C, A (์ ๊ทผ ์์)
// LinkedHashSet ๋ ๊ฐ์ ์๋ฆฌ
Set<String> set = new LinkedHashSet<>();
set.add("C");
set.add("A");
set.add("B");
for (String s : set) {
System.out.println(s); // C, A, B (์ฝ์
์์)
}
โ HashSet + ์๋ฐฉํฅ ๋ฆฌ์คํธ.
@Service
public class CargoFareService {
private final NavigableMap<Integer, List<Cargo>> cargosByFare = new TreeMap<>();
public void registerCargo(Cargo cargo) {
cargosByFare
.computeIfAbsent(cargo.getFare(), k -> new ArrayList<>())
.add(cargo);
}
public List<Cargo> findByFareRange(int minFare, int maxFare) {
// O(log n + k) โ ์ ๋ ฌ ํธ๋ฆฌ์์ ๋ฒ์ ๊ฒ์
return cargosByFare.subMap(minFare, true, maxFare, true)
.values().stream()
.flatMap(List::stream)
.collect(Collectors.toList());
}
public Cargo findCheapestAbove(int minFare) {
// ๊ฐ์ฅ ์ ๋ ดํ ํ๋ฌผ ์ค minFare ์ด์
Map.Entry<Integer, List<Cargo>> entry = cargosByFare.ceilingEntry(minFare);
return entry == null ? null : entry.getValue().get(0);
}
public Cargo findMostExpensive() {
Map.Entry<Integer, List<Cargo>> entry = cargosByFare.lastEntry();
return entry == null ? null : entry.getValue().get(0);
}
}
@Service
public class FareCalculationCache {
private final LRUCache<String, BigDecimal> cache = new LRUCache<>(1000);
public BigDecimal getFare(String routeKey) {
BigDecimal cached = cache.get(routeKey);
if (cached != null) return cached;
// ๋น์ผ ๊ณ์ฐ
BigDecimal computed = fareCalculator.calculate(routeKey);
cache.put(routeKey, computed);
return computed;
}
}
// LRU ์บ์ โ 6 ์ค!
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
ํจ๊ณผ:
@Service
public class DailySalesService {
// ์ผ์ -> ๋งค์ถ
private final NavigableMap<LocalDate, Long> dailySales = new TreeMap<>();
public void recordSale(LocalDate date, long amount) {
dailySales.merge(date, amount, Long::sum);
}
public Map<LocalDate, Long> getRecentDays(int days) {
LocalDate today = LocalDate.now();
LocalDate from = today.minusDays(days);
// O(log n + k)
return dailySales.subMap(from, true, today, true);
}
public Long getMaxSale() {
return dailySales.values().stream()
.max(Long::compareTo)
.orElse(0L);
}
public LocalDate getMaxSaleDate() {
return dailySales.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
}
public void printChronological() {
// ์๋์ผ๋ก ๋ ์ง์!
dailySales.forEach((date, amount) ->
System.out.println(date + ": " + amount + "์"));
}
}
public class TopNAnalysis {
public List<Map.Entry<String, Integer>> getTopCustomers(
Map<String, Integer> ordersByCustomer, int n) {
// ์ฃผ๋ฌธ ์ โ ๊ณ ๊ฐ (์ ๋ ฌ ํธ๋ฆฌ)
NavigableMap<Integer, List<String>> byCount = new TreeMap<>();
ordersByCustomer.forEach((customer, count) ->
byCount.computeIfAbsent(count, k -> new ArrayList<>()).add(customer));
// ์์ N ๊ฐ
List<Map.Entry<String, Integer>> top = new ArrayList<>();
for (Map.Entry<Integer, List<String>> e : byCount.descendingMap().entrySet()) {
for (String customer : e.getValue()) {
if (top.size() >= n) return top;
top.add(Map.entry(customer, e.getKey()));
}
}
return top;
}
}
@RestController
public class CargoController {
@GetMapping("/api/cargo/{id}")
public Map<String, Object> getCargoDetails(@PathVariable Long id) {
Cargo cargo = cargoService.findById(id);
// ์์๊ฐ ์ค์ํ ์๋ต
Map<String, Object> response = new LinkedHashMap<>();
response.put("id", cargo.getId());
response.put("name", cargo.getName());
response.put("weight", cargo.getWeight());
response.put("fare", cargo.getFare());
response.put("originPort", cargo.getOriginPort());
response.put("destinationPort", cargo.getDestinationPort());
response.put("createdAt", cargo.getCreatedAt());
return response;
// JSON ์ถ๋ ฅ ์์ ๋ณด์ฅ!
}
}
public class CustomerSortExample {
public NavigableMap<Customer, Order> sortByGradeAndName(
Map<Customer, Order> orders) {
// ๋ฑ๊ธ (๋์ ๋ฑ๊ธ ์ฐ์ ) โ ์ด๋ฆ (๊ฐ๋๋ค์)
Comparator<Customer> sorter = Comparator
.comparing(Customer::getGrade, Comparator.reverseOrder())
.thenComparing(Customer::getName);
TreeMap<Customer, Order> sorted = new TreeMap<>(sorter);
sorted.putAll(orders);
return sorted;
}
public NavigableMap<Customer, Order> sortByFare(
Map<Customer, Order> orders) {
// ์ด์ (๋์ ์)
Comparator<Customer> byFare = (a, b) -> {
long fareA = orders.get(a).getFare();
long fareB = orders.get(b).getFare();
return Long.compare(fareB, fareA); // ๋ด๋ฆผ์ฐจ์
};
TreeMap<Customer, Order> sorted = new TreeMap<>(byFare);
sorted.putAll(orders);
return sorted;
}
}
public class CacheWithStats<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
private long hits = 0;
private long misses = 0;
public CacheWithStats(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
public V get(Object key) {
V value = super.get(key);
if (value != null) hits++;
else misses++;
return value;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
public double getHitRate() {
long total = hits + misses;
return total == 0 ? 0 : (double) hits / total;
}
public void printStats() {
System.out.printf("Hits: %d, Misses: %d, HitRate: %.2f%%%n",
hits, misses, getHitRate() * 100);
}
}
public class OrderBook {
// ๋งค์ ์ฃผ๋ฌธ (๊ฐ๊ฒฉ ๋์ ์์ผ๋ก ์ ๋ ฌ)
private final NavigableMap<BigDecimal, Long> bids = new TreeMap<>(Comparator.reverseOrder());
// ๋งค๋ ์ฃผ๋ฌธ (๊ฐ๊ฒฉ ๋ฎ์ ์์ผ๋ก ์ ๋ ฌ)
private final NavigableMap<BigDecimal, Long> asks = new TreeMap<>();
public void addBid(BigDecimal price, long quantity) {
bids.merge(price, quantity, Long::sum);
}
public void addAsk(BigDecimal price, long quantity) {
asks.merge(price, quantity, Long::sum);
}
public BigDecimal getBestBid() {
return bids.isEmpty() ? null : bids.firstKey(); // ๊ฐ์ฅ ๋์ ๋งค์ ๊ฐ๊ฒฉ
}
public BigDecimal getBestAsk() {
return asks.isEmpty() ? null : asks.firstKey(); // ๊ฐ์ฅ ๋ฎ์ ๋งค๋ ๊ฐ๊ฒฉ
}
public BigDecimal getSpread() {
BigDecimal bestBid = getBestBid();
BigDecimal bestAsk = getBestAsk();
if (bestBid == null || bestAsk == null) return null;
return bestAsk.subtract(bestBid);
}
public Map<BigDecimal, Long> getTop5Bids() {
return bids.entrySet().stream()
.limit(5)
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new));
// LinkedHashMap โ ์์ ๋ณด์กด
}
}
TreeMap<String, Integer> map = new TreeMap<>();
map.put(null, 1); // โ NullPointerException
์ด์ : ์ ๋ ฌ์ ์ํด ํค ๋น๊ต โ null ๋น๊ต ๋ถ๊ฐ.
ํด๊ฒฐ: HashMap ๋๋ LinkedHashMap ์ฌ์ฉ (null ํค 1๊ฐ ํ์ฉ).
class Customer {
String customerId;
}
TreeMap<Customer, String> map = new TreeMap<>();
map.put(new Customer(), "value"); // โ ClassCastException
์ด์ : Customer ๊ฐ Comparable ๋ฏธ๊ตฌํ โ ์ ๋ ฌ ๋ถ๊ฐ.
ํด๊ฒฐ 1: Comparable ๊ตฌํ
class Customer implements Comparable<Customer> {
@Override
public int compareTo(Customer other) {
return this.customerId.compareTo(other.customerId);
}
}
ํด๊ฒฐ 2: Comparator ์ ๋ฌ
TreeMap<Customer, String> map = new TreeMap<>(
Comparator.comparing(Customer::getCustomerId)
);
NavigableMap<Integer, String> map = new TreeMap<>();
map.put(10, "A"); map.put(20, "B"); map.put(30, "C");
Map<Integer, String> sub = map.subMap(15, 25); // {20=B}
map.put(25, "X"); // ์๋ณธ ์์
System.out.println(sub); // {20=B, 25=X} โ view ๊ฐ ๊ฐฑ์ ๋จ!
// ์์ ์ ์๋ณธ๋ ๋ณ๊ฒฝ
sub.put(22, "Y");
System.out.println(map); // {10=A, 20=B, 22=Y, 25=X, 30=C}
์์น: subMap ์ view. ๋ ๋ฆฝ ์ฌ๋ณธ ํ์ ์ ๋ณต์ฌ.
Map<Integer, String> copy = new TreeMap<>(map.subMap(15, 25));
// LRU ์๋
Map<String, Integer> cache = new LinkedHashMap<>(100); // โ ๊ธฐ๋ณธ = ์ฝ์
์์
cache.put("A", 1);
cache.get("A"); // ์์ ๋ณ๊ฒฝ ์ ๋จ!
ํด๊ฒฐ: ์์ฑ์ ๋ช ์
new LinkedHashMap<>(100, 0.75f, true); // accessOrder = true
// LRU ์บ์ ์๋
Map<String, Data> cache = new LinkedHashMap<>(100, 0.75f, true);
// โ ์๋ ์ ๊ฑฐ ์ ๋จ!
// removeEldestEntry ๊ฐ ํญ์ false ๋ฐํ (๊ธฐ๋ณธ๊ฐ)
ํด๊ฒฐ: ์ต๋ช ํด๋์ค ๋๋ ์์
Map<String, Data> cache = new LinkedHashMap<>(100, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
return size() > 100;
}
};
// โ ๋น ๋ฅธ lookup ์ด ํ์ํ๋ฐ TreeMap ์ฌ์ฉ
Map<String, Customer> lookup = new TreeMap<>();
for (int i = 0; i < 1_000_000; i++) {
lookup.get(keys[i]); // O(log n) ร 100๋ง โ 2์ฒ๋ง
}
ํด๊ฒฐ: ์ ๋ ฌ/๋ฒ์ ํ์ ์์ผ๋ฉด HashMap.
Map<String, Customer> lookup = new HashMap<>(); // O(1) ร 100๋ง = 100๋ง
// 100๋ง ๋ฐ์ดํฐ + ์์ ๋ถํ์
Map<String, Customer> map = new LinkedHashMap<>(1_000_000); // โ 1.5๋ฐฐ ๋ฉ๋ชจ๋ฆฌ
ํด๊ฒฐ: ์์ ํ์ ์์ผ๋ฉด HashMap.
Map<String, Customer> map = new HashMap<>(1_000_000); // ๋ฉ๋ชจ๋ฆฌ ํจ์จ
class Cargo {
private double weight;
// โ Comparable ์๋ฐ
public int compareTo(Cargo other) {
if (this.weight < other.weight) return -1;
return 1; // โ ๏ธ ๊ฐ์ ๋๋ 1 ๋ฐํ โ ์ผ๊ด์ฑ X
}
}
์์น: compareTo == 0 ์ด๋ฉด equals == true ์ฌ์ผ ํจ (์ ๋ ฌ ์์ ์ฑ).
ํด๊ฒฐ:
public int compareTo(Cargo other) {
return Double.compare(this.weight, other.weight);
}
[Unit 6.1: String + Constant Pool] โ
โ
[Unit 6.2: StringBuilder vs StringBuffer] โ
โ
[Unit 6.3: ArrayList vs LinkedList] โ
โ
[Unit 6.4: HashMap ๋ด๋ถ ๊ตฌ์กฐ] โ โ
โ
โ
โ
[Unit 6.5: TreeMap, LinkedHashMap] โ ์ง๊ธ (Phase 6 ์๋ฃ!)
โ
[Phase 7: I/O & NIO] (์์ )
List โโโ ArrayList โญ (Unit 6.3)
โโโ LinkedList
โโโ Vector
Set โโโ HashSet
โโโ LinkedHashSet (LinkedHashMap ์ Set ๋ฒ์ )
โโโ TreeSet (TreeMap ์ Set ๋ฒ์ )
Map โโโ HashMap โญ (Unit 6.4)
โโโ LinkedHashMap โญ (Unit 6.5)
โโโ TreeMap โญ (Unit 6.5)
โโโ ConcurrentHashMap
โโโ Hashtable (legacy)
Queue โโ ArrayDeque โญ
โโโ LinkedList (Deque ๋ ๊ตฌํ)
โโโ PriorityQueue (TreeMap ๊ณผ ์ ์ฌ โ ์ ๋ ฌ ์ ์ง)
| ์ฌ์ฉ์ฒ | ์ค๋ช |
|---|---|
| TreeMap | Map ์ ๊ธฐ๋ณธ ๊ตฌํ |
| TreeSet | Set ์ ์ ๋ ฌ ๊ตฌํ |
| HashMap (Java 8+) | ์ถฉ๋ 8๊ฐ ์ด๊ณผ ์ ํธ๋ฆฌํ |
| LinkedHashMap (Java 8+) | ๊ฐ์ ํธ๋ฆฌํ |
| ConcurrentHashMap (Java 8+) | ๊ฐ์ ํธ๋ฆฌํ |
โ Red-Black Tree ๋ ์๋ฐ ์ปฌ๋ ์ ์ ํต์ฌ.
| ์ธ์ด | ์ ๋ ฌ Map | ์์ Map |
|---|---|---|
| Java | TreeMap | LinkedHashMap |
| Python | (sortedcontainers) | OrderedDict |
| JavaScript | (์์) | Map (์ฝ์ ์์) |
| C++ | std::map | (์์) |
| C# | SortedDictionary | (์์) |
| Rust | BTreeMap | IndexMap (์ธ๋ถ) |
โ ์๋ฐ๋ ๋ ๊ฐ์ง ๋ชจ๋ ํ์ค ์ ๊ณต โ ๊ฐ์ .
| ์ง๋ฌธ | ์ด Unit ์์์ ๋ต |
|---|---|
| TreeMap ์ธ์ ? | ์ ๋ ฌ/๋ฒ์ ์ฟผ๋ฆฌ |
| LinkedHashMap ์ธ์ ? | ์์ ๋ณด์กด/LRU |
| LRU ์บ์ ๊ตฌํ? | LinkedHashMap + accessOrder + removeEldestEntry |
| TreeMap ๋ด๋ถ? | Red-Black Tree |
| HashMap vs TreeMap? | O(1) vs O(log n) |
1๏ธโฃ TreeMap = Red-Black Tree, LinkedHashMap = HashMap + ์๋ฐฉํฅ ๋ฆฌ์คํธ.
TreeMap ์ ์ ๋ ฌ๋ ์ํ ์ ์ง โ ๋ชจ๋ ์์ O(log n), ๊ทธ๋ฌ๋ ๋ฒ์ ์ฟผ๋ฆฌ/๊ทผ์ ํค ๊ฒ์ ์ด ๊ฐ์ (subMap, floorKey, ceilingKey). LinkedHashMap ์ HashMap ์ ๋ชจ๋ ๊ธฐ๋ฅ (O(1)) + ์๋ฐฉํฅ ์ฐ๊ฒฐ ๋ฆฌ์คํธ ๋ก ์ฝ์ ์์ ๋๋ ์ ๊ทผ ์์ ์ ์ง. ๋ฉ๋ชจ๋ฆฌ๋ HashMap ์ 1.5๋ฐฐ.
2๏ธโฃ LinkedHashMap ์ accessOrder=true + removeEldestEntry = LRU ์บ์ 6์ค.
accessOrder=true๋ฉด get ์ ๊ทธ ํญ๋ชฉ์ด ๋งจ ๋ค๋ก ์ด๋ โ head ๋ ํญ์ ๊ฐ์ฅ ์ค๋ ์ ์ด ํญ๋ชฉ. removeEldestEntry ์ค๋ฒ๋ผ์ด๋ ๋ก capacity ์ด๊ณผ ์ head ์๋ ์ ๊ฑฐ. ๋ชจ๋ ์์ O(1). ์๋ฐ ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์จ๊ฒจ์ง ๋ณด์.3๏ธโฃ ์ ํ ์์น โ ์ ๋ ฌ/๋ฒ์ โ TreeMap, ์์/LRU โ LinkedHashMap, ๊ทธ ์ธ โ HashMap.
TreeMap ์ ๊ฐ์ :
subMap,headMap,tailMap,floorKey,ceilingKey๊ฐ์ NavigableMap ๋ฉ์๋. LinkedHashMap ์ ๊ฐ์ : ์ฝ์ ์์ ๋ณด์กด (API ์๋ต JSON ์์), LRU ์บ์. HashMap ์ ์์ ๋ฌด๊ด ์ ๊ธฐ๋ณธ. ILIC ์ ๊ฐ๊ฒฉ๋๋ณ ํ๋ฌผ = TreeMap, ์ด์ ์บ์ = LinkedHashMap, ์ผ๋ฐ ๋งคํ = HashMap. Phase 6 ์๋ฃ โ ์๋ฐ ์ปฌ๋ ์ ์ ๋ณต!
Q1: LRU ์บ์๋ฅผ LinkedHashMap ์ผ๋ก ์ด๋ป๊ฒ ๊ตฌํํ๋๊ฐ?
ํ ์ค ๋ต: accessOrder=true ๋ก ์์ฑ ํ removeEldestEntry ์ค๋ฒ๋ผ์ด๋. 6์ค ๊ตฌํ, ๋ชจ๋ ์์
O(1).
์์ธ ์ค๋ช :
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // โ
accessOrder = true
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // โ
์ด๊ณผ ์ ๊ฐ์ฅ ์ค๋๋ ์ ๊ฑฐ
}
}
6์ค๋ก LRU ์์ฑ!
์ํ 1: ์บ์ capacity = 3, ๋น์ด์์
head โ null โ tail
์ํ 2: put("A", 1)
head โ A โ tail
์ํ 3: put("B", 2), put("C", 3)
head โ A โ B โ C โ tail
์ํ 4: get("A")
A ๊ฐ ์ ๊ทผ๋จ โ ๋งจ ๋ค๋ก ์ด๋
head โ B โ C โ A โ tail
์ํ 5: put("D", 4) โ capacity ์ด๊ณผ!
1. afterNodeInsertion(true) ํธ์ถ
2. removeEldestEntry(head=B) ํธ์ถ โ size()=4 > 3 โ true
3. head (B) ์ ๊ฑฐ
head โ C โ A โ D โ tail
โ ๊ฐ์ฅ ์ค๋ ์ ์ด ํญ๋ชฉ (B) ์๋ ์ ๊ฑฐ.
๋ชจ๋ ์์ : O(1)
get: HashMap ์ O(1) + ์๋ฐฉํฅ ๋ฆฌ์คํธ ์ด๋ O(1)put: HashMap ์ O(1) + ๋ฆฌ์คํธ ๋ ์ถ๊ฐ O(1)remove (LRU): head ์ ๊ฑฐ O(1)@Service
public class CustomerCache {
private final Map<String, Customer> cache =
Collections.synchronizedMap(new LRUCache<>(10000));
// 10,000 ๊ฐ๊น์ง ์บ์, ๊ทธ ์ด์์ ์๋ ์ ๊ฑฐ
public Customer getCustomer(String id) {
return cache.computeIfAbsent(id, this::loadFromDb);
}
private Customer loadFromDb(String id) {
return customerRepository.findById(id).orElse(null);
}
}
LinkedHashMap ์ Thread-Safe X.
ํด๊ฒฐ 1: synchronizedMap
Map<K, V> cache = Collections.synchronizedMap(new LRUCache<>(100));
ํด๊ฒฐ 2: Caffeine ๋ผ์ด๋ธ๋ฌ๋ฆฌ (๊ถ์ฅ)
Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(100)
.build();
ํด๊ฒฐ 3: ConcurrentHashMap + ๋ณ๋ LRU ๋ก์ง (๋ณต์ก)
"LinkedHashMap ์ accessOrder=true + removeEldestEntry ์ค๋ฒ๋ผ์ด๋ = ์๋ฒฝํ LRU.
6์ค๋ก ๋ชจ๋ ์์ O(1), ๋ฉ๋ชจ๋ฆฌ ์ผ์ .
์๋ฐ ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์จ๊ฒจ์ง ๋ณด์ โ ์๋ฉด ์๋์ด, ๋ชจ๋ฅด๋ฉด ์ง์ ๊ตฌํํด O(n).
๋ฉํฐ์ค๋ ๋ ํ๊ฒฝ์์๋ synchronizedMap ๋๋ Caffeine ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ฉ."
Q2: TreeMap ์ subMap ๊ณผ HashMap ์ผ๋ก ๊ฐ์ ์์ ๊ตฌํ ์ ์๊ฐ ์ฐจ์ด๋?
ํ ์ค ๋ต: TreeMap ์ subMap = O(log n + k), HashMap ์ผ๋ก ๊ฐ์ ์์ = O(n). ๋ฐ์ดํฐ ํฌ๋ฉด ์์ฒ ๋ฐฐ ์ฐจ์ด.
์์ธ ์ค๋ช :
// 100๋ง ๊ฐ ํ๋ฌผ, 5,000~10,000์ ๋ฒ์ ๊ฒ์
NavigableMap<Integer, List<Cargo>> tree = new TreeMap<>();
Map<Integer, List<Cargo>> hash = new HashMap<>();
// ๋ฐ์ดํฐ ์ฑ์ฐ๊ธฐ ...
// TreeMap
List<Cargo> result1 = tree.subMap(5000, 10001).values().stream()
.flatMap(List::stream).collect(Collectors.toList());
// HashMap
List<Cargo> result2 = hash.entrySet().stream()
.filter(e -> e.getKey() >= 5000 && e.getKey() <= 10000)
.flatMap(e -> e.getValue().stream())
.collect(Collectors.toList());
TreeMap.subMap:
1. ์์ ํค (5000) ์์น ์ฐพ๊ธฐ: O(log n) โ 20๋ฒ ๋น๊ต (100๋ง)
2. ๋ ํค (10001) ์์น ์ฐพ๊ธฐ: O(log n) โ 20๋ฒ ๋น๊ต
3. ์ฌ์ด์ entry ์ํ: O(k) (์: 100๊ฐ)
4. ํฉ๊ณ: O(log n + k) โ 140๋ฒ ์์
HashMap ํํฐ๋ง:
1. ๋ชจ๋ entry ์ํ: O(n) = 100๋ง ๋ฒ
2. ๊ฐ๊ฐ ๋ฒ์ ๊ฒ์ฌ: 100๋ง ๋ฒ ๋น๊ต
3. ํฉ๊ณ: O(n) = 100๋ง ๋ฒ
์ฐจ์ด: ์ฝ 7,000๋ฐฐ.
public class RangeQueryBenchmark {
public static void main(String[] args) {
int N = 1_000_000;
NavigableMap<Integer, String> tree = new TreeMap<>();
Map<Integer, String> hash = new HashMap<>();
for (int i = 0; i < N; i++) {
tree.put(i, "value-" + i);
hash.put(i, "value-" + i);
}
// TreeMap subMap
long start = System.nanoTime();
Map<Integer, String> sub = tree.subMap(500000, 500100);
int size1 = sub.size();
long treeTime = System.nanoTime() - start;
// HashMap ํํฐ๋ง
start = System.nanoTime();
long count = hash.entrySet().stream()
.filter(e -> e.getKey() >= 500000 && e.getKey() < 500100)
.count();
long hashTime = System.nanoTime() - start;
System.out.println("TreeMap subMap: " + treeTime + " ns");
// ์ฝ 10,000 ns = 10 ฮผs
System.out.println("HashMap filter: " + hashTime + " ns");
// ์ฝ 30,000,000 ns = 30 ms (3,000๋ฐฐ)
}
}
| ๋ฐ์ดํฐ ์ | TreeMap subMap | HashMap filter | ์ฐจ์ด |
|---|---|---|---|
| 1,000 | 1 ฮผs | 50 ฮผs | 50๋ฐฐ |
| 10,000 | 2 ฮผs | 500 ฮผs | 250๋ฐฐ |
| 100,000 | 5 ฮผs | 5 ms | 1,000๋ฐฐ |
| 1,000,000 | 10 ฮผs | 30 ms | 3,000๋ฐฐ |
| 10,000,000 | 15 ฮผs | 300 ms | 20,000๋ฐฐ |
โ ๋ฐ์ดํฐ ํด์๋ก ์ฐจ์ด ํญ์ฆ.
"๋ฒ์ ์ฟผ๋ฆฌ๋ ์๋ฃ๊ตฌ์กฐ ์ ํ์ ๊ฒฐ์ ์ ์ฐจ์ด.
TreeMap ์ ์ ๋ ฌ๋ ๊ตฌ์กฐ ๋๋ถ์ O(log n + k) โ ๋ฐ์ดํฐ๊ฐ 100๋ง ๊ฐ์ฌ๋ ๊ฑฐ์ ์ฆ์.
HashMap ์ ์ ๋ ฌ X โ O(n) ํํฐ๋ง ๊ฐ์ โ ๋งค๋ฒ ์ ์ฒด ์ํ.
ILIC ์ ๊ฐ๊ฒฉ๋๋ณ ํ๋ฌผ, ์๊ฐ๋๋ณ ์กฐํ, ๋ฑ๊ธ๋ณ ๊ฒ์ ๋ฑ ๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋น๋ฒํ๋ฉด TreeMap ์ ๋ต.
์๋์ด ์ฐจ๋ณํ: subMap ์ view ํน์ฑ, NavigableMap ์ ๊ฐ๋ ฅํ ๋ฉ์๋ ํ์ฉ."