기본 자료구조 마스터하기 - 배열, 연결리스트, 스택, 큐
📌 학습 목표
- 기본 자료구조 4개 완전 이해 및 구현
- 각 자료구조의 장단점과 활용 사례 파악
- 시간/공간 복잡도 분석 능력 향상
📝 개념 정리
1. 배열 (Array)
연속된 메모리에 동일한 타입의 데이터를 저장
특징
- 인덱스 기반 접근: O(1)
- 크기 고정: 선언 시 크기 결정
- 삽입/삭제 비용 큼: O(n)
1
2
3
4
5
6
7
8
9
// C++ 배열 예제
int arr[5] = {1, 2, 3, 4, 5};
arr[2] = 10; // O(1) 접근
// 중간 삽입은 비효율적
for(int i = 4; i > 2; i--) {
arr[i] = arr[i-1]; // 요소들을 한 칸씩 이동
}
arr[2] = 99;
장점 vs 단점
| 장점 | 단점 | |——|——| | 빠른 접근 속도 | 크기 고정 | | 캐시 친화적 | 삽입/삭제 비용 | | 메모리 효율적 | 타입 제한 |
2. 연결리스트 (Linked List)
노드(값 + 포인터)들이 연결된 구조
특징
- 동적 크기: 런타임에 크기 변경 가능
- 삽입/삭제: O(1) (위치를 안다면)
- 순차 접근: 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
// 단일 연결리스트 구현
struct Node {
int data;
Node* next;
Node(int val) : data(val), next(nullptr) {}
};
class LinkedList {
private:
Node* head;
public:
LinkedList() : head(nullptr) {}
// 앞쪽에 삽입 O(1)
void insertFront(int val) {
Node* newNode = new Node(val);
newNode->next = head;
head = newNode;
}
// 특정 값 삭제 O(n)
void remove(int val) {
if (!head) return;
if (head->data == val) {
Node* temp = head;
head = head->next;
delete temp;
return;
}
Node* current = head;
while (current->next && current->next->data != val) {
current = current->next;
}
if (current->next) {
Node* temp = current->next;
current->next = current->next->next;
delete temp;
}
}
};
3. 스택 (Stack)
LIFO (Last In, First Out) 구조
핵심 연산
-
push()
: 최상단에 요소 추가 -
pop()
: 최상단 요소 제거 -
top()
: 최상단 요소 확인
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
#include <stack>
#include <iostream>
// STL 스택 사용
void stackExample() {
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
while (!s.empty()) {
std::cout << s.top() << " "; // 3 2 1
s.pop();
}
}
// 배열로 스택 구현
class ArrayStack {
private:
int* arr;
int capacity;
int topIndex;
public:
ArrayStack(int size) : capacity(size), topIndex(-1) {
arr = new int[capacity];
}
~ArrayStack() { delete[] arr; }
void push(int val) {
if (topIndex < capacity - 1) {
arr[++topIndex] = val;
}
}
int pop() {
if (topIndex >= 0) {
return arr[topIndex--];
}
return -1; // 에러 처리 필요
}
bool isEmpty() { return topIndex == -1; }
};
4. 큐 (Queue)
FIFO (First In, First Out) 구조
핵심 연산
-
enqueue()
: 뒤쪽에 요소 추가 -
dequeue()
: 앞쪽 요소 제거 -
front()
: 앞쪽 요소 확인
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
#include <queue>
// STL 큐 사용
void queueExample() {
std::queue<int> q;
q.push(1); // enqueue
q.push(2);
q.push(3);
while (!q.empty()) {
std::cout << q.front() << " "; // 1 2 3
q.pop(); // dequeue
}
}
// 원형 큐 구현
class CircularQueue {
private:
int* arr;
int capacity;
int front, rear, size;
public:
CircularQueue(int cap) : capacity(cap), front(0), rear(-1), size(0) {
arr = new int[capacity];
}
~CircularQueue() { delete[] arr; }
void enqueue(int val) {
if (size < capacity) {
rear = (rear + 1) % capacity;
arr[rear] = val;
size++;
}
}
int dequeue() {
if (size > 0) {
int val = arr[front];
front = (front + 1) % capacity;
size--;
return val;
}
return -1;
}
bool isEmpty() { return size == 0; }
bool isFull() { return size == capacity; }
};
🎯 실전 연습 문제
1. 연결리스트 뒤집기
1
2
3
4
5
6
7
8
9
10
11
12
13
Node* reverseLinkedList(Node* head) {
Node* prev = nullptr;
Node* current = head;
while (current) {
Node* next = current->next;
current->next = prev;
prev = current;
current = next;
}
return prev;
}
2. 스택으로 괄호 유효성 검사
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool isValidParentheses(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();
}
3. 큐로 BFS 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void bfs(vector<vector<int>>& graph, int start) {
vector<bool> visited(graph.size(), false);
queue<int> q;
q.push(start);
visited[start] = true;
while (!q.empty()) {
int node = q.front();
q.pop();
cout << node << " ";
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
q.push(neighbor);
}
}
}
}
📊 복잡도 비교표
자료구조 | 접근 | 검색 | 삽입 | 삭제 | 공간복잡도 |
---|---|---|---|---|---|
배열 | O(1) | O(n) | O(n) | O(n) | O(n) |
연결리스트 | O(n) | O(n) | O(1)* | O(1)* | O(n) |
스택 | O(n) | O(n) | O(1) | O(1) | O(n) |
큐 | O(n) | O(n) | O(1) | O(1) | O(n) |
*위치를 아는 경우
🔗 관련 링크
문제 풀이 사이트
시각화 도구
🔎 심화 학습 주제
고급 변형들
-
동적 배열:
std::vector
,std::deque
- 이중 연결 리스트: 양방향 탐색 가능
- 원형 큐: 메모리 효율적 활용
- 덱(Deque): 양쪽 끝에서 삽입/삭제 가능
메모리 관점
- 배열 vs 연결리스트: 캐시 친화성 차이
- 메모리 지역성: 배열이 더 유리한 이유
- 메모리 단편화: 연결리스트의 단점
💡 실무 적용 팁
-
적절한 자료구조 선택
- 빈번한 접근 → 배열
- 빈번한 삽입/삭제 → 연결리스트
- 후입선출 → 스택
- 선입선출 → 큐
-
STL 적극 활용
-
std::vector
,std::list
,std::stack
,std::queue
- 직접 구현보다는 검증된 라이브러리 사용
-
-
성능 측정
- 이론적 복잡도와 실제 성능은 다를 수 있음
- 프로파일링으로 병목지점 확인
🪞 회고 질문
- 각 자료구조의 장단점을 실제 예시와 함께 설명할 수 있는가?
- 언제 어떤 자료구조를 선택해야 하는지 판단 기준이 있는가?
- 최근 프로젝트에서 어떤 자료구조를 주로 사용했고, 그 이유는?
- 메모리 제약이 있는 환경에서는 어떤 자료구조가 더 적합할까?
🚀 다음 학습 주제
다음으로는 고급 자료구조인 해시맵, 셋, 트리, 힙에 대해 알아보겠습니다. 더 복잡하지만 강력한 도구들을 만나보세요!
This post is licensed under CC BY 4.0 by the author.