자료구조 입문: 배열·리스트·스택·큐·트리·그래프의 특징과 고르는 기준

이 글의 핵심

자료구조를 문법 단위로만 외우면 실제 문제 앞에서 무엇을 써야 할지 판단하기 어렵습니다. 이 글은 선형과 비선형 구조를 나눠 각 구조의 특징과 대표 사용처를 설명하고, 조회·삽입·삭제 비용을 한 표로 비교한 뒤 시나리오별 추천과 최근 방문 페이지 예제로 선택 기준을 정리합니다.

같은 데이터라도 배열에 두느냐, 연결 리스트나 해시 테이블에 두느냐에 따라 삽입·삭제·검색 비용이 완전히 달라집니다. 이 글은 배열, 연결 리스트, 스택, 큐 같은 선형 구조와 트리, 그래프, 해시 테이블 같은 비선형 구조의 특징과 장단점을 C++ 예제로 살펴보고, 연산별 시간복잡도 비교와 상황별 선택 기준, 최근 방문 페이지 예제로 마무리합니다.


데이터 구조란?

데이터 구조(자료구조, Data Structure)는 데이터를 효율적으로 저장하고 관리하기 위한 방법입니다. 프로그램에서 데이터를 어떻게 조직화하느냐에 따라 성능이 크게 달라집니다.

왜 데이터 구조가 중요한가?

// ❌ 비효율적: 배열에서 중간 요소 삭제 (O(n))
vector<int> arr = {1, 2, 3, 4, 5};
arr.erase(arr.begin() + 2);  // 3을 삭제하려면 뒤의 모든 요소를 이동
// ✅ 효율적: 리스트에서 중간 요소 삭제 (O(1))
list<int> lst = {1, 2, 3, 4, 5};
auto it = next(lst.begin(), 2);
lst.erase(it);  // 포인터만 조정

올바른 자료구조를 선택하면:

  • 실행 속도가 빨라집니다
  • 메모리를 절약할 수 있습니다
  • 코드가 간결해집니다

다만 이 예제는 교과서적인 설명이 흔히 빠뜨리는 함정을 하나 품고 있습니다. lst.erase(it) 자체는 O(1)이지만, 그 it를 얻기 위한 next(lst.begin(), 2)는 앞에서부터 노드를 하나씩 따라가는 O(n) 작업입니다. “세 번째 원소를 지운다”는 작업 전체로 보면 리스트도 O(n)이고, 오히려 포인터를 따라 흩어진 메모리를 읽는 만큼 vector보다 느린 경우가 많습니다. 연결 리스트의 O(1) 삭제가 의미를 갖는 것은 이미 그 위치의 반복자를 들고 있을 때뿐입니다. 예를 들어 LRU 캐시처럼 해시 테이블에 리스트 반복자를 저장해 두고 바로 찾아가는 구조가 그 경우입니다. 자료구조를 고를 때는 연산 하나의 복잡도가 아니라 “그 연산을 하기까지 필요한 모든 단계”의 비용을 봐야 한다는 것이 이 글 전체를 관통하는 기준입니다.


선형 자료구조

배열 (Array)

연속된 메모리 공간에 같은 타입의 데이터를 저장합니다.

#include <iostream>
#include <vector>
using namespace std;
int main() {
    // 정적 배열
    int arr[5] = {1, 2, 3, 4, 5};
    
    // 동적 배열 (vector)
    vector<int> vec = {1, 2, 3, 4, 5};
    
    // 인덱스로 빠른 접근 O(1)
    cout << vec[2] << endl;  // 3
    
    // 끝에 추가 O(1)
    vec.push_back(6);
    
    // 중간에 삽입 O(n)
    vec.insert(vec.begin() + 2, 99);
}

장점:

  • 인덱스로 빠른 접근 (O(1))

  • 메모리 효율적 (연속 배치)

  • 캐시 친화적 단점:

  • 중간 삽입/삭제 느림 (O(n))

  • 크기 변경 비용 (재할당) 언제 사용?

  • 데이터 크기가 고정적일 때

  • 인덱스 접근이 빈번할 때

  • 순차 탐색이 주된 작업일 때

