Post

고급 자료구조: 해시맵, 셋, 트리, 힙 완벽 가이드

📌 학습 목표

  • 중급 자료구조의 특성과 동작 원리 이해
  • 해시 기반, 트리 기반, 힙 기반 구조의 차이점 학습
  • 각 자료구조의 시간/공간 복잡도 분석
  • 실무에서의 적절한 자료구조 선택 기준 습득

📝 개념 정리

1. 해시맵 (Hash Map)

핵심 원리:

  • 해시 함수를 통해 키를 인덱스로 변환하여 키-값 쌍 저장
  • 평균 시간복잡도: O(1) - 탐색, 삽입, 삭제
  • 최악 시간복잡도: O(n) - 모든 키가 같은 해시값을 가질 때
  • 공간복잡도: O(n)

해시 충돌 해결 방법:

  1. 체이닝 (Chaining): ```cpp class HashMapChaining { private: vector<list<pair<int, string»> table; int size;

    int hashFunction(int key) { return key % size; }

public: HashMapChaining(int s) : size(s) { table.resize(size); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void insert(int key, string value) {
    int index = hashFunction(key);
    
    // 기존 키 업데이트 확인
    for (auto& pair : table[index]) {
        if (pair.first == key) {
            pair.second = value;
            return;
        }
    }
    
    // 새 키-값 쌍 추가
    table[index].push_back({key, value});
}

string search(int key) {
    int index = hashFunction(key);
    for (auto& pair : table[index]) {
        if (pair.first == key) {
            return pair.second;
        }
    }
    return "";  // 키를 찾지 못함
} }; ```
  1. 개방 주소법 (Open Addressing): ```cpp class HashMapOpenAddressing { private: vector<pair<int, string» table; vector deleted; int size;

    int hashFunction(int key) { return key % size; }

    int linearProbe(int key) { int index = hashFunction(key); while (table[index].first != -1 && table[index].first != key) { index = (index + 1) % size; } return index; }

public: HashMapOpenAddressing(int s) : size(s) { table.resize(size, {-1, “”}); deleted.resize(size, false); }

1
2
3
4
5
void insert(int key, string value) {
    int index = linearProbe(key);
    table[index] = {key, value};
    deleted[index] = false;
} }; ```

2. 셋 (Set)

핵심 원리:

  • 중복을 허용하지 않는 원소들의 집합
  • 일반적으로 트리 기반(Red-Black Tree) 또는 해시 기반 구현
  • 시간복잡도: O(log n) - 트리 기반, O(1) - 해시 기반

C++ STL 예제:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <set>
#include <unordered_set>

// 트리 기반 set (정렬된 상태 유지)
set<int> orderedSet;
orderedSet.insert(3);
orderedSet.insert(1);
orderedSet.insert(4);
// 결과: {1, 3, 4}

// 해시 기반 unordered_set (정렬되지 않음, 더 빠름)
unordered_set<int> hashSet;
hashSet.insert(3);
hashSet.insert(1);
hashSet.insert(4);
// 결과: 순서 보장 안됨

// 집합 연산
set<int> setA = {1, 2, 3, 4};
set<int> setB = {3, 4, 5, 6};
set<int> result;

// 교집합
set_intersection(setA.begin(), setA.end(),
                 setB.begin(), setB.end(),
                 inserter(result, result.begin()));
// 결과: {3, 4}

3. 트리 (Tree)

이진 탐색 트리 (Binary Search Tree):

  • 왼쪽 자식 < 부모 < 오른쪽 자식 속성 유지
  • 평균 시간복잡도: O(log n)
  • 최악 시간복잡도: O(n) - 편향 트리일 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

class BST {
private:
    TreeNode* root;
    
    TreeNode* insert(TreeNode* node, int val) {
        if (!node) return new TreeNode(val);
        
        if (val < node->val) {
            node->left = insert(node->left, val);
        } else if (val > node->val) {
            node->right = insert(node->right, val);
        }
        return node;
    }
    
    TreeNode* search(TreeNode* node, int val) {
        if (!node || node->val == val) return node;
        
        if (val < node->val) {
            return search(node->left, val);
        }
        return search(node->right, val);
    }
    
    TreeNode* findMin(TreeNode* node) {
        while (node->left) node = node->left;
        return node;
    }
    
