2025-11-14
오늘 배운 것
- 기본 개념
- 자료구조란?
- 데이터를 효율적으로 저장/관리/검색/수정하기 위한 구조
- 목적 : 시간과 공간을 최소화해 문제를 더 빠르게 해결
- 종류 : 선형, 비선형, 해시, 트리, 그래프 등
- ADT(Abastract Data Type: 추상화된 데이터 타입)
- 자료구조를 사용 관점에서 정의한 것으로 내부 구현이 어떻게 되어 있는지 숨기고 어떤 연산을 지원하는지만 정의
- 예 : Stack ADT는 push, pop, peek, isEmpty만 정의 내부가 배열인지 리스트인지 관심 없음
- 배열 VS 연결리스트
- 배열
- 개념
- 메모리에 연속된 공간으로 저장
- 인덱스 O(1) 접근 가능
- 장점
- 랜덤 접근 빠름 O(1)
- 캐시 친화적 → 실제로도 빠름
- 단점
- 중간 삽입/삭제 O(n)
- 크기 고정 (자바 배열은 불변)
- 개념
- 연결 리스트
- 개념
- 노드가 포인터로 연결된 구조
- 메모리가 연속적일 필요 없음
- 장점
- 삽입/삭제 O(1) (단, 노드 참조를 알고 있을 때)
- 크기 제약 없음
- 단점
- 랜덤 접근 O(n)
- 캐시 비친화적 → 실제 성능 체감 낮음
- 추가 메모리 사용(포인터)
- 개념
- 배열
- 자료구조란?
- 선형 자료구조
- ArrayList vs LinkedList
- ArrayList
- 개념 : 동적 배열 기반, 연속된 메모리
- 구조 : Object[] 배열
- 장점
- 단점
- LinkedList
- 개념 : Node + Pointer 기반, 비연속 메모리
- 구조 : Node(prev, valaue, next)
- 장점
- 단점
- 조회가 많을 때 → ArrayList
- 삽입/삭제가 많을 때 → LinkedList
- ArrayList
- Stack
- 개념
- LIFO (Last In First Out)
- 한쪽(top)에서만 삽입/삭제
- 제공 연산
- push
- pop
- peek
- isEmpty
- 시간 복잡도
- push, pop, peek : O(1)
- 특징
- 함수 호출 스택, 괄호 검사, DFS 등에서 사용
- 개념
- Queue
- 개념
- FIFO (First In First Out)
- 한쪽에서 넣고(front) 다른 쪽에서 뺌(back)
- 제공 연산
- offer
- poll
- peek
- isEmpty
- 시간 복잡도
- offer, poll, peek : O(1)
- 특징
- BFS, 프로세스 스케줄러, 캐시, 작업 큐 등 사용
- 개념
- Deque(Double-ended Queue)
- 개념
- 양쪽에서 삽입/삭제 가능한 큐
- 제공 연산
- addFirst
- addLast
- removeFirst
- removeLast
- peekFirst
- peekLast
- 시간 복잡도 : O(1)
- 특징 : 슬라이딩 윈도우, Monotonic Queue, BFS 최적화 등에 사용
- 개념
- stack → queue
- 아이디어
- 스택 2개를 사용
- inStack : 입력
- outStack : 출력
- 출력시 outStack이 비어 있으면 inStack모든 값을 옮김
- 시간 복잡도
- 각 원소가 최대 1번만 이동 : 평균 O(1)
- 최악 : O(n) (하지만 거의 없음! → amortized O(1))
-
코드
class MyQueue { Stack<Integer> inStack = new Stack<>(); Stack<Integer> outStack = new Stack<>(); // 큐에 데이터 넣기 public void push(int x) { inStack.push(x); } // 큐에서 데이터 꺼내기 public int pop() { shift(); return outStack.pop(); } // 큐의 front 확인 public int peek() { shift(); return outStack.peek(); } // 요소가 없을 때 true public boolean empty() { return inStack.isEmpty() && outStack.isEmpty(); } // 필요한 순간에만 in → out으로 옮김 (역순) private void shift() { if (outStack.isEmpty()) { while (!inStack.isEmpty()) { outStack.push(inStack.pop()); } } } }
- 아이디어
- queue → stack
- 아이디어
- 큐 2개를 사용
- push 할 때 q2에 새 원소를 넣고
- q1의 원소를 q2에 다 넣어서 새원소가 front에 오게 함
- 이후 swap(q1, q2)
- 시간 복잡도
- push: O(n)
- pop: O(1)
-
코드
class MyStack { Queue<Integer> q1 = new LinkedList<>(); Queue<Integer> q2 = new LinkedList<>(); // 스택 push public void push(int x) { q2.offer(x); // 새 값 우선 넣고 while (!q1.isEmpty()) { // q1 요소를 전부 q2 뒤로 이동 q2.offer(q1.poll()); } // swap Queue<Integer> temp = q1; q1 = q2; q2 = temp; } // pop : q1의 front가 top public int pop() { return q1.poll(); } // top public int top() { return q1.peek(); } // empty public boolean empty() { return q1.isEmpty(); } }
- 아이디어
- Monotonic Stack / Monotonic Queue
- Monotonic Stack
- 개념
- 사용처
- 시간 복잡도
- Monotonic Queue
- 개념
- 사용처
- 시간 복잡도
- Monotonic Stack
- ArrayList vs LinkedList
- 우선순위 큐/힙
- 우선 순위 큐
- 개념
- 들어온 순서와 상관 없이 가장 작은/큰 값이 먼저 나오는 큐
- 특징
- 내부적으로 Binary Heap
- 자바 기본은 min-heap
- 제공 연산
- offer, poll, peek
- 시간 복잡도
- offer/poll : O(logn)
- peek : O(1)
- 개념
- 힙
- 개념
- 완전 이진 트리 형태
- 최소 힙 기준, 부모 ≤ 자식
- 가장 큰/작은 값을 O(1)로 조회 가능
- 구조적 특징
- 완전 이진 트리
- 배열로 표현되는 이유
- 힙의 종류
- 최소/최대 힙
- indexed heap (인덱스 업데이트 가능한 힙)
- d-ary heap (자식이 여러개)
- Heapify : 힙 재정렬 과정
- down-heapify (힙이 깨졌을 때 아래로 내려가며 정렬)
- up-heapify (삽입 시 위로 올라가며 정렬)
- 시간 복잡도 : 항상 트리의 높이만큼 이동
- 시간 복잡도
- 삽입/삭제 : O(log n)
- heapify 전체 구성 : O(n)
- 최대/최소 조회 : O(1)
-
코드
class MinHeap { List<Integer> heap = new ArrayList<>(); public void add(int x) { heap.add(x); up(heap.size() - 1); } public int poll() { int root = heap.get(0); int last = heap.remove(heap.size() - 1); if (!heap.isEmpty()) { heap.set(0, last); down(0); } return root; } private void up(int i) { while (i > 0) { int p = (i - 1) / 2; if (heap.get(p) <= heap.get(i)) break; swap(p, i); i = p; } } private void down(int i) { int n = heap.size(); while (true) { int left = i * 2 + 1; int right = i * 2 + 2; int smallest = i; if (left < n && heap.get(left) < heap.get(smallest)) smallest = left; if (right < n && heap.get(right) < heap.get(smallest)) smallest = right; if (smallest == i) break; swap(i, smallest); i = smallest; } } private void swap(int a, int b) { int t = heap.get(a); heap.set(a, heap.get(b)); heap.set(b, t); } }
- 개념
- 우선 순위 큐
- 해시
- 개념
- key를 해시 함수를 통해 배열의 인덱스(해시 값)로 변환하는 자료구조
- 탐색, 삽입, 삭제 평균 O(1)
- 최악은 체이닝 리스트면 O(n), 레드블랙트리면 O(logn)
- 내부는 실제로 배열 + LinkedList/Tree 조합
- 핵심 구조
- 해시 함수
- key → index
- 이상적인 해시함수 조건
- 충돌이 적음
- 연산이 빠름
- 키를 고르게 분산
- 해시 버킷
- key-value 저장
- 보통 배열 + 그 안에 여러개 저장할 연결리스트 or 트리 구조
- 해시 함수
- 사용 이유
- 탐색, 삽입, 삭제가 평균 O(1)로 빠름
- 문자열 키 기반 탐색을 빠르게 저장
- 백엔드에서 사용자 조회, 캐싱, 권한, 세션 처리에서 사용
- 해싱 충돌
- 해싱 값이 같아 서로 다른 키가 같은 버킷을 차지하는 현상
- 해결 방법
- 체이닝 (Separate Chaining)
- 개념
- 각 해시 버킷 내부에 LinkedList / Tree로 여러 값 저장
- 구조
table[hash(key)] → LinkedList or Tree of Nodes
- 장점
- 구현 간단
- 충돌 많이 나도 안정적으로 저장
- 확장성 좋음
- 단점
- 최악 O(n) : 모두 같은 버킷으로 몰릴 때
- Java8부터는 이 상황에서 LinkedList가 Red-Black Tree로 전환 ⇒ O(log n)
- 최악 O(n) : 모두 같은 버킷으로 몰릴 때
- 개념
- 오픈 어드레싱 (Openn Addressing)
- 개념
- 충돌이 나면 빈 공간을 찾아 저장
- 탐색 방식
- Linear Probing
- 한칸씩 순차적 이동
- Quadratic Probing
- 제곱 간격으로 이동
- 1^2, 2^2, 3^2
- 제곱 간격으로 이동
- Double Hashing
- 또 다른 해시 함수를 한 번 더 사용
- 탐색 성능 가장 우수
- Linear Probing
- 장점
- 추가적인 메모리 필요 없음
- 캐시 친화적(배열 기반)
- 단점
- 삭제 구현 복잡
- 테이블이 찰수록 성능 급격히 저하 → 탐색 시간이 늘어나기 때문
- 버킷 클러스터링 문제 발생 가능
- 해시 충돌이 계속 이어져서 특정 구간의 연속된 영역이 꽉차는 현상
- 특히 선형 탐색 방식에서 자주 발생
- 개념
- 체이닝 (Separate Chaining)
- Load Factor (적재율)
- 해시 테이블이 얼마나 차있는지 나타내는 지표
Load Factor = 현재 저장된 데이터 수 / 버킷 수- 적재율이 너무 높으면 → 충돌이 많아지고 성능 저하 → 리사이징 발생
- 자바 기본 값 : 0.75
- 현재 저장된 데이터 수 / 배열의길이(혹은 버킷 수)> load factor(0.75) 이면 용량 2배로 증가
- Java HashMap 내부구조
- 버킷 구조
배열 + (연결리스트 or 레드블랙트리)- 비어 있음 - null
- LinkedList 형태 (충돌 낮을 때)
- 버킷 크기가 커지면 Red-Black Tree로 자동 변환 treeify
- 변환 조건
- 버킷의 길이 ≥ 8
- 전체 테이블 크기 ≥ 64
- 변환 조건
- 버킷 구조
- 개념
- 트리 기초
- 개념
- 부모- 자식 관계로 구성된 비순환(acyclic) 계층적 자료구조
- 사이클이 없음
- 루트에서 시작
- 루트의 깊이는 0
- 그래프의 한 종류
- 노드가 n개면 간선은 항상 n-1개
- 트리 순회
- 전위 순회 : root → left → right
- 구조 복사/직렬화 할 때 사용
- 중위 순회 : left → root → right
- BST 오름차순 결과
- 후위 순회 : left → right → root
- 하위 구조부터 삭제해야할때 사용(폴더 삭제 등)
- 전위 순회 : root → left → right
- 종류
- 이진 트리 (Binary Tree)
- 자식이 최대 2개
- 완전 이진 트리 (Complete Binary Tree)
- 왼쪽 부터 빈칸 없이 채워짐
- 힙이 이 구조
- 포화 이진 트리 (Full Binary Tree)
- 모든 내부 노드가 자식 2개
- 정 이진 트리 (Perfect Binary Tree)
- 모든 레벨이 꽉 차있음
- 리프가 같은 depth
- 이진 탐색 트리 (Binary Search Tree)
- left → root → right
- 탐색/삽입 O(logn) 보장 (평균)
- 정렬된 순서는 inorder
- 이진 트리 (Binary Tree)
- 시간 복잡도
- 탐색 / 삽입 / 삭제
- 평균 : O(log n)
- 최악 : O(n) → 한쪽으로 치우쳐져 linked list처럼 됐을 때
- 순회
- O(n)
- 탐색 / 삽입 / 삭제
- 개념
- 균형 트리 (Balanced Binary Search Tree)
- 개념
- 트리의 높이를 O(log n) 수준으로 유지하기 위해 삽입/삭제 시 자동으로 균형을 맞추는 이진 탐색 트리 (한쪽으로 치우쳐지는 것을 방지)
- 시간 복잡도
- 탐색 : O(log n)
- 삽입/삭제 : O(log n) + 재균형
- 종류
- AVL Tree (Adelson-Velsky & Landis Tree)
- 개념
- 균형이 엄격함
- 어떤 노드든 왼쪽 높이 - 오른쪽 높이 ≤ 1
- 탐색이 아주 빠름
- 회전
- 장점
- 검색 성능 최강 (항상 더 균형 잡혀 있음)
- 균형 조건이 엄격하여 탐색 O(log n) 보장
- 단점
- 삽입/삭제 시 회전이 자주 발생
- 구현 복잡
- 언제 사용?
- 검색 비중이 매우 높을 때
- 개념
- Red-Black Tree
- 개념
- 자바 TreeMap, TreeSet이 사용하는 구조
- FIFO/균형 유지 덜 엄격하지만 삽입/삭제가 더 빠름
- 균형 조건 완화로 연산 속도가 전체적으로 안정적
- 핵심 규칙
- 5가지 색 규칙으로 균형 유지
- 이 조건 덕분에 최대 높이 = 2 * log n
- 장점
- 속도 매우 안정적
- 삽입/삭제가 AVL보다 덜 부담
- 대부분의 언어 라이브러리 기본 구현
- 단점
- 탐색에서 AVL보다 약간 느릴 수 있음
- 어디에 사용?
- Java TreeMap / TreeSet / HashMap(충돌 버킷이 커지면! 자동 변환)
- C++ map / multimap / set
- Linux 커널의 레드블랙트리 구조체(rbtree)
- JVM 내부 자료구조 탑재
- 개념
- AVL Tree (Adelson-Velsky & Landis Tree)
- 개념
- 특수 트리
- 트라이(Trie) - 문자열 처리에 특화된 트리
- 개념
- 문자열을 문자 단위로 분리해 트리 형태로 저장하는 자료 구조
- 장점
- 문자열 탐색 O(m) (m = 문자열 길이) → 해시 충돌 걱정 없음
- 공통 접두사 재사용 → 메모리 절약
- 자동완성 / 사전 / 문자열 검색에 매우 강력
- 단점
- 해시보다는 메모리 많이 씀
- 구현 복잡도 증가
-
코드
class TrieNode { TrieNode[] child = new TrieNode[26]; boolean isEnd; } class Trie { TrieNode root = new TrieNode(); void insert(String word) { TrieNode cur = root; for (char c : word.toCharArray()) { int idx = c - 'a'; if (cur.child[idx] == null) cur.child[idx] = new TrieNode(); cur = cur.child[idx]; } cur.isEnd = true; } boolean search(String word) { TrieNode cur = root; for (char c : word.toCharArray()) { int idx = c - 'a'; if (cur.child[idx] == null) return false; cur = cur.child[idx]; } return cur.isEnd; } boolean startsWith(String prefix) { TrieNode cur = root; for (char c : prefix.toCharArray()) { int idx = c - 'a'; if (cur.child[idx] == null) return false; cur = cur.child[idx]; } return true; } }
- 개념
- 세그먼트트리(Segment Tree) - 구간(min/max/sum) 쿼리를 빠르게 처리하는 트리
- 개념
- 배열의 구간 정보를 저장하는 트리
- 구간 합/최소/최대/최빈값 등을 O(log n)에 처리
- 특징
- 완전 이진 트리
- 업데이트 O(log n)
- 구간 쿼리 O(log n)
- 저장 메모리 약 4배 필요
-
코드 - 구간 합 버전
int[] tree; int n; int init(int node, int start, int end) { if (start == end) return tree[node] = arr[start]; int mid = (start + end) / 2; return tree[node] = init(node*2, start, mid) + init(node*2+1, mid+1, end); } int query(int node, int start, int end, int l, int r) { if (r < start || end < l) return 0; if (l <= start && end <= r) return tree[node]; int mid = (start + end) / 2; return query(node*2, start, mid, l, r) + query(node*2+1, mid+1, end, l, r); } void update(int node, int start, int end, int idx, int diff) { if (idx < start || end < idx) return; tree[node] += diff; if (start != end) { int mid = (start + end) / 2; update(node*2, start, mid, idx, diff); update(node*2+1, mid+1, end, idx, diff); } }
- 개념
- 펜윅 트리 (Fenwick Tree / Binary Indexed Tree)
- 개념
- 세그먼트 트리보다 단순하고 빠른 구간 합 자료구조
- 내부적으로 이진수 LSB(가장 낮은 1비트) 기반으로 동작
- 장점
- 구현 매우 쉬움
- 메모리 적음
- O(log n)
- prefix sum에 매우 강력
- 단점
- 구간 갱신 / 임의 범위 갱신 은 세그트리가 더 강력
- 최소/최대 구하기에는 부적합
-
코드
class Fenwick { int[] fenwick; int n; Fenwick(int size) { n = size; fenwick = new int[n+1]; } void update(int idx, int delta) { while (idx <= n) { fenwick[idx] += delta; idx += idx & -idx; // LSB 만큼 증가 } } int sum(int idx) { int s = 0; while (idx > 0) { s += fenwick[idx]; idx -= idx & -idx; } return s; } int rangeSum(int l, int r) { return sum(r) - sum(l-1); } }
- 개념
- B-트리 / B+ 트리 - DB, 파일 시스템 인덱스
- B-Tree
- 개념
- 특징
- B+Tree - DB 인덱스 구조
- 개념
- 장점
- 사용처
- B-Tree
- 트라이(Trie) - 문자열 처리에 특화된 트리
- 그래프 저장 구조
- 인접 행렬
- 구조
- 2차원 배열로 표현
matrix[u][v] = 1 또는 가중치 matrix[u][v] = 0 (연결 없음) - 특징
- 메모리 : O(V²)
- 간선 존재 확인 : O(1)
- 인접 노드 탐색 : O(V)
- 방향 그래프 : 가능
- 가중치 : 가능
- 공간 낭비 : 그래프가 희소하면(간선이 적으면) 매우 낭비 큼
- 장점
- 특정 간선 존재 여부 체크 빠름 O(1)
- 구현 단순
- DP/플로이드 워셜 처럼 V가 작고 전처리가 많은 그래프에서 좋음
- 단점
- 노드 수가 10만 이상이면 메모리 폭발
- 거의 모든 실전 문제에서는 사용 불가
- 사용처
- 플로이드 워셜
- 작은 그래프 ( V ≤ 2000 )
- 비트 마스크 DP에서 상태 인접성 저장할 때
- 구조
- 인접 리스트
- 구조
- 각 정점이 자신의 인접 정점 리스트를 가짐
- 특징
- 메모리 : O(V+E)
- 인접 탐색 : O(연결된 간선 수) → 평균 매우 빠름
- 희소그래프에 최적
- 가중치 가능
- 방향그래프 가능
- 장점
- 메모리 효율 최고
- BFS/DFS/Dijkstra 등 대부분의 그래프 문제에서 표준
- 단점
- 간선 존재 여부 체크는 느림 : O(deg(v))
- 사용처
- BFS/DFS/다익스트라/MST(프림, 크루스칼) 등
- 구조
- 간선 리스트
- 구조
- 간선만 저장하는 형태
- 특징
- 메모리 : O(E)
- 간선 순회 : O(E)
- 인접성 탐색 : 느림
- 가중치 가능
- 방향 그래프 가능
- 장점
- 정렬이 필요할 때 최강 (크루스칼)
- 메모리 효율 좋음
- 단점
- 인접 노드 탐색 분리
- 사용처
- 크루스칼
- 입력이 간선 형태로 주어질 때
- 그래프 재가공/필터링
- 구조
-
희소 그래프 / 밀집 그래프
타입 정의 추천 구조 희소 그래프 E ≪ V² 인접 리스트 밀집 그래프 E ≈ V² 인접 행렬
- 인접 행렬