인덱스 접근이 O(1)인 이유는 원소가 메모리에 빈틈없이 붙어 있어서 시작 주소 + 인덱스 × 원소 크기로 위치를 바로 계산할 수 있기 때문입니다. 같은 이유로 순차 탐색도 빠릅니다. CPU는 메모리를 캐시 라인(보통 64바이트) 단위로 읽고 다음에 읽을 주소를 미리 가져오기(prefetch) 때문에, 연속된 배열을 앞에서부터 읽으면 대부분의 접근이 캐시에서 처리됩니다. 복잡도 표에는 드러나지 않지만 실제 속도에서는 이 차이가 몇 배씩 나기도 합니다.

vector의 push_back이 O(1)인 것은 “평균적으로”입니다. 용량이 다 차면 보통 1.5~2배 큰 공간을 새로 할당하고 기존 원소를 모두 옮기는데, 이 비용을 여러 번의 push_back에 나눠 보면 한 번당 상수가 됩니다. 원소 수를 미리 안다면 reserve()로 재할당을 없앨 수 있습니다. 재할당이 일어나면 기존 원소의 주소가 모두 바뀌므로, 원소를 가리키던 포인터·참조·반복자가 전부 무효화된다는 점은 vector를 쓸 때 가장 흔한 버그의 원인입니다. 순회하면서 push_back하는 코드가 가끔 크래시한다면 이것을 의심해 봐야 합니다.


연결 리스트 (Linked List)

노드들이 포인터로 연결된 구조입니다.

#include <list>
#include <iostream>
using namespace std;
int main() {
    list<int> lst = {1, 2, 3, 4, 5};
    
    // 앞에 삽입 O(1)
    lst.push_front(0);
    
    // 중간에 삽입 O(1) - 이터레이터가 있을 때
    auto it = next(lst.begin(), 2);
    lst.insert(it, 99);
    
    // 순회
    for (int val : lst) {
        cout << val << " ";
    }
}

장점:

  • 중간 삽입/삭제 빠름 (O(1))

  • 크기 제한 없음 단점:

  • 인덱스 접근 느림 (O(n))

  • 추가 메모리 필요 (포인터)

  • 캐시 비친화적 언제 사용?

  • 삽입/삭제가 빈번할 때

  • 크기를 예측할 수 없을 때

  • 순차 접근만 필요할 때

“삽입/삭제가 빈번하면 리스트”라는 기준은 이론적으로는 맞지만 실무에서는 생각보다 자주 틀립니다. 노드마다 별도로 할당되어 메모리 곳곳에 흩어지므로 순회할 때마다 캐시 미스가 나고, int 하나를 저장하는 데도 앞뒤 포인터 두 개(64비트에서 16바이트)와 할당 헤더가 붙어 메모리를 몇 배 씁니다. 그래서 원소가 수천 개 수준이라면 중간 삽입을 하더라도 vector가 list보다 빠른 경우가 흔하고, C++ 표준 위원회 구성원들도 “기본은 vector, 측정 결과가 다르게 말할 때만 다른 것”을 권합니다. 리스트가 확실히 유리한 경우는 원소가 매우 크거나 이동 비용이 비쌀 때, 삽입·삭제 후에도 다른 원소의 반복자와 주소가 유지되어야 할 때, splice로 리스트끼리 노드를 옮겨야 할 때입니다.


스택 (Stack)

LIFO (Last In, First Out) - 마지막에 들어간 것이 먼저 나옵니다.

#include <stack>
#include <iostream>
using namespace std;
int main() {
    stack<int> st;
    
    // 삽입
    st.push(1);
    st.push(2);
    st.push(3);
    
    // 제거 (역순)
    while (!st.empty()) {
        cout << st.top() << " ";  // 3 2 1
        st.pop();
    }
}

실전 활용:

  • 함수 호출 스택
  • 괄호 검사
  • 되돌리기 (Undo) 기능
  • DFS (깊이 우선 탐색) 예제: 괄호 검사
bool isValid(string s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '{' || c == '[') {
            st.push(c);
        } else {
            if (st.empty()) return false;
            char top = st.top();
            st.pop();
            if ((c == ')' && top != '(') ||
                (c == '}' && top != '{') ||
                (c == ']' && top != '[')) {
                return false;
            }
        }
    }
    return st.empty();
}