    TreeNode* deleteNode(TreeNode* node, int val) {
        if (!node) return node;
        
        if (val < node->val) {
            node->left = deleteNode(node->left, val);
        } else if (val > node->val) {
            node->right = deleteNode(node->right, val);
        } else {
            // 삭제할 노드 발견
            if (!node->left) {
                TreeNode* temp = node->right;
                delete node;
                return temp;
            } else if (!node->right) {
                TreeNode* temp = node->left;
                delete node;
                return temp;
            }
            
            // 두 자식 모두 있는 경우
            TreeNode* temp = findMin(node->right);
            node->val = temp->val;
            node->right = deleteNode(node->right, temp->val);
        }
        return node;
    }
    
public:
    BST() : root(nullptr) {}
    
    void insert(int val) {
        root = insert(root, val);
    }
    
    bool search(int val) {
        return search(root, val) != nullptr;
    }
    
    void remove(int val) {
        root = deleteNode(root, val);
    }
};

자가 균형 트리 (Self-Balancing Trees):

  • AVL 트리: 모든 노드의 좌우 서브트리 높이 차이 ≤ 1
  • Red-Black 트리: 색상 규칙으로 균형 유지 (C++ STL map/set 내부 구현)

4. 힙 (Heap)

핵심 원리:

  • 완전 이진 트리 기반의 자료구조
  • 최대 힙: 부모 노드 ≥ 자식 노드
  • 최소 힙: 부모 노드 ≤ 자식 노드
  • 우선순위 큐 구현에 주로 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class MinHeap {
private:
    vector<int> heap;
    
    void heapifyUp(int index) {
        if (index == 0) return;
        
        int parent = (index - 1) / 2;
        if (heap[parent] > heap[index]) {
            swap(heap[parent], heap[index]);
            heapifyUp(parent);
        }
    }
    
    void heapifyDown(int index) {
        int left = 2 * index + 1;
        int right = 2 * index + 2;
        int smallest = index;
        
        if (left < heap.size() && heap[left] < heap[smallest]) {
            smallest = left;
        }
        
        if (right < heap.size() && heap[right] < heap[smallest]) {
            smallest = right;
        }
        
        if (smallest != index) {
            swap(heap[index], heap[smallest]);
            heapifyDown(smallest);
        }
    }
    
public:
    void insert(int val) {
        heap.push_back(val);
        heapifyUp(heap.size() - 1);
    }
    
    int extractMin() {
        if (heap.empty()) throw runtime_error("Heap is empty");
        
        int root = heap[0];
        heap[0] = heap.back();
        heap.pop_back();
        
        if (!heap.empty()) {
            heapifyDown(0);
        }
        
        return root;
    }
    
    int peek() {
        if (heap.empty()) throw runtime_error("Heap is empty");
        return heap[0];
    }
    
    bool empty() {
        return heap.empty();
    }
};

💻 실무 활용 예제

1. 단어 빈도 계산 (해시맵 활용)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unordered_map<string, int> countWords(vector<string>& words) {
    unordered_map<string, int> frequency;
    
    for (const string& word : words) {
        frequency[word]++;
    }
    
    return frequency;
}

// 사용 예시
vector<string> document = {"apple", "banana", "apple", "cherry", "banana", "apple"};
auto freq = countWords(document);
// 결과: {"apple": 3, "banana": 2, "cherry": 1}

2. 우선순위 작업 스케줄러 (힙 활용)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct Task {
    int priority;
    string description;
    
    // 우선순위가 높을수록 먼저 처리 (최대 힙)
    bool operator<(const Task& other) const {
        return priority < other.priority;
    }
};

class TaskScheduler {
private:
    priority_queue<Task> taskQueue;
    
public:
    void addTask(int priority, string description) {
        taskQueue.push({priority, description});
    }
    
    Task getNextTask() {
        if (taskQueue.empty()) {
            throw runtime_error("No tasks available");
        }
        
        Task nextTask = taskQueue.top();
        taskQueue.pop();
        return nextTask;
    }
    
    bool hasTasks() {
        return !taskQueue.empty();
    }
};

3. 중복 제거와 정렬 (셋 활용)

1
2
3
4
5
6
7
8
9
vector<int> removeDuplicatesAndSort(vector<int>& nums) {
    set<int> uniqueNums(nums.begin(), nums.end());
    return vector<int>(uniqueNums.begin(), uniqueNums.end());
}

