list vs vector?
std::list 와 std::vector의 차이점?
List
- 이중 연결 리스트를 기반으로
각 데이터가 ‘노드’로서 포인터로 연결됨
struct Node {
Node* prev;
Node* next;
T value;
};
- 전/후 를 가리키는 노드 포인터를 사용하여
‘이전/다음’ 노드를 확인 가능
- 보통 ‘끊김’을 nullptr 등으로 확인
- 보통 ‘끊김’을 nullptr 등으로 확인
-
이러한 노드들은 각각이 ‘별도’로 생성되기에
Heap 영역에 각각 흩어져 있음 - 삽입/삭제는 위치를 안다는 가정하에 O(1)이 가능
- prev와 next의 연결을 끊은 후,
그 사이에 새로운 Node를 연결하는 방식으로
포인터 대입 연산을 4번 정도 하는 편
- prev와 next의 연결을 끊은 후,
- 다만, 직접적인 ‘i’번째 인덱스 접근이 안되며
앞 or 뒤에서 차례대로 접근(O(N))
- 그렇기에 사실상 ‘접근’이
탐색이 됨 - 그렇기에 위쪽의 삽입/삭제 위치를 모른다면
O(N)이 되어버린다
- 그렇기에 사실상 ‘접근’이
-
보통 다음 노드를 가리키는 포인터가 필요하기에
추가적인 메모리가 필요함 -
작은 데이터들이 여러 공간에 흩어져 있게 되어
메모리 파편화가 발생할 수 있음 -
삭제 연산 등을 진행하여도 기본적으로
prev/next 자체가 변동되지는 않기에
‘안전한 삽입/삭제’를 원하는 경우라면 고려 가능 - 단일로 사용하기 보다는 커스텀 자료구조 등에서
iterator에 대한 ‘안정성’이 필요한 경우 고려 가능
Vector
- 동적 배열로서
하나의 ‘큰 메모리’에 원소들이 차례대로 저장
class Vector{
T* data; // 데이터 시작
size_t size; // 현재 원소 개수
size_t capacity; // 현재 할당된 전체 칸수
};
- capacity를 넘어서게 되면
새로운 할당 가능 영역을 찾아 Heap 영역을 배회,
이후, 기존 원소들을 이동시키고 원래 있던 곳은 delete
- TMI : 보통 새로운 capacity는 이전의 2배로 잡음
- reserve() 로 초기 vector의 사이즈를 잡아두는 것이 최적화에 도움이 되는 이유
(첫 동적 할당을 필요한 만큼 잡아 재할당 가능성을 적게 함)
- TMI : 보통 새로운 capacity는 이전의 2배로 잡음
- ‘연속된 메모리’에 존재하기에
임의의 인덱스 접근이 O(1)
- ‘연속된 메모리’에 존재하므로 캐시가 긁어올 때
한번에 긁어와서 캐시 친화적인 자료 구조
- ‘연속된 메모리’에 존재하므로 캐시가 긁어올 때
- 다만 ‘삽입/삭제’는 기본적으로 O(N)
- 맨 뒤의 추가/삭제는 O(1)이나
그 외의 경우는 데이터를 ‘당겨야’ 함 - TMI : 예외적으로 ‘삭제’를 O(1)로 만드는 경우도 존재는 함
- 삭제 위치의 값을 ‘back()’ 값을 집어넣어 버리고
맨 뒤의 값을 pop_back()
다만 이 경우, 기존 vector의 정렬 순서가 깨지는 단점이 존재함
- 삭제 위치의 값을 ‘back()’ 값을 집어넣어 버리고
- 삽입/삭제 가 진행된 경우
Iterator 들의 안정성이 보장되지 않는 점에 유의
- 맨 뒤의 추가/삭제는 O(1)이나
- 대부분의 상황에서 범용적으로 선택되는 자료구조
정리 표
| 관점 | std::vector |
std::list |
|---|---|---|
| 내부 구조 | 동적 배열 | 이중 연결 리스트 |
| 메모리 배치 | 연속 | 노드별 따로 |
| 캐시 성능 | 매우 좋음 | 나쁨 |
| 임의 접근 | O(1) 가능 ([i]) |
불가(O(n)) |
| 끝 삽입/삭제 | 평균 O(1) | O(1) |
| 중간 삽입/삭제 | O(n) (원소 이동) | O(1) (iterator 알고 있을 때) |
| 메모리 오버헤드 | 작음 | 큼 (prev/next 포인터, 파편화) |
| iterator 안정성 | 재할당/중간 삽입 시 대량 무효 | 해당 노드만 제외하고 대부분 안정적 |
| 실제 추천도 | 기본 선택지 | 정말 특수한 경우에만 고려 |
TMI : 메모리 단편화?
-
메모리 총량은 남아 있으나, ‘연속된 공간으로 쪼개져’
실제로는 못쓰는 현상 -
분명 충분한 메모리 량이 있음에도
할당이 실패하는 현상이 발생한다면 단편화를 의심해 보아야 함
외부 단편화
빈 공간들이 서로 떨어져 있어서 큰 연속 블록 할당이 불가능한 상태
[ 8K Free ][ 4K used ][ 8K Free ][ 4K used ][ 8K Free ]
-
Free의 총 용량은 24K이나
16k 할당은 불가능한 상황! -
동적 할당 & 해제의 반복 구조에서 생기게 됨
내부 단편화
할당은 되었으나, 실제 사용량보다
더 큰 할당량을 가져가서
불필요한 공간 낭비가 발생
요청: 30 bytes
할당단위: 64 bytes
=> 실제 34 bytes 낭비
- ‘고정 사이즈 할당’ 시스템이라면 발생
대응 전략
- ‘연속된 메모리’ 사용 권장
- ex) vector
- ex) vector
- 사용 영역을 잡아두기
- 재할당 횟수 감소
- 메모리 이동과 단편화를 완화시킴
- 재할당 횟수 감소
vec.reserve(10000);
-
- Pool 기법
- 필요한 객체들을 ‘미리 생성’해두고
이들을 ‘제거’하지 않고 재사용 하는 방식
- Pool 기법
정리표
| 구분 | 외부 단편화 | 내부 단편화 |
|---|---|---|
| 원인 | 빈 블록들이 흩어짐 | 할당 단위가 실제 요구보다 큼 |
| 결과 | 큰 연속 블록 요청 실패 | 메모리 낭비 |
| 심각도 | 치명적 | 보통 |
| 발생 주체 | list, new/delete 남발 | alignment, pool |
| 대응 | vector + pool | 적절한 size-class |
TMI 2 : 복잡도
- 입력한 데이터에 ‘늘어남’에 따라 다음의 요소가 얼마나 늘어나는지 측정
- 시간 복잡도 : ‘실행 시간’
- 공간 복잡도 : ‘필요 공간’
- 시간 복잡도 : ‘실행 시간’
보통 빅 O 표기법으로 표현하며
알고리즘의 성능을 나타내는 척도
-
- 빅 O 표기법
- ‘대략 그 함수 정도’ (order of the function)이란 의미
알고리즘의 실행 시간을 일반화하여 표기하며
‘실행 시간 증가’에 가장 큰 영향을 미치는 부분만 적용
- 빅 O 표기법
TMI 3 : list.sort()?
-
list는 노드 기반의 컨테이너이기에
std::sort를 사용 불가
iterator를 기반으로 ‘연속 메모리 접근’이 가능한
자료구조만 sort를 사용 가능함 -
그렇기에 자체적인 sort 멤버를 가지고
내부적으로 병합 정렬을 사용 -
std::sort 사용이 가능하려면
‘Random Access Iterator’를 가져
임의의 위치로 건너띄는 연산이 가능해야 함
sort 사용이 가능한 경우
| 컨테이너 | 사용 가능 | 이유 |
|---|---|---|
| std::vector | ✅ | 연속 메모리 → Random Access |
| std::deque | ✅ | 분할 저장이지만 RA 지원 |
| std::array | ✅ | 고정 배열 → RA |
| C 배열 | ✅ | Pointer = Random Access Iterator |
| C-style array pointer range | ✅ | 동일 |
사용이 불가능한 경우
| 컨테이너 | 불가 이유 |
|---|---|
| std::list | Bidirectional → 점프 불가 |
| std::forward_list | Forward only |
| std::set / multiset | Iterator category OK여도 구조상 “정렬 상태 유지” + 직접 요소 이동 불가 |
| std::map / multimap | Key immutable (정렬 구조 변경 불가) |
| std::unordered_set/map | 순서 없음 + 해시 기반 |
댓글남기기