괄호 검사는 스택이 왜 필요한지 잘 보여 주는 문제입니다. 닫는 괄호는 가장 최근에 열린 괄호와 짝을 이뤄야 하므로 “마지막에 넣은 것부터 꺼내는” LIFO 구조가 정확히 맞습니다. 코드에서 놓치기 쉬운 경계 조건은 두 가지입니다. 닫는 괄호가 먼저 나오면 스택이 비어 있으므로 top()을 호출하기 전에 empty()를 확인해야 하고(빈 std::stack의 top()은 정의되지 않은 동작), 문자열을 다 읽은 뒤에도 스택에 여는 괄호가 남아 있으면 짝이 맞지 않은 것이므로 마지막에 st.empty()를 반환합니다. 이 함수는 괄호 외의 문자가 들어오면 닫는 괄호로 취급해 false를 반환하므로, 수식처럼 다른 문자가 섞인 입력을 다루려면 괄호가 아닌 문자는 건너뛰도록 분기를 추가해야 합니다.

std::stack은 독립된 자료구조가 아니라 다른 컨테이너(기본은 deque)를 감싸 push/pop/top만 노출하는 어댑터입니다. 인덱스 접근이나 순회를 일부러 막아 “LIFO로만 쓴다”는 의도를 코드에 드러내는 것이 목적이며, std::stack<int, std::vector<int>>처럼 내부 컨테이너를 바꿀 수도 있습니다. pop()이 값을 반환하지 않고 top()과 나뉘어 있는 것은 예외 안전성 때문입니다. 값을 반환하면서 제거하면, 반환값을 복사하다 예외가 났을 때 원소가 이미 사라진 상태가 되기 때문입니다.


큐 (Queue)

FIFO (First In, First Out) - 먼저 들어간 것이 먼저 나옵니다.

#include <queue>
#include <iostream>
using namespace std;
int main() {
    queue<int> q;
    
    // 삽입
    q.push(1);
    q.push(2);
    q.push(3);
    
    // 제거 (순서대로)
    while (!q.empty()) {
        cout << q.front() << " ";  // 1 2 3
        q.pop();
    }
}

실전 활용:

  • 작업 대기열
  • BFS (너비 우선 탐색)
  • 프린터 스풀러
  • 메시지 큐

큐를 vector로 직접 구현하면서 erase(begin())으로 앞에서 꺼내면 매번 모든 원소가 한 칸씩 이동해 O(n)이 됩니다. std::queue의 기본 내부 컨테이너인 std::deque는 양쪽 끝의 삽입·삭제가 O(1)이라 이 문제가 없습니다. 여러 스레드가 함께 쓰는 작업 대기열이라면 std::queue 자체는 스레드 안전하지 않으므로, 뮤텍스와 조건 변수로 감싸거나 전용 동시성 큐를 써야 합니다. BFS가 큐를 쓰는 이유는 시작점에서 가까운 정점부터 차례로 방문해야 하기 때문입니다. 먼저 발견한 정점을 먼저 처리하는 FIFO 순서가 “거리 순서”를 보장하므로, 가중치 없는 그래프에서 BFS로 찾은 경로가 최단 경로가 됩니다.


비선형 자료구조

트리 (Tree)

계층적 구조를 표현합니다.

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 이진 탐색 트리 삽입
TreeNode* insert(TreeNode* root, int val) {
    if (!root) return new TreeNode(val);
    
    if (val < root->val) {
        root->left = insert(root->left, val);
    } else {
        root->right = insert(root->right, val);
    }
    return root;
}
// 중위 순회 (정렬된 순서)
void inorder(TreeNode* root) {
    if (!root) return;
    inorder(root->left);
    cout << root->val << " ";
    inorder(root->right);
}

트리 종류:

  • 이진 트리: 자식이 최대 2개
  • 이진 탐색 트리 (BST): 왼쪽 < 부모 < 오른쪽
  • AVL 트리: 균형 잡힌 BST
  • 힙: 우선순위 큐 구현 언제 사용?
  • 계층 구조 표현 (파일 시스템, 조직도)
  • 빠른 검색/삽입/삭제 (균형 트리에서 O(log n))
  • 정렬된 데이터 유지