// 사용 예시
vector<int> input = {3, 1, 4, 1, 5, 9, 2, 6, 5};
vector<int> result = removeDuplicatesAndSort(input);
// 결과: {1, 2, 3, 4, 5, 6, 9}

🎯 연습 문제

1. LRU 캐시 구현 (해시맵 + 이중 연결 리스트)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class LRUCache {
private:
    struct Node {
        int key, value;
        Node* prev;
        Node* next;
        Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
    };
    
    unordered_map<int, Node*> cache;
    Node* head;
    Node* tail;
    int capacity;
    
    void addToHead(Node* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }
    
    void removeNode(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
    
public:
    LRUCache(int cap) : capacity(cap) {
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
        if (cache.find(key) != cache.end()) {
            Node* node = cache[key];
            removeNode(node);
            addToHead(node);
            return node->value;
        }
        return -1;
    }
    
    void put(int key, int value) {
        if (cache.find(key) != cache.end()) {
            Node* node = cache[key];
            node->value = value;
            removeNode(node);
            addToHead(node);
        } else {
            if (cache.size() >= capacity) {
                Node* last = tail->prev;
                removeNode(last);
                cache.erase(last->key);
                delete last;
            }
            
            Node* newNode = new Node(key, value);
            addToHead(newNode);
            cache[key] = newNode;
        }
    }
};

2. K개의 가장 빈번한 원소 찾기 (힙 활용)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vector<int> topKFrequent(vector<int>& nums, int k) {
    unordered_map<int, int> frequency;
    for (int num : nums) {
        frequency[num]++;
    }
    
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> minHeap;
    
    for (auto& [num, freq] : frequency) {
        minHeap.push({freq, num});
        if (minHeap.size() > k) {
            minHeap.pop();
        }
    }
    
    vector<int> result;
    while (!minHeap.empty()) {
        result.push_back(minHeap.top().second);
        minHeap.pop();
    }
    
    return result;
}

🔎 심화 학습

고급 해시 기법

1
2
3
// Robin Hood Hashing - 더 균등한 분포
// Cuckoo Hashing - 최악의 경우에도 O(1) 보장
// Consistent Hashing - 분산 시스템에서 활용

고급 트리 구조

1
2
3
4
// B-Tree, B+ Tree - 데이터베이스 인덱스
// Trie (Prefix Tree) - 문자열 검색
// Segment Tree - 구간 쿼리 처리
// Fenwick Tree (Binary Indexed Tree) - 누적합 쿼리

자료구조 성능 비교표

| 자료구조 | 탐색 | 삽입 | 삭제 | 공간복잡도 | 특징 | |———|——|——|——|————|——| | 해시맵 | O(1) | O(1) | O(1) | O(n) | 순서 없음, 충돌 가능 | | 트리맵 | O(log n) | O(log n) | O(log n) | O(n) | 정렬된 순서 유지 | | 힙 | O(n) | O(log n) | O(log n) | O(n) | 최대/최소값 빠른 접근 |


🌐 외부 링크


💡 실무 적용 팁

  1. 자료구조 선택 기준:
    • 빈번한 탐색이 필요하면 해시맵 고려
    • 정렬된 데이터가 필요하면 트리맵 선택
    • 우선순위 처리가 필요하면 힙 활용
  2. 성능 최적화:
    • 해시맵의 로드 팩터 관리 (0.75 이하 유지)
    • 트리의 균형 유지로 최악 성능 방지
    • 힙에서는 배열 기반 구현으로 캐시 효율성 향상
  3. 메모리 관리:
    • 동적 할당 최소화
    • 메모리 풀 사용 고려
    • 참조 지역성 고려한 데이터 배치

다음 학습 주제

  • 고급 트리: Trie, Segment Tree, Fenwick Tree
  • 그래프 자료구조: 인접 리스트, 인접 행렬, Union-Find
  • 문자열 자료구조: Suffix Array, Suffix Tree
  • 확률적 자료구조: Bloom Filter, Count-Min Sketch

🪞 회고 질문

  • 해시 충돌이 발생했을 때 어떤 해결 방법을 선택할 것인가?
  • 힙과 트리의 차이점을 실무 상황에 맞춰 설명할 수 있는가?
  • 대용량 데이터 처리 시 어떤 자료구조가 가장 적합할까?
  • 메모리 제약이 있는 환경에서는 어떤 자료구조를 우선적으로 고려해야 할까?
This post is licensed under CC BY 4.0 by the author.