가장 기본적인 두 자료구조를 마스터합시다! 배열과 연결 리스트의 차이를 이해하면 다른 모든 자료구조가 쉬워집니다.
🎯 이 글을 읽고 나면
- 배열과 연결 리스트의 장단점을 비교할 수 있습니다
- 시간 복잡도를 기준으로 적절한 자료구조를 선택할 수 있습니다
- 동적 배열의 동작 원리를 이해합니다
- 이중 연결 리스트를 직접 구현할 수 있습니다
- 실무에서 어떤 상황에 무엇을 쓸지 판단할 수 있습니다
📚 사전 지식
- 프로그래밍 기초: 배열, 포인터/참조 개념
- 메모리 구조: 컴퓨터 구조 편 참고
- 복잡도 분석: 시간 복잡도 개념 (O(1), O(n) 등)
💡 핵심 개념 미리보기
배열(Array)은 연속된 메모리 공간에 데이터를 저장하여 인덱스로 O(1) 접근이 가능하지만, 중간 삽입/삭제가 비효율적(O(n))입니다. 연결 리스트(Linked List)는 노드들이 포인터로 연결되어 삽입/삭제가 효율적(O(1))이지만, 특정 위치 접근에 O(n)이 필요합니다. 각각의 장단점을 이해하면 상황에 맞는 자료구조를 선택할 수 있습니다!
📚 들어가며: 자료구조는 왜 중요한가?
“프로그램 = 자료구조 + 알고리즘”이라는 유명한 공식이 있습니다. 적절한 자료구조를 선택하면 복잡한 문제도 간단하게 해결할 수 있습니다.
오늘은 가장 기본이 되는 두 자료구조, 배열(Array)과 연결 리스트(Linked List)를 깊이 있게 알아보겠습니다. 이 둘의 차이를 이해하면 다른 모든 자료구조를 쉽게 이해할 수 있습니다!
🎯 자료구조란?
자료구조는 데이터를 효율적으로 저장하고 관리하는 방법입니다.
graph TD
DS[자료구조]
DS --> Linear[선형 구조]
DS --> NonLinear[비선형 구조]
Linear --> Array[배열]
Linear --> LinkedList[연결 리스트]
Linear --> Stack[스택]
Linear --> Queue[큐]
NonLinear --> Tree[트리]
NonLinear --> Graph[그래프]
NonLinear --> Hash[해시 테이블]
style Array fill:#ff9999
style LinkedList fill:#ff9999
📦 배열 (Array)
배열은 연속된 메모리 공간에 데이터를 순차적으로 저장하는 자료구조입니다.
배열의 메모리 구조
graph LR
subgraph "메모리 (연속 할당)"
M0[0x100<br/>10] --> M1[0x104<br/>20]
M1 --> M2[0x108<br/>30]
M2 --> M3[0x10C<br/>40]
M3 --> M4[0x110<br/>50]
end
subgraph "배열 인덱스"
I0[arr[0]] -.-> M0
I1[arr[1]] -.-> M1
I2[arr[2]] -.-> M2
I3[arr[3]] -.-> M3
I4[arr[4]] -.-> M4
end
배열 구현
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
80
81
| class Array:
"""고정 크기 배열 구현"""
def __init__(self, capacity):
self.capacity = capacity
self.data = [None] * capacity
self.size = 0
def __getitem__(self, index):
"""인덱스로 접근 - O(1)"""
if 0 <= index < self.size:
return self.data[index]
raise IndexError("Index out of range")
def __setitem__(self, index, value):
"""인덱스로 값 설정 - O(1)"""
if 0 <= index < self.size:
self.data[index] = value
else:
raise IndexError("Index out of range")
def append(self, value):
"""끝에 추가 - O(1)"""
if self.size >= self.capacity:
raise OverflowError("Array is full")
self.data[self.size] = value
self.size += 1
def insert(self, index, value):
"""중간에 삽입 - O(n)"""
if self.size >= self.capacity:
raise OverflowError("Array is full")
if index < 0 or index > self.size:
raise IndexError("Index out of range")
# 뒤로 한 칸씩 이동
for i in range(self.size, index, -1):
self.data[i] = self.data[i - 1]
self.data[index] = value
self.size += 1
def remove(self, index):
"""삭제 - O(n)"""
if index < 0 or index >= self.size:
raise IndexError("Index out of range")
value = self.data[index]
# 앞으로 한 칸씩 이동
for i in range(index, self.size - 1):
self.data[i] = self.data[i + 1]
self.size -= 1
self.data[self.size] = None
return value
def search(self, value):
"""선형 검색 - O(n)"""
for i in range(self.size):
if self.data[i] == value:
return i
return -1
def __str__(self):
return str([self.data[i] for i in range(self.size)])
# 배열 사용 예시
arr = Array(10)
arr.append(10)
arr.append(20)
arr.append(30)
print(f"배열: {arr}")
arr.insert(1, 15)
print(f"1번 인덱스에 15 삽입: {arr}")
arr.remove(2)
print(f"2번 인덱스 삭제: {arr}")
print(f"값 30의 위치: {arr.search(30)}")
|
동적 배열 (Dynamic Array)
Python의 list처럼 크기가 자동으로 조절되는 배열입니다.
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
| class DynamicArray:
"""동적 배열 구현 (Python list와 유사)"""
def __init__(self):
self.capacity = 1
self.size = 0
self.data = [None] * self.capacity
def _resize(self, new_capacity):
"""배열 크기 조정 - O(n)"""
new_data = [None] * new_capacity
for i in range(self.size):
new_data[i] = self.data[i]
self.data = new_data
self.capacity = new_capacity
print(f"크기 조정: {self.capacity // 2} → {self.capacity}")
def append(self, value):
"""끝에 추가 - Amortized O(1)"""
if self.size == self.capacity:
self._resize(self.capacity * 2) # 2배로 확장
self.data[self.size] = value
self.size += 1
def pop(self):
"""끝에서 제거 - O(1)"""
if self.size == 0:
raise IndexError("Array is empty")
value = self.data[self.size - 1]
self.size -= 1
# 25% 이하로 줄면 크기 절반으로 축소
if self.size > 0 and self.size == self.capacity // 4:
self._resize(self.capacity // 2)
return value
def __len__(self):
return self.size
def __str__(self):
return str([self.data[i] for i in range(self.size)])
# 동적 배열 테스트
dynamic_arr = DynamicArray()
print("동적 배열 테스트:")
for i in range(10):
dynamic_arr.append(i * 10)
print(f"추가 {i * 10}: 크기={len(dynamic_arr)}, 용량={dynamic_arr.capacity}")
|
🔗 연결 리스트 (Linked List)
연결 리스트는 노드(Node)들이 포인터로 연결된 자료구조입니다.
연결 리스트의 메모리 구조
graph LR
subgraph "메모리 (분산 할당)"
N1[0x200<br/>Data: 10<br/>Next: 0x350]
N2[0x350<br/>Data: 20<br/>Next: 0x180]
N3[0x180<br/>Data: 30<br/>Next: 0x420]
N4[0x420<br/>Data: 40<br/>Next: NULL]
end
HEAD[HEAD] --> N1
N1 --> N2
N2 --> N3
N3 --> N4
N4 --> NULL[NULL]
단일 연결 리스트 구현
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
| class Node:
"""연결 리스트의 노드"""
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
"""단일 연결 리스트"""
def __init__(self):
self.head = None
self.size = 0
def prepend(self, data):
"""맨 앞에 추가 - O(1)"""
new_node = Node(data)
new_node.next = self.head
self.head = new_node
self.size += 1
def append(self, data):
"""맨 뒤에 추가 - O(n)"""
new_node = Node(data)
self.size += 1
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def insert(self, index, data):
"""특정 위치에 삽입 - O(n)"""
if index < 0 or index > self.size:
raise IndexError("Index out of range")
if index == 0:
self.prepend(data)
return
new_node = Node(data)
current = self.head
for _ in range(index - 1):
current = current.next
new_node.next = current.next
current.next = new_node
self.size += 1
def remove(self, data):
"""값으로 삭제 - O(n)"""
if not self.head:
return False
if self.head.data == data:
self.head = self.head.next
self.size -= 1
return True
current = self.head
while current.next:
if current.next.data == data:
current.next = current.next.next
self.size -= 1
return True
current = current.next
return False
def search(self, data):
"""검색 - O(n)"""
current = self.head
index = 0
while current:
if current.data == data:
return index
current = current.next
index += 1
return -1
def get(self, index):
"""인덱스로 접근 - O(n)"""
if index < 0 or index >= self.size:
raise IndexError("Index out of range")
current = self.head
for _ in range(index):
current = current.next
return current.data
def reverse(self):
"""리스트 뒤집기 - O(n)"""
prev = None
current = self.head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
self.head = prev
def __str__(self):
if not self.head:
return "[]"
result = []
current = self.head
while current:
result.append(str(current.data))
current = current.next
return " -> ".join(result)
# 연결 리스트 사용 예시
ll = LinkedList()
ll.append(10)
ll.append(20)
ll.append(30)
print(f"연결 리스트: {ll}")
ll.prepend(5)
print(f"맨 앞에 5 추가: {ll}")
ll.insert(2, 15)
print(f"2번 위치에 15 삽입: {ll}")
ll.remove(20)
print(f"20 삭제: {ll}")
ll.reverse()
print(f"뒤집기: {ll}")
|
이중 연결 리스트 구현
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
| class DNode:
"""이중 연결 리스트의 노드"""
def __init__(self, data):
self.data = data
self.prev = None
self.next = None
class DoublyLinkedList:
"""이중 연결 리스트"""
def __init__(self):
self.head = None
self.tail = None
self.size = 0
def append(self, data):
"""맨 뒤에 추가 - O(1)"""
new_node = DNode(data)
if not self.head:
self.head = self.tail = new_node
else:
new_node.prev = self.tail
self.tail.next = new_node
self.tail = new_node
self.size += 1
def prepend(self, data):
"""맨 앞에 추가 - O(1)"""
new_node = DNode(data)
if not self.head:
self.head = self.tail = new_node
else:
new_node.next = self.head
self.head.prev = new_node
self.head = new_node
self.size += 1
def remove_at(self, index):
"""인덱스로 삭제 - O(n)"""
if index < 0 or index >= self.size:
raise IndexError("Index out of range")
# 앞쪽에서 접근이 빠른지 뒤쪽에서 접근이 빠른지 판단
if index < self.size // 2:
current = self.head
for _ in range(index):
current = current.next
else:
current = self.tail
for _ in range(self.size - 1 - index):
current = current.prev
# 노드 제거
if current.prev:
current.prev.next = current.next
else:
self.head = current.next
if current.next:
current.next.prev = current.prev
else:
self.tail = current.prev
self.size -= 1
return current.data
def __str__(self):
if not self.head:
return "[]"
result = []
current = self.head
while current:
result.append(str(current.data))
current = current.next
return " ⟷ ".join(result)
def reverse_str(self):
"""역방향 출력"""
if not self.tail:
return "[]"
result = []
current = self.tail
while current:
result.append(str(current.data))
current = current.prev
return " ⟷ ".join(result)
# 이중 연결 리스트 테스트
dll = DoublyLinkedList()
dll.append(10)
dll.append(20)
dll.append(30)
dll.prepend(5)
print(f"이중 연결 리스트: {dll}")
print(f"역방향: {dll.reverse_str()}")
dll.remove_at(2)
print(f"인덱스 2 삭제 후: {dll}")
|
⚖️ 배열 vs 연결 리스트 비교
시간 복잡도 비교
| 연산 | 배열 | 연결 리스트 |
| 접근 (Access) | O(1) | O(n) |
| 검색 (Search) | O(n) | O(n) |
| 삽입 (맨 앞) | O(n) | O(1) |
| 삽입 (맨 뒤) | O(1)* | O(n)** |
| 삽입 (중간) | O(n) | O(n) |
| 삭제 (맨 앞) | O(n) | O(1) |
| 삭제 (맨 뒤) | O(1) | O(n)** |
| 삭제 (중간) | O(n) | O(n) |
*동적 배열의 경우 Amortized O(1) **이중 연결 리스트에서 tail 포인터가 있으면 O(1)
공간 복잡도 비교
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
| import sys
def memory_comparison():
"""메모리 사용량 비교"""
# 배열 (Python list)
arr = [i for i in range(1000)]
arr_memory = sys.getsizeof(arr)
# 연결 리스트
ll = LinkedList()
for i in range(1000):
ll.append(i)
# 노드 하나의 크기 추정
node = Node(0)
node_size = sys.getsizeof(node) + sys.getsizeof(node.data) + sys.getsizeof(node.next)
ll_memory = node_size * 1000
print("1000개 정수 저장 시 메모리 사용량:")
print(f"배열: {arr_memory:,} bytes")
print(f"연결 리스트: {ll_memory:,} bytes (추정)")
print(f"오버헤드: {(ll_memory / arr_memory - 1) * 100:.1f}%")
memory_comparison()
|
🎯 언제 무엇을 사용해야 할까?
배열을 사용해야 할 때
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
| class ArrayUseCases:
"""배열이 적합한 경우"""
@staticmethod
def random_access_example():
"""빈번한 인덱스 접근이 필요한 경우"""
# 이미지 픽셀 처리
image = [[0] * 100 for _ in range(100)]
# O(1) 접근으로 특정 픽셀 수정
image[50][50] = 255
# 행렬 연산
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
# O(1) 접근으로 대각선 합
diagonal_sum = sum(matrix[i][i] for i in range(3))
print(f"대각선 합: {diagonal_sum}")
@staticmethod
def cache_friendly_example():
"""캐시 지역성이 중요한 경우"""
# 연속된 메모리 접근으로 캐시 효율 극대화
data = list(range(1000000))
# 순차 접근 - 캐시 친화적
total = 0
for value in data:
total += value
return total
@staticmethod
def binary_search_example():
"""이진 탐색이 필요한 경우"""
sorted_array = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
index = binary_search(sorted_array, 11)
print(f"11의 위치: {index}")
|
연결 리스트를 사용해야 할 때
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
| class LinkedListUseCases:
"""연결 리스트가 적합한 경우"""
@staticmethod
def frequent_insertion_deletion():
"""빈번한 삽입/삭제가 발생하는 경우"""
# LRU 캐시 구현
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = DoublyLinkedList()
self.map = {}
def get(self, key):
if key in self.map:
# 노드를 맨 앞으로 이동 (최근 사용)
# 실제로는 노드 참조를 저장하여 O(1)로 구현
return self.map[key]
return -1
def put(self, key, value):
if len(self.map) >= self.capacity:
# 가장 오래된 항목 제거 (tail)
pass
# 새 항목을 맨 앞에 추가 (head)
self.cache.prepend(value)
self.map[key] = value
@staticmethod
def undo_redo_system():
"""실행 취소/재실행 시스템"""
class History:
def __init__(self):
self.history = DoublyLinkedList()
self.current = None
def add_action(self, action):
"""새 작업 추가"""
self.history.append(action)
def undo(self):
"""이전 상태로"""
# 이중 연결 리스트로 쉽게 구현
pass
def redo(self):
"""다음 상태로"""
# 이중 연결 리스트로 쉽게 구현
pass
@staticmethod
def music_playlist():
"""음악 재생 목록"""
class Playlist:
def __init__(self):
self.songs = DoublyLinkedList()
self.current = None
def add_song(self, song):
self.songs.append(song)
def next_song(self):
# 다음 곡으로
if self.current and self.current.next:
self.current = self.current.next
return self.current.data
def prev_song(self):
# 이전 곡으로
if self.current and self.current.prev:
self.current = self.current.prev
return self.current.data
|
🔄 순환 연결 리스트
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
| class CircularLinkedList:
"""순환 연결 리스트"""
def __init__(self):
self.head = None
self.size = 0
def append(self, data):
"""추가 - O(n)"""
new_node = Node(data)
if not self.head:
self.head = new_node
new_node.next = self.head # 자기 자신을 가리킴
else:
current = self.head
while current.next != self.head:
current = current.next
current.next = new_node
new_node.next = self.head
self.size += 1
def rotate(self, k):
"""k만큼 회전 - O(k)"""
if not self.head or k == 0:
return
for _ in range(k % self.size):
current = self.head
while current.next != self.head:
current = current.next
# head를 다음 노드로 이동
self.head = self.head.next
def josephus_problem(self, k):
"""요세푸스 문제 해결"""
if not self.head:
return None
current = self.head
while self.size > 1:
# k-1번 이동
for _ in range(k - 1):
prev = current
current = current.next
# current 노드 제거
print(f"제거: {current.data}")
prev.next = current.next
current = current.next
self.size -= 1
return current.data
# 순환 연결 리스트 활용 예제
print("라운드 로빈 스케줄링 시뮬레이션:")
scheduler = CircularLinkedList()
for i in range(1, 6):
scheduler.append(f"Process {i}")
# 요세푸스 문제
print("\n요세푸스 문제 (k=3):")
josephus = CircularLinkedList()
for i in range(1, 8):
josephus.append(i)
survivor = josephus.josephus_problem(3)
print(f"생존자: {survivor}")
|
💡 실전 문제 풀이
문제 1: 두 정렬된 연결 리스트 병합
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| def merge_sorted_lists(l1, l2):
"""두 정렬된 연결 리스트를 병합"""
dummy = Node(0)
current = dummy
while l1 and l2:
if l1.data <= l2.data:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
# 남은 노드 연결
current.next = l1 if l1 else l2
return dummy.next
|
문제 2: 연결 리스트의 사이클 감지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| def has_cycle(head):
"""플로이드의 토끼와 거북이 알고리즘"""
if not head or not head.next:
return False
slow = head
fast = head.next
while fast and fast.next:
if slow == fast:
return True
slow = slow.next
fast = fast.next.next
return False
|
문제 3: 배열에서 k번째 큰 수 찾기
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
| def find_kth_largest(arr, k):
"""Quick Select 알고리즘 - 평균 O(n)"""
def partition(left, right):
pivot = arr[right]
i = left - 1
for j in range(left, right):
if arr[j] >= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[right] = arr[right], arr[i + 1]
return i + 1
def quick_select(left, right, k):
if left == right:
return arr[left]
pivot_index = partition(left, right)
if pivot_index == k - 1:
return arr[pivot_index]
elif pivot_index > k - 1:
return quick_select(left, pivot_index - 1, k)
else:
return quick_select(pivot_index + 1, right, k)
return quick_select(0, len(arr) - 1, k)
# 테스트
arr = [3, 2, 1, 5, 6, 4]
print(f"2번째로 큰 수: {find_kth_largest(arr.copy(), 2)}")
|
🎯 핵심 정리
꼭 기억해야 할 5가지
- 배열은 인덱스 접근이 O(1), 연결 리스트는 O(n)
- 배열은 연속 메모리, 연결 리스트는 분산 메모리
- 삽입/삭제는 연결 리스트가 유리 (특히 맨 앞)
- 캐시 효율은 배열이 우수
- 메모리 오버헤드는 연결 리스트가 더 큼
실무 활용 팁
- 크기가 고정적이고 접근이 빈번하면 → 배열
- 크기가 동적이고 삽입/삭제가 빈번하면 → 연결 리스트
- 대부분의 경우 동적 배열(Python list)이 적절
- 특수한 요구사항이 있을 때만 연결 리스트 고려
📚 추가 학습 자료
추천 문제
- LeetCode: Reverse Linked List, Merge Two Sorted Lists
- 프로그래머스: 주식가격, 기능개발
- 백준: 1406번(에디터), 1158번(요세푸스)
심화 주제
- Skip List (확률적 자료구조)
- XOR Linked List (메모리 절약)
- Unrolled Linked List (캐시 친화적)
🚀 다음 시간 예고
다음 포스트에서는 스택(Stack)과 큐(Queue)를 다룹니다. LIFO와 FIFO의 원리, 그리고 실제 활용 사례를 알아보겠습니다!
✅ 이 글에서 배운 것
스스로 확인해보세요! 각 항목을 설명할 수 있다면 체크하세요.
개념 이해
- 배열과 연결 리스트의 메모리 구조 차이
- 각 자료구조의 시간 복잡도 (접근, 삽입, 삭제)
- 동적 배열의 크기 조정 원리
- 단일/이중 연결 리스트의 차이
- 순환 연결 리스트의 특징
실용적 이해
- 언제 배열을, 언제 연결 리스트를 사용해야 하는지
- Python list와 Java ArrayList의 내부 동작
- 캐시 친화성이 배열에 유리한 이유
- 연결 리스트의 메모리 오버헤드
실습 완료
- 동적 배열 구현 코드를 이해했다
- 단일/이중 연결 리스트를 직접 구현해봤다
- 각 자료구조의 연산 성능을 비교해봤다
- 실제 활용 예제 (LRU 캐시 등)를 실행했다
📌 시리즈 네비게이션
| 순서 | 제목 | 링크 |
| 5 | 네트워크 기초: OSI 7계층, TCP/IP 이해하기 | ← 이전 글 |
| 6 | 배열과 연결 리스트 ✅ | 현재 글 |
| 7 | 스택과 큐: LIFO와 FIFO의 활용 | 다음 글 → |
📋 전체 시리즈 목차 보기
기초 이론편
- CS 입문 ✅
- 컴퓨터 구조 ✅
- 이진수와 논리 게이트 ✅
- 운영체제 기초 ✅
- 네트워크 기초 ✅
자료구조편
- 배열과 연결 리스트 ✅
- 스택과 큐
- 트리 구조
- 그래프
- 해시 테이블
알고리즘편
- 복잡도 분석
- 정렬 알고리즘
- 탐색 알고리즘
- 동적 계획법
- 그리디와 분할정복
실무 응용편
- 데이터베이스 이론
- 소프트웨어 공학
- 보안 기초
- 분산 시스템
- CS 종합과 면접 준비