위의 insert는 가장 단순한 이진 탐색 트리라서 균형을 유지하지 않습니다. 1, 2, 3, 4, 5처럼 정렬된 순서로 넣으면 모든 노드가 오른쪽 자식으로만 이어져 사실상 연결 리스트가 되고, 검색·삽입이 O(n)으로 떨어집니다. 재귀 구현이라 노드가 수만 개인 한쪽으로 치우친 트리에서는 호출 깊이가 그만큼 깊어져 스택 오버플로로 프로그램이 죽을 수도 있습니다. 실제 데이터는 정렬된 채로 들어오는 경우가 많아서(시간순 ID, 이미 정렬된 파일) 이 문제는 이론이 아니라 실무에서 자주 나타납니다. 그래서 표준 라이브러리의 std::map과 std::set은 레드-블랙 트리처럼 삽입할 때마다 회전으로 높이를 맞추는 균형 이진 탐색 트리로 구현되어 최악의 경우에도 O(log n)을 보장합니다. 직접 트리를 만들 일이 있다면 new로 만든 노드를 해제하는 코드도 함께 필요합니다. 예제는 delete가 없어 누수가 나므로, std::unique_ptr<TreeNode>로 자식을 소유하게 하면 트리가 파괴될 때 자동으로 정리됩니다.


그래프 (Graph)

노드(정점)와 간선으로 관계를 표현합니다.

#include <vector>
#include <queue>
using namespace std;
// 인접 리스트 표현
class Graph {
    int V;  // 정점 개수
    vector<vector<int>> adj;
    
public:
    Graph(int V) : V(V), adj(V) {}
    
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);  // 무방향 그래프
    }
    
    // BFS
    void BFS(int start) {
        vector<bool> visited(V, false);
        queue<int> q;
        
        visited[start] = true;
        q.push(start);
        
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            cout << u << " ";
            
            for (int v : adj[u]) {
                if (!visited[v]) {
                    visited[v] = true;
                    q.push(v);
                }
            }
        }
    }
};

실전 활용:

  • 소셜 네트워크 (친구 관계)
  • 지도/내비게이션 (최단 경로)
  • 웹 크롤링 (링크 구조)
  • 의존성 관리

그래프를 저장하는 방법은 크게 인접 리스트와 인접 행렬 두 가지입니다. 예제의 인접 리스트(vector<vector<int>>)는 정점마다 이웃 목록을 저장하므로 메모리가 O(V + E)이고, 한 정점의 이웃을 순회하는 데 이웃 수만큼만 걸립니다. 인접 행렬(V × V 배열)은 두 정점이 연결되어 있는지 O(1)로 확인할 수 있지만 메모리가 O(V²)이라 정점이 수만 개만 되어도 수 GB가 필요합니다. 소셜 네트워크나 도로망처럼 정점에 비해 간선이 적은 희소 그래프가 대부분이라 실무에서는 인접 리스트가 기본입니다. BFS 코드에서 visited를 큐에 넣을 때 표시하는 점도 중요합니다. 꺼낼 때 표시하면 같은 정점이 여러 경로로 큐에 중복으로 들어가 처리량이 크게 늘어납니다.


해시 테이블 (Hash Table)

키-값 쌍을 빠르게 저장/검색합니다.

#include <unordered_map>
#include <iostream>
using namespace std;
int main() {
    unordered_map<string, int> ages;
    
    // 삽입 O(1)
    ages["Alice"] = 25;
    ages["Bob"] = 30;
    
    // 검색 O(1)
    cout << ages["Alice"] << endl;  // 25
    
    // 존재 확인
    if (ages.find("Charlie") == ages.end()) {
        cout << "Not found" << endl;
    }
}

실전 활용:

  • 캐싱
  • 중복 제거
  • 빈도수 계산
  • 데이터베이스 인덱스

해시 테이블은 키를 해시 함수로 숫자로 바꾼 뒤 그 숫자로 버킷 위치를 계산해 저장합니다. 위치를 바로 계산하므로 평균 O(1)이지만, 서로 다른 키가 같은 버킷에 몰리면(충돌) 그 버킷 안에서 순차 비교를 해야 해서 최악의 경우 O(n)이 됩니다. 원소가 늘어 버킷당 평균 원소 수(load factor)가 기준을 넘으면 버킷 배열을 키우고 모든 원소를 다시 배치하는 재해싱이 일어나는데, 이때 한 번의 삽입이 O(n) 시간을 쓰므로 지연 시간에 민감한 코드라면 reserve()로 미리 크기를 잡아 두는 것이 좋습니다.

