
프로그래머라면 누구나 한 번쯤 듣는 말이 있습니다.
"자료구조를 잘 이해하면, 더 좋은 코드를 짤 수 있다!"
하지만 정작 자료구조를 배울 때는 머리가 아파지곤 하죠. 스택, 큐, 연결 리스트, 트리, 그래프… 😵💫
도대체 자료구조가 뭐길래 이렇게 중요한 걸까요? 오늘은 자료구조의 중요성을 10배 더 깊이 있게 파헤쳐 보겠습니다!
자료구조(Data Structure)는 데이터를 효율적으로 저장하고 관리하는 방법을 의미합니다. 컴퓨터는 데이터를 처리하는 기계이므로, 데이터를 어떻게 정리하느냐에 따라 성능과 효율성이 크게 달라집니다. 자료구조는 데이터를 조직화하고, 접근 및 수정하는 방법을 체계화하여 프로그램의 성능을 최적화하는 데 필수적입니다.
📦 배열(Array):

🗄️ 스택(Stack):

🚶♂️ 큐(Queue):

🔗 연결 리스트(Linked List):

🌳 트리(Tree):

🔗 그래프(Graph):

자료구조를 선택할 때는 시간 복잡도와 공간 복잡도를 고려해야 합니다.
위에서 “라이브러리를 쓰는 것도 중요하다!”고 강조했지만, 실제로 어떤 라이브러리를 쓰면 좋을지 감이 안 잡힐 수 있죠. 그래서 자주 쓰이는 Python 라이브러리들을 자료구조별로 간단히 정리해봤습니다.
간편함
복잡한 자료구조를 처음부터 구현할 필요 없이,
한두 줄 코드로 바로 사용이 가능합니다.
최적화
파이썬 표준 라이브러리나 유명 라이브러리는
이미 성능이 검증되었고, 내부 구현도 깔끔합니다.
유지보수 용이
내가 직접 구현한 자료구조 코드가 길어질수록
디버깅과 유지보수가 어려울 수 있지만,
라이브러리를 사용하면 그런 부분을 신경 쓰지 않아도 됩니다.
아래 예시들은 파이썬 기준으로 작성했지만, 다른 언어에도 비슷한 표준 라이브러리나 패키지가 있으니 참고하세요!
listarr = [1, 2, 3, 4]
arr.append(5) # 요소 추가
arr.pop() # 마지막 요소 제거
print(arr[2]) # 인덱스 접근(O(1))
list 또는 collections.deque (파이썬 리스트로도 스택 기능을 충분히 구현 가능)stack = []
stack.append(1) # push
stack.append(2)
print(stack.pop()) # pop -> 2
collections.dequefrom collections import deque
queue = deque()
queue.append(1) # enqueue
queue.append(2)
print(queue.popleft()) # dequeue -> 1
list로 대부분 대체 가능합니다.anytree (서드파티) 또는 직접 구현from anytree import Node, RenderTree
root = Node("Root")
child1 = Node("Child1", parent=root)
child2 = Node("Child2", parent=root)
for pre, fill, node in RenderTree(root):
print(f"{pre}{node.name}")
networkx (서드파티)import networkx as nx
G = nx.Graph()
G.add_edge("A", "B")
G.add_edge("B", "C")
# "A"에서 "C"까지 최단 경로 찾기
print(list(nx.shortest_path(G, "A", "C"))) # ['A', 'B', 'C']
dicthash_table = {}
hash_table["key"] = "value"
print(hash_table["key"]) # "value"
heapqimport heapq
heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 2)
print(heapq.heappop(heap)) # 1 (최소 힙)
pygtrie (서드파티)import pygtrie
trie = pygtrie.Trie()
trie["app"] = True
trie["apple"] = True
print(trie.has_subtrie("app")) # True (해당 prefix를 가지는 단어가 있음)
print(trie.has_key("app")) # True (정확히 "app" 존재)
deque를 쓰는 식으로,networkx 같은 서드파티 문서를 보면, 더 효율적으로 쓸 수 있는 꿀팁들이 많이 있습니다.맞아요! 파이썬 같은 언어에는 list, deque, heapq 같은 자료구조 라이브러리가 이미 준비되어 있습니다. 이러한 라이브러리를 사용하면 기본적인 자료구조를 손쉽게 구현하고 사용할 수 있습니다.
라이브러리가 모든 문제를 해결해 주지는 않아요. 우리가 직접 자료구조를 만들 줄 모르면, 최적의 방법을 찾기 어려워질 수 있어요. 직접 구현해 보면 자료구조의 내부 동작을 더 깊이 이해할 수 있고, 복잡한 문제를 해결하는 데 필요한 창의적인 접근 방식을 배울 수 있습니다.
class Stack:
def __init__(self):
self.data = []
def push(self, item):
self.data.append(item)
def pop(self):
return self.data.pop() if self.data else None
def peek(self):
return self.data[-1] if self.data else None
def is_empty(self):
return len(self.data) == 0
def size(self):
return len(self.data)
# 사용 예시
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.pop()) # 3 (후입선출)
print(stack.peek()) # 2
print(stack.is_empty()) # False
print(stack.size()) # 2
class Queue:
def __init__(self):
self.data = []
def enqueue(self, item):
self.data.append(item)
def dequeue(self):
return self.data.pop(0) if self.data else None
def front(self):
return self.data[0] if self.data else None
def is_empty(self):
return len(self.data) == 0
def size(self):
return len(self.data)
# 사용 예시
queue = Queue()
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)
print(queue.dequeue()) # 1 (선입선출)
print(queue.front()) # 2
print(queue.is_empty()) # False
print(queue.size()) # 2
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = Node(data)
if not self.head:
self.head = new_node
return
last = self.head
while last.next:
last = last.next
last.next = new_node
def prepend(self, data):
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def delete_with_value(self, data):
if not self.head:
return
if self.head.data == data:
self.head = self.head.next
return
current = self.head
while current.next and current.next.data != data:
current = current.next
if current.next:
current.next = current.next.next
def find(self, data):
current = self.head
while current and current.data != data:
current = current.next
return current
def display(self):
current = self.head
while current:
print(current.data, end=' -> ')
current = current.next
print('None')
# 사용 예시
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.display() # 1 -> 2 -> 3 -> None
ll.prepend(0)
ll.display() # 0 -> 1 -> 2 -> 3 -> None
ll.delete_with_value(2)
ll.display() # 0 -> 1 -> 3 -> None
print(ll.find(3).data) # 3
class TreeNode:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
class BinarySearchTree:
def __init__(self):
self.root = None
def insert(self, data):
if not self.root:
self.root = TreeNode(data)
return
self._insert(self.root, data)
def _insert(self, node, data):
if data < node.data:
if node.left:
self._insert(node.left, data)
else:
node.left = TreeNode(data)
else:
if node.right:
self._insert(node.right, data)
else:
node.right = TreeNode(data)
def search(self, data):
return self._search(self.root, data)
def _search(self, node, data):
if not node or node.data == data:
return node
if data < node.data:
return self._search(node.left, data)
return self._search(node.right, data)
def inorder_traversal(self):
return self._inorder_traversal(self.root, [])
def _inorder_traversal(self, node, result):
if node:
self._inorder_traversal(node.left, result)
result.append(node.data)
self._inorder_traversal(node.right, result)
return result
# 사용 예시
bst = BinarySearchTree()
bst.insert(10)
bst.insert(5)
bst.insert(15)
bst.insert(3)
bst.insert(7)
bst.insert(12)
bst.insert(18)
print(bst.inorder_traversal()) # [3, 5, 7, 10, 12, 15, 18]
print(bst.search(7).data) # 7
print(bst.search(20)) # None
설명: 키-값 쌍으로 데이터를 저장하는 자료구조로, 해시 함수를 사용해 데이터를 빠르게 검색할 수 있습니다.
특징:
사용 예시:
dict는 해시 테이블을 기반으로 합니다.코드 예시:
class HashTable:
def __init__(self, size=100):
self.size = size
self.table = [[] for _ in range(size)]
def _hash(self, key):
return hash(key) % self.size
def set(self, key, value):
index = self._hash(key)
for pair in self.table[index]:
if pair[0] == key:
pair[1] = value
return
self.table[index].append([key, value])
def get(self, key):
index = self._hash(key)
for pair in self.table[index]:
if pair[0] == key:
return pair[1]
return None
def delete(self, key):
index = self._hash(key)
for i, pair in enumerate(self.table[index]):
if pair[0] == key:
del self.table[index][i]
return
# 사용 예시
ht = HashTable()
ht.set("apple", 1)
ht.set("banana", 2)
print(ht.get("apple")) # 1
ht.set("apple", 3)
print(ht.get("apple")) # 3
ht.delete("banana")
print(ht.get("banana")) # None
설명: 완전 이진 트리의 일종으로, 부모 노드가 자식 노드보다 크거나 작은 성질을 유지하는 자료구조입니다. 주로 우선순위 큐 구현에 사용됩니다.
특징:
사용 예시:
코드 예시:
import heapq
# 최소 힙
min_heap = []
heapq.heappush(min_heap, 3)
heapq.heappush(min_heap, 1)
heapq.heappush(min_heap, 2)
print(heapq.heappop(min_heap)) # 1
print(heapq.heappop(min_heap)) # 2
print(heapq.heappop(min_heap)) # 3
# 최대 힙 (힙에 음수를 넣어 최대 힙처럼 사용)
max_heap = []
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -1)
heapq.heappush(max_heap, -2)
print(-heapq.heappop(max_heap)) # 3
print(-heapq.heappop(max_heap)) # 2
print(-heapq.heappop(max_heap)) # 1
설명: 문자열을 효율적으로 저장하고 검색할 수 있는 트리의 일종으로, 각 노드가 하나의 문자를 저장합니다.
특징:
사용 예시:
코드 예시:
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
current = self.root
for char in word:
if char not in current.children:
current.children[char] = TrieNode()
current = current.children[char]
current.is_end_of_word = True
def search(self, word):
current = self.root
for char in word:
if char not in current.children:
return False
current = current.children[char]
return current.is_end_of_word
def starts_with(self, prefix):
current = self.root
for char in prefix:
if char not in current.children:
return False
current = current.children[char]
return True
# 사용 예시
trie = Trie()
trie.insert("apple")
trie.insert("app")
print(trie.search("app")) # True
print(trie.search("appl")) # False
print(trie.starts_with("appl")) # True
그래프는 노드와 간선으로 구성되며, 다양한 방식으로 표현할 수 있습니다.
코드 예시 (인접 리스트):
class Graph:
def __init__(self):
self.adj_list = {}
def add_vertex(self, vertex):
if vertex not in self.adj_list:
self.adj_list[vertex] = []
def add_edge(self, vertex1, vertex2):
self.add_vertex(vertex1)
self.add_vertex(vertex2)
self.adj_list[vertex1].append(vertex2)
self.adj_list[vertex2].append(vertex1) # 무방향 그래프인 경우
def display(self):
for vertex in self.adj_list:
print(f"{vertex}: {self.adj_list[vertex]}")
# 사용 예시
g = Graph()
g.add_edge("A", "B")
g.add_edge("A", "C")
g.add_edge("B", "D")
g.add_edge("C", "D")
g.display()
# 출력:
# A: ['B', 'C']
# B: ['A', 'D']
# C: ['A', 'D']
# D: ['B', 'C']
설명: 트리의 높이를 최소화하여 연산의 시간 복잡도를 최적화한 트리 구조.
종류:
특징:
코드 예시 (AVL 트리 간단 구현):
class AVLNode:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.height = 1
class AVLTree:
def insert(self, root, key):
if not root:
return AVLNode(key)
elif key < root.key:
root.left = self.insert(root.left, key)
else:
root.right = self.insert(root.right, key)
root.height = 1 + max(self.get_height(root.left), self.get_height(root.right))
balance = self.get_balance(root)
# LL Case
if balance > 1 and key < root.left.key:
return self.right_rotate(root)
# RR Case
if balance < -1 and key > root.right.key:
return self.left_rotate(root)
# LR Case
if balance > 1 and key > root.left.key:
root.left = self.left_rotate(root.left)
return self.right_rotate(root)
# RL Case
if balance < -1 and key < root.right.key:
root.right = self.right_rotate(root.right)
return self.left_rotate(root)
return root
def left_rotate(self, z):
y = z.right
T2 = y.left
y.left = z
z.right = T2
z.height = 1 + max(self.get_height(z.left), self.get_height(z.right))
y.height = 1 + max(self.get_height(y.left), self.get_height(y.right))
return y
def right_rotate(self, z):
y = z.left
T3 = y.right
y.right = z
z.left = T3
z.height = 1 + max(self.get_height(z.left), self.get_height(z.right))
y.height = 1 + max(self.get_height(y.left), self.get_height(y.right))
return y
def get_height(self, node):
if not node:
return 0
return node.height
def get_balance(self, node):
if not node:
return 0
return self.get_height(node.left) - self.get_height(node.right)
def inorder_traversal(self, root):
res = []
if root:
res = self.inorder_traversal(root.left)
res.append(root.key)
res = res + self.inorder_traversal(root.right)
return res
# 사용 예시
avl = AVLTree()
root = None
keys = [10, 20, 30, 40, 50, 25]
for key in keys:
root = avl.insert(root, key)
print(avl.inorder_traversal(root)) # [10, 20, 25, 30, 40, 50]
직접 구현해보면, 왜 배열 접근이 O(1)인지, 연결 리스트 검색이 왜 O(n)인지 등을 훨씬 직관적으로 알 수 있게 됩니다.
구글 검색:
구글에서 검색할 때 0.1초 만에 결과가 나오는 이유는 트라이(Trie), 해시 테이블(Hash Table) 같은 고급 자료구조 덕분입니다. 트라이는 단어의 공통 접두사를 공유하여 검색 효율을 높이고, 해시 테이블은 빠른 키-값 검색을 가능하게 합니다.
예시:
페이스북의 "이 사람을 아시나요?":
페이스북이 "이 사람을 아시나요?"라고 추천할 때, 그래프(Graph)를 이용해 친구 관계를 분석합니다. 친구 관계는 그래프의 노드와 간선으로 표현되며, 연결된 친구의 친구를 추천하는 방식으로 구현됩니다.
예시:
게임에서 적을 피해 빠르게 도망가야 할 때:
게임에서 캐릭터가 적을 피하거나 목표 지점으로 이동할 때, 그래프 탐색 알고리즘(BFS, DFS, A*)이 최단 경로를 찾는 데 쓰여요. 이러한 알고리즘은 게임의 실시간 성능에 큰 영향을 미칩니다.
예시:
데이터베이스:
대규모 데이터베이스에서 데이터를 빠르게 검색하고 관리하기 위해 다양한 자료구조가 사용됩니다. B-트리(B-Tree), 비트맵 인덱스(Bitmap Index) 등이 대표적입니다.
예시:
네트워크:
인터넷과 같은 대규모 네트워크에서는 데이터 패킷을 최적의 경로로 전달하기 위해 그래프 기반 알고리즘이 사용됩니다.
예시:
머신러닝:
머신러닝 알고리즘은 대규모 데이터를 처리하고 분석하기 위해 다양한 자료구조를 사용합니다. 행렬(Matrix), 텐서(Tensor), 그래프 등이 대표적입니다.
예시:
운영 체제:
운영 체제는 프로세스 관리, 메모리 관리, 파일 시스템 관리 등에서 다양한 자료구조를 사용합니다.
예시:
많은 기업의 코딩 면접에서는 자료구조와 알고리즘 문제가 출제됩니다. 자료구조를 잘 이해하고 직접 구현할 줄 알면, 문제 해결 능력을 효과적으로 어필할 수 있어 면접 합격률이 높아집니다.
예시:
적절한 자료구조를 선택하면 코드의 성능이 크게 향상됩니다. 예를 들어, 데이터 검색이 빈번한 경우 해시 테이블을 사용하면 검색 속도를 대폭 향상시킬 수 있습니다.
예시:
복잡한 문제를 해결할 때, 자료구조를 통해 최적의 해결책을 설계할 수 있습니다. 예를 들어, 대규모 데이터 처리 시 적절한 자료구조를 사용하면 시스템의 확장성과 안정성을 높일 수 있습니다.
예시:
자료구조를 학습하면서 다양한 문제를 해결해보면 논리적 사고와 문제 해결 능력이 향상됩니다. 이는 개발자로서의 역량을 크게 높여줍니다.
예시:
잘 설계된 자료구조는 소프트웨어의 성능을 최적화하고, 유지보수를 용이하게 만듭니다. 코드의 가독성이 높아지고, 새로운 기능을 추가하기도 쉬워집니다.
예시:
자료구조는 방대한 주제이므로 체계적인 학습 계획이 필요합니다. 기본 자료구조부터 시작해 점차 복잡한 자료구조로 나아가는 것이 좋습니다.
학습 단계:
자료구조를 학습할 때는 이론적인 개념을 이해하는 것뿐만 아니라, 실제로 구현해보는 것이 중요합니다. 이를 통해 자료구조의 동작 원리를 깊이 있게 이해할 수 있습니다.
방법:
자료구조는 시각적으로 이해하면 더 쉽게 학습할 수 있습니다. 다양한 시각화 도구를 활용하여 자료구조의 동작을 시각적으로 확인해보세요.
추천 도구:
좋은 참고서와 온라인 강의를 통해 체계적으로 자료구조를 학습할 수 있습니다. 유명한 자료구조 교재나 온라인 강의를 참고해보세요.
추천 자료:
자료구조는 한 번에 완전히 이해하기 어렵습니다. 꾸준히 연습하고, 주기적으로 복습하여 지식을 확고히 다지세요.
방법:
자료구조는 단순히 이론적인 지식이 아니라, 실무에서의 문제 해결과 성능 최적화에 필수적인 도구입니다. 라이브러리를 잘 활용하는 것도 중요하지만, 직접 구현해 보는 경험이 문제 해결 능력을 크게 향상시킵니다.
📢 자료구조 마스터에 도전해보세요! 💪🔥 여러분의 프로그래밍 실력을 한층 더 높여줄 자료구조의 세계에 뛰어들어 보세요. 꾸준한 노력과 학습을 통해 자료구조의 전문가가 되어, 더 나은 코드와 효율적인 문제 해결 능력을 갖추시길 바랍니다!