예제에서 조심해야 할 부분은 ages["Alice"]처럼 operator[]로 읽는 방식입니다. operator[]는 키가 없으면 기본값(0)으로 새 원소를 만들어 넣고 그 값을 반환합니다. cout << ages["Charlie"]처럼 확인 없이 읽으면 에러 없이 0이 출력되면서 맵에 “Charlie”가 추가됩니다. 존재 여부 확인은 find나 C++20의 contains로, 읽기 전용 조회는 at()(없으면 예외)이나 find 결과로 하는 것이 안전합니다. 또 unordered_map은 순서를 보장하지 않으므로, 출력 순서가 중요한 곳에서 쓰면 실행 환경에 따라 결과 순서가 달라집니다.


시간복잡도 비교

자료구조접근검색삽입삭제
배열O(1)O(n)O(n)O(n)
연결 리스트O(n)O(n)O(1)*O(1)*
스택O(n)O(n)O(1)O(1)
큐O(n)O(n)O(1)O(1)
이진 탐색 트리 (균형)O(log n)O(log n)O(log n)O(log n)
이진 탐색 트리 (비균형 최악)O(n)O(n)O(n)O(n)
해시 테이블-O(1) 평균 / O(n) 최악O(1) 평균O(1) 평균

*이터레이터가 있을 때

이 표를 읽을 때 기억할 점은 두 가지입니다. 첫째, 빅오 표기는 데이터가 커질 때의 증가 추세만 말해 주고 상수 비용은 감춥니다. O(1)인 해시 조회도 해시 계산과 캐시 미스 때문에 수십 나노초가 걸리는 반면, 원소가 수십 개뿐인 정렬된 배열의 이진 탐색이나 선형 탐색이 더 빠른 경우가 흔합니다. 둘째, 평균과 최악은 다릅니다. 해시 테이블과 비균형 트리는 입력 분포에 따라 최악으로 떨어질 수 있고, 외부 입력을 키로 쓰는 서버라면 악의적으로 충돌을 유도하는 공격(Hash DoS)도 실제로 보고된 적이 있습니다. 최악 보장이 필요하면 균형 트리(std::map)가, 평균 성능이 중요하면 해시(std::unordered_map)가 기본 선택입니다.


실전 선택 가이드

시나리오별 추천

1. 순차 접근만 필요

vector<int> data;  // 배열이 최선

2. 빈번한 삽입/삭제

list<int> data;  // 연결 리스트

3. 최근 항목 우선

stack<int> history;  // 스택 (Undo 기능)

4. 먼저 온 순서대로

queue<Task> tasks;  // 큐 (작업 대기열)

5. 우선순위 처리

priority_queue<int> pq;  // 힙

6. 빠른 검색

unordered_set<int> seen;  // 해시 테이블

7. 정렬 유지 + 빠른 검색

set<int> sorted_data;  // 이진 탐색 트리

시나리오 2의 “빈번한 삽입/삭제 → 연결 리스트”는 앞에서 설명한 대로 삽입 위치를 이미 알고 있을 때만 성립합니다. 위치를 찾는 데 탐색이 필요하다면 vector가 대개 더 빠르고, 삭제 순서가 상관없다면 지울 원소를 마지막 원소와 바꾼 뒤 pop_back()하는 방식으로 vector에서도 O(1) 삭제가 가능합니다. 시나리오 5의 priority_queue는 트리 모양의 힙을 배열 위에 구현한 것으로, 가장 큰 값 확인은 O(1), 삽입과 꺼내기는 O(log n)입니다. 기본은 최대 힙이라 작은 값부터 꺼내려면 priority_queue<int, vector<int>, greater<int>>로 선언해야 하는데, 이 차이를 모르고 다익스트라 알고리즘을 구현해 틀린 답을 내는 경우가 흔합니다.


실전 예제: 최근 방문 페이지

#include <iostream>
#include <deque>
#include <string>
using namespace std;
class BrowserHistory {
    deque<string> history;
    int current = -1;
    
public:
    void visit(string url) {
        // 현재 위치 이후 제거
        while (history.size() > current + 1) {
            history.pop_back();
        }
        history.push_back(url);
        current++;
    }
    
    string back() {
        if (current > 0) current--;
        return history[current];
    }
    
    string forward() {
        if (current < history.size() - 1) current++;
        return history[current];
    }
};
int main() {
    BrowserHistory browser;
    browser.visit("google.com");
    browser.visit("youtube.com");
    browser.visit("facebook.com");
    
    cout << browser.back() << endl;     // youtube.com
    cout << browser.back() << endl;     // google.com
    cout << browser.forward() << endl;  // youtube.com
}

요구사항부터 자료구조를 고르는 과정을 이 예제로 정리해 보면 이렇습니다. 브라우저 방문 기록에는 세 가지 연산이 필요합니다. 새 페이지 방문(현재 위치 뒤의 “앞으로” 기록은 버리고 끝에 추가), 뒤로 가기(현재 위치를 하나 앞으로), 앞으로 가기(하나 뒤로)입니다. 뒤로·앞으로는 현재 위치 인덱스만 옮기면 되므로 인덱스 접근이 O(1)인 구조가 필요하고, 방문할 때는 끝에서 여러 개를 지우고 추가해야 합니다. 이 두 조건을 모두 O(1)로 만족하는 것이 deque(또는 vector)입니다. 같은 문제를 “뒤로 가기용 스택”과 “앞으로 가기용 스택” 두 개로 푸는 방법도 있는데, 새 페이지를 방문할 때 앞으로 스택을 비우기만 하면 되어 논리가 더 단순하다는 장점이 있습니다. 한쪽 끝에서만 넣고 빼는 기록이라면 스택 두 개, 위치를 임의로 옮겨야 한다면(예: 기록 목록에서 5단계 전으로 바로 이동) 인덱스가 있는 배열이 맞습니다.

예제 코드에는 경계 조건 버그가 있습니다. 아무 페이지도 방문하지 않은 상태에서 back()을 부르면 current가 -1인 채로 history[-1]에 접근하고, forward()의 history.size() - 1은 빈 deque에서 부호 없는 정수 언더플로로 아주 큰 값이 됩니다. 또 int인 current와 size_t인 size()를 비교하면 current가 부호 없는 값으로 변환되어, 음수일 때 비교 결과가 뒤집힙니다. 실제 코드에서는 current를 size_t로 두고 빈 상태를 따로 처리하거나, 기록이 없을 때 std::optional<string>을 반환하도록 바꾸는 것이 안전합니다. 실제 브라우저처럼 기록 개수에 상한을 두려면 오래된 기록을 pop_front()로 버리면 되는데, 이때 current도 함께 하나 줄여야 한다는 점이 deque를 고른 또 하나의 이유입니다.


마무리

데이터 구조 선택 체크리스트:

  1. 어떤 연산이 가장 빈번한가?
    • 접근: 배열
    • 위치를 이미 아는 삽입/삭제: 리스트
    • 검색: 해시 테이블
  2. 데이터 크기는?
    • 작음: 배열 (캐시 효율)
    • 큼: 트리/해시
  3. 순서가 중요한가?
    • 삽입 순서: 큐
    • 역순: 스택
    • 정렬: 트리/힙
  4. 메모리 제약은?
    • 제한적: 배열
    • 여유: 트리/그래프

이 체크리스트에서 가장 중요한 것은 1번입니다. 자료구조 선택은 “데이터가 무엇인가”보다 “그 데이터로 무엇을 가장 자주 하는가”로 결정됩니다. 같은 사용자 목록이라도 ID로 조회하는 것이 대부분이면 해시 테이블, 가입일 순으로 범위 조회를 한다면 정렬된 트리, 처음부터 끝까지 한 번씩 훑는다면 배열이 맞습니다. 여러 연산이 모두 빨라야 한다면 자료구조를 조합합니다. LRU 캐시가 해시 테이블(키로 O(1) 조회)과 연결 리스트(사용 순서를 O(1)로 갱신)를 함께 쓰는 것이 대표적인 예입니다. 확신이 서지 않을 때는 vector로 먼저 만들고, 실제 데이터로 측정해 병목이 보일 때 바꾸는 순서가 가장 실용적입니다.


자주 묻는 질문

Q: 자료구조와 알고리즘의 차이는? A: 자료구조는 “데이터를 어떻게 저장할까”, 알고리즘은 “데이터를 어떻게 처리할까”입니다. 둘은 밀접하게 연관되어 있습니다.

Q: 실무에서 가장 많이 쓰는 자료구조는? A: 배열(vector), 해시 테이블(unordered_map), 큐가 가장 빈번합니다.

Q: 자료구조를 직접 구현해야 하나요? A: 실무에서는 STL/표준 라이브러리를 사용합니다. 하지만 면접과 이해를 위해 직접 구현해보는 것이 중요합니다.

Q: 어떤 자료구조부터 공부해야 하나요? A: 배열 → 리스트 → 스택/큐 → 트리 → 그래프 순서를 추천합니다.


같이 보면 좋은 글