반응형

알고리즘

1. C++ 표준 라이브러리의 알고리즘은 코드의 복잡도를 줄이고 효율성을 높일 수 있으며 다양한 작업을 단순화하고 최적화할 수 있다.

 

2. 일반 함수로 제공되며 컨테이너의 구조와 상관없이 독립적으로 동작한다.

알고리즘 종류

1. 정렬

1) 데이터를 정렬하는 이유는 처리 속도를 올리기 위해서이다. 데이터를 저장하고 저장된 데이터를 빠르게 찾으려고 데이터를 정렬한다.

1.1 퀵 정렬(std::sort)

1) n개의 데이터를 정렬한다고 가정했을 때 최악의 경우에는 이차 시간[O(n^2)] 번의 비교를 수행하지만, 평균적으로 지수 로그 시간[O(n log n)] 번의 비교를 수행한다.

 

2) std::array나 std::vector 처럼 임의 접근 반복자(index를 통한 접근으로 이해하면 쉽다)를 지원하는 컨테이너를 대상으로만 사용할 수 있기 때문에 std::list처럼 원소에 차례로 접근해야하는 컨테이너는 사용할 수 없다.

 

  • (기본) 오름차순 정렬
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

// 벡터의 원소를 순서대로 출력
template <typename T>
void print_vector_all(vector<T>& vec) {
  for (typename vector<T>::iterator it = vec.begin(); it != vec.end(); it++) {
    cout << *it << " ";
  }
  cout << endl;
}

int main() {
  vector<int> vec = { 7, 6, 3, 5, 4, 1, 2, 0, 8 };

  // 벡터의 원소를 오름차순으로 정렬
  sort(vec.begin(), vec.end());
  print_vector_all(vec);

  return 0;
}

/*
출력 결과

0 1 2 3 4 5 6 7 8
*/
  • 내림차순 정렬
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

template <typename T>
void print_vector_all(vector<T>& vec) {
  for (typename vector<T>::iterator it = vec.begin(); it != vec.end(); it++) {
    cout << *it << " ";
  }
  cout << endl;
}

int main() {
  vector<int> vec = { 7, 6, 3, 5, 4, 1, 2, 0, 8 };

  // 벡터의 원소를 내림차순으로 정렬
  sort(vec.begin(), vec.end(), greater<int>());
  print_vector_all(vec);

  return 0;
}

/*
출력 결과

8 7 6 5 4 3 2 1 0
*/
  • 사용자 정의 기준으로 정렬(나이를 기준으로 오름차순 정렬)
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>

using namespace std;

struct Person {
  string name;   // 이름
  int age;       // 나이
  float height;	 // 키
  float weight;	 // 몸무게
};

void print_preson_all(vector<Person>& vec) {
  for (vector<Person>::iterator it = vec.begin(); it != vec.end(); it++) {
    cout << "이름: " << it->name << "\t > " << "나이: " << it->age << ", " << "키: " << it->height << ", " << "몸무게: " << it->weight << endl;
  }
}

// Person 구조체의 나이 비교(첫 번째 인자의 나이가 작으면 true)
bool compare(const Person& lhs, const Person& rhs) {
  return lhs.age < rhs.age;
}

int main() {
  Person p[5] = {
      {"Brain", 24, 180, 70},
      {"Jessica", 22, 165, 55},
      {"James", 30, 170, 65},
      {"Tom", 12, 155, 46},
      {"Mary", 18, 172, 62}
  };

  vector<Person> vec;
  vec.push_back(p[0]);
  vec.push_back(p[1]);
  vec.push_back(p[2]);
  vec.push_back(p[3]);
  vec.push_back(p[4]);

  cout << "-----정렬 전-----" << endl;
  print_preson_all(vec);
  cout << endl;

  // 사용자 정의 기준으로 정렬
  sort(vec.begin(), vec.end(), compare);

  cout << "-----정렬 후-----" << endl;
  print_preson_all(vec);

  return 0;
}

1.2 안정 정렬(std::stable_sort)

1) 순서의 안정성이 필요할 때 사용하며, 같은 원소가 정렬 후에도 원본의 순서와 일치한다.

 

2) 사용 방법은 sort 함수(불안정 정렬 함수)와 같다.

 

3) 시간 복잡도 측면에서 sort 함수보다 느리다.

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>

using namespace std;

bool compare_pairs(const pair<int, string>& a, const pair<int, string>& b) {
  return a.first < b.first;
}

int main() {
  vector<pair<int, string>> pairs = {
    {5, "apple"},
    {2, "orange"},
    {5, "banana"},
    {3, "grape"}
  };

  // std::stable_sort를 사용하여 pair를 첫 번째 요소에 따라 안정 정렬
  stable_sort(pairs.begin(), pairs.end(), compare_pairs);

  // 정렬된 결과 출력
  for (vector<pair<int, string>>::const_iterator it = pairs.begin(); it != pairs.end(); ++it) {
    const pair<int, string>& pair = *it;
    cout << pair.first << ": " << pair.second << endl;
  }

  return 0;
}

/*
출력 결과

2: orange
3: grape
5: apple
5: banana
*/

1.3 부분 정렬(std::partial_sort)

1) 컨테이너의 모든 원소가 아닌 일정 구간만 정렬할 때 사용한다.

 

2) 매개변수가 first, middle, last 로 구성되어 있다.

  • first : 정렬을 시작할 범위의 시작을 가리키는 반복자
  • middle : 정렬을 원하는 범위의 끝을 가리키는 반복자. 정렬 후에 이 반복자가 가리키는 위치 이전의 원소들은 정렬되지만, 이후의 원소들은 순서가 보장되지 않음
  • last : 정렬을 종료할 범위의 끝을 가리키는 반복자
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main() {
  vector<int> numbers = { 7, 2, 5, 1, 8, 9, 3, 6, 4 };

  // numbers 벡터에서 가장 작은 3개의 요소만 정렬
  partial_sort(numbers.begin(), numbers.begin() + 3, numbers.end());

  // 정렬된 결과 출력
  for (vector<int>::const_iterator it = numbers.begin(); it != numbers.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;

  return 0;
}

/*
출력 결과

1 2 3 7 8 9 5 6 4
*/

2. 찾기(std::find)

1) 순차 컨테이너에서 원하는 값을 찾는 함수이다.

 

2) 선형 검색을 기반으로 동작한다. 따라서 선형 시간[O(n)]이 걸린다.

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main() {
  vector<int> numbers = { 1, 2, 3, 4, 5 };

  cout << "찾고 싶은 숫자를 입력하세요: ";
  int target;
  cin >> target;

  // 입력받은 숫자를 찾아서 해당 위치를 반환
  vector<int>::iterator it = find(numbers.begin(), numbers.end(), target);

  if (it != numbers.end()) {
    cout << "찾은 위치: " << distance(numbers.begin(), it) << endl;
  }
  else {
    cout << "찾을 수 없음" << endl;
  }

  return 0;
}

/*
출력 결과

찾고 싶은 숫자를 입력하세요: 3
찾은 위치: 2
*/
  • 사용자 정의 클래스에  == 연산자 오버로딩 예시
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>

using namespace std;

class my_class {
public:
  int value;
  string name;

  // == 연산자 오버로딩
  bool operator==(const my_class& other) const {
    return value == other.value && name == other.name;
  }
};

int main() {
  vector<my_class> objects = { {1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}, {5, "five"} };

  // my_class 클래스의 모든 멤버 변수까지 같은 객체를 찾음
  vector<my_class>::iterator it = find(objects.begin(), objects.end(), my_class{ 3, "three" });

  if (it != objects.end()) {
    cout << "찾은 위치: " << distance(objects.begin(), it) << endl;
  }
  else {
    cout << "찾을 수 없음" << endl;
  }

  return 0;
}

/*
출력 결과

찾은 위치: 2
*/

3. 이진 탐색(std::binary_search)

1) 정렬된 범위에서 특정 값을 이진 탐색으로 찾는 함수이다. 탐색 볌위를 반으로 나눠 범위를 좁혀가는 방식으로 동작하는 알고리즘이다.

 

2) 정렬되지 않은 상태에서 binary_search 함수를 사용하면, 검색 결과가 불확실하거나 검색을 아예 실패 할 수 있다.

 

3) 이진 탐색 알고리즘을 사용하므로 시간 복잡도는 로그 시간[O(log n)] 이다

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main() {
  // 정렬되지 않은 벡터
  vector<int> numbers = { 8, 3, 1, 7, 4, 5, 9, 2, 6 };

  // 정렬 수행
  sort(numbers.begin(), numbers.end());

  // 사용자에게 숫자 입력 받기
  int target;
  cout << "컨테이너에서 검색하고 싶은 숫자를 입력해보세요: ";
  cin >> target;

  // 이진 탐색
  bool found = binary_search(numbers.begin(), numbers.end(), target);

  if (found) {
    cout << "찾았습니다." << endl;
  }
  else {
    cout << "저장되어 있지 않습니다." << endl;
  }

  return 0;
}

/*
출력 결과

컨테이너에서 검색하고 싶은 숫자를 입력해보세요: 6
찾았습니다.
*/

알고리즘 비교

고리즘 사용 가능한 컨테이너 요구 반복자 종류 시간 복잡도 정렬/탐색 안정성  특징
std::sort array, vector, deque (연속적 메모리) Random Access Iterator 평균 O(N log N)
최악 O(N log² N)
❌ 불안정 (unstable sort) 빠른 정렬, 내부적으로 introsort(quick + heap + insertion)
std::stable_sort array, vector, deque Random Access Iterator 평균/최악 O
(N log² N)
✅ 안정적 (stable sort) 동일 값의 상대적 순서 유지
std::partial_sort array, vector, deque Random Access Iterator O(N log M)
(M = 정렬할 원소 개수)
❌ 불안정 전체 정렬이 필요 없을 때 효율적 (예: 상위 10개만 정렬)
std::find 모든 순차 컨테이너 및 연관 컨테이너
array, vector, deque, list, forward_list, set, map, unordered_* 등
Input Iterator 이상 O(N) 순차 탐색, 처음 일치하는 원소 반환
std::binary_search array, vector, deque(정렬된 컨테이너만 가능) Random Access Iterator O(log N) 정렬된 구간에서 이진 탐색 수행

 

반응형

'Programming Language > C++' 카테고리의 다른 글

자료 구조  (0) 2025.11.05
시간 복잡도  (0) 2025.11.05
컨테이너와 반복자  (0) 2025.11.05
용어 정리  (0) 2025.11.04
템플릿  (0) 2025.11.03
반응형
분류  자료구조 이름 구조적 형태 설명  장점  단점  사용 예시
선형 구조 (Linear Structure) 배열 (Array) 연속 메모리 인덱스로 원소 접근 가능, 고정 크기 인덱스로 빠른 접근(O(1)) 크기 고정, 중간 삽입/삭제 느림(O(N)) 고정 크기 데이터, 테이블, 버퍼
  연결 리스트 (Linked List) 비연속 노드 + 포인터 각 노드가 다음 노드를 가리킴 삽입/삭제 빠름(O(1)) 임의 접근 불가, 포인터 오버헤드 큐, 스택, 메모리 관리
  스택 (Stack) LIFO 구조 마지막에 들어온 데이터가 먼저 나감 구현 간단, 함수 호출 스택에 활용 중간 접근 불가 함수 호출, undo 기능
  큐 (Queue) FIFO 구조 먼저 들어온 데이터가 먼저 나감 순차적 처리 용이 중간 접근 불가 작업 대기열, 이벤트 처리
  덱 (Deque) 양쪽 입출력 가능 앞뒤 모두 삽입/삭제 가능 유연한 데이터 처리 임의 접근은 느림 스케줄링, 캐시 관리
비선형 구조 (Non-linear Structure) 트리 (Tree) 계층적 구조 부모-자식 관계 노드 집합 탐색/정렬 효율적 구현 복잡, 균형 유지 필요 계층 데이터, 파일 시스템, 이진 탐색
  이진 탐색 트리 (BST) 트리 (좌 < root < 우) 정렬된 자료 저장, 빠른 검색 탐색 O(log N) 불균형 시 성능 저하 O(N) map, set 등
  힙 (Heap) 완전 이진 트리 부모-자식 간 우선순위 규칙 유지 최대/최소값 빠른 접근 정렬에는 부적합 우선순위 큐, 힙 정렬
  그래프 (Graph) 노드 + 간선 관계(연결성)를 표현 복잡한 관계 모델링 가능 순회 복잡 네트워크, 경로 탐색
해시 기반 구조 (Hash-based) 해시 테이블 (Hash Table) 배열 + 체인 구조 해시 함수로 key → 인덱스 변환 평균 O(1) 탐색 충돌 발생 가능, 공간 낭비 unordered_map, 캐시
특수 구조 (Specialized) 집합 (Set) 트리 or 해시 기반 중복 없는 원소 집합 빠른 삽입/검색 순서 없음 중복 제거, 수학적 집합
  맵 (Map) 트리 or 해시 기반 key-value 쌍 저장 키 기반 빠른 검색 중복 불가 사전, DB 인덱스
  트라이 (Trie) 접두어 트리 문자열 검색에 특화 검색 O(L) (L=문자열 길이) 메모리 소모 많음 자동완성, 사전
  그래프 인접 행렬 / 리스트 행렬 or 리스트 그래프 표현 방식 빠른 연결 확인 메모리 사용량 차이 큼 네트워크, 경로 탐색
추상 자료형 (ADT) 리스트 (List) 순차적 데이터 집합 삽입/삭제/탐색 기능 유연한 인터페이스 구현 방식에 따라 성능 차이 vector, list
  우선순위 큐 (Priority Queue) 힙 기반 우선순위 높은 데이터 먼저 처리 최대/최소 빠르게 추출 임의 접근 불가 작업 스케줄링, AI 탐색
  집합(Set) / 맵(Map) 트리 or 해시 기반 고유한 키 기반 검색 효율적 탐색/삽입 순서/중복 제약 데이터베이스, 인덱스
반응형

'Programming Language > C++' 카테고리의 다른 글

알고리즘  (0) 2025.11.05
시간 복잡도  (0) 2025.11.05
컨테이너와 반복자  (0) 2025.11.05
용어 정리  (0) 2025.11.04
템플릿  (0) 2025.11.03
반응형
시간 복잡도 이름  설명  예시 입력 / 동작 예시
O(1) 상수 시간 (Constant Time) 입력 크기와 관계없이 항상 일정한 시간이 걸림 배열 인덱스 접근, 해시 테이블 조회 (unordered_map::find)
O(log N) 로그 시간 (Logarithmic Time) 입력이 커질수록 단계적으로 줄어드는 연산 (이진 탐색) 이진 탐색, 힙 삽입/삭제 (std::priority_queue), 트리 탐색
O(N) 선형 시간 (Linear Time) 입력 크기에 비례한 반복 수행 선형 탐색, 전체 합 계산
O(N log N) 준선형 시간 (Linearithmic Time) 분할정복 기반 알고리즘 (정렬 등) 병합 정렬, 퀵 정렬, 힙 정렬
O(N²) 이차 시간 (Quadratic Time) 두 중첩 반복문 사용 버블 정렬, 선택 정렬, 플로이드-워셜
O(2^N) 지수 시간 (Exponential Time) 입력 크기 증가 시 연산량이 폭발적으로 증가 부분집합 탐색, 브루트포스 재귀
O(N!) 팩토리얼 시간 (Factorial Time) 모든 경우의 수 탐색 (순열 등) 순열 생성, 외판원 문제(Brute force)

 

반응형

'Programming Language > C++' 카테고리의 다른 글

알고리즘  (0) 2025.11.05
자료 구조  (0) 2025.11.05
컨테이너와 반복자  (0) 2025.11.05
용어 정리  (0) 2025.11.04
템플릿  (0) 2025.11.03
반응형

컨테이너

1. 같은 타입의 여러 객체를 저장할 수 있는 묶음 단위의 데이터 구조이며, 주 역할은 데이터를 저장하고 관리하며 저장된 원소에 접근할 수 있는 멤버 함수를 제공한다.

 

2. 표준 템플릿 라이브러리의 컨테이너는 템플릿 클래스 형태로 제공된다.


순차 컨테이너

1. 원소들이 선형으로 배열된 데이터 구조이며, 각 원소는 위치(인덱스)에 따라 접근할 수 있다.

 

순차 컨테이너의 종류

1. 배열(std::array)

1) C++11 에서 소개된 array는 고정된 크기의 배열을 담는 컨테이너이다.

 

2) 일반 배열 변수가 포인터로 변환되었을 때 배열의 길이 정보를 알 수 없다는 문제와 동적 배열의 메모리 할당, 해제 문제에서 벗어날 수 있는 장점이 있다.

 

3) 초기화할 때 작성한 원소의 갯수보다 많은 값을 넣으려고 시도하면 컴파일 단계에서 오류가 발생한다.

 

4) 일반 배열 처럼 [] 연산자로 원소에 접근할 때 배열의 유효 범위 밖의 인덱스로 접근을 시도하면 런타임 오류가 발생한다. (런타임 오류를 방지하고자 at 함수를 사용하고 배열의 유효 범위를 벗어나는 인덱스로 접근을 시도할 경우 std::out_of_range 예외를 던진다.)

#include <iostream>
#include <array>

using namespace std;

int main() {
  // 크기가 5인 std::array 생성
  array<int, 5> myArray;

  // 배열 초기화
  myArray = { 1, 2, 3, 4, 5 };

  // 배열 출력
  cout << "배열 출력: ";
  for (int i = 0; i < 5; i++) {
    cout << myArray[i] << " ";
  }
  cout << endl;

  // 배열 크기 출력
  cout << "배열 크기: " << myArray.size() << endl;

  // 배열의 첫 번째 요소 출력
  cout << "첫 번째 요소: " << myArray[0] << endl;

  // 배열의 두 번째 요소 변경
  myArray[1] = 10;

  // 변경된 배열 출력
  cout << "변경된 배열: ";
  for (int i = 0; i < 5; i++) {
    cout << myArray[i] << " ";
  }
  cout << endl;

  return 0;
}

2. 벡터(std::vector)

1) 동적인 길이의 배열 컨테이너이며, 동적 배열 관리를 안전하게 수행해주기 때문에 표준 컨테이너 가운데 가장 많이 활용된다.

 

2) 벡터의 크기는 동적이므로 힙 메모리가 허용하는 한 계속 늘릴 수 있으며, 배열의 길이보다 항상 여유분을 포함한 메모리를 운영합니다.

 

3) 벡터는 원소의 삽입, 삭제시 밀리거나, 당겨져지며 복사 저장이 발생하기 때문에 삽입이나, 삭제가 빈번하게 발생하는 데이터일 경우 다른 컨테이너를 사용하는 것이 좋다.

 

4) 동적 배열로 구현되어 원소들은 연속된 메모리 공간에 저장된다. 따라서 임의 접근에 효율적이다.

 

5) 기존에 할당된 공간이 다차서 공간이 부족할 경우 더 큰 새로운 공간으로 이동하면서 원소의 갯수만큼 복사해야 한다. 따라서 원소의 갯수(n)만큼 시간 복잡도는 O(n)만큼 걸린다.

  • 삽입 삭제 예시
#include <iostream>
#include <vector>

using namespace std;

// 벡터의 모든 원소를 출력하는 함수
template <typename T>
void print_vector_all(vector<T>& vec) {
  cout << "벡터 내 원소 개수 : " << vec.size() << endl;

  // 벡터의 모든 원소 출력
  for (typename vector<T>::iterator it = vec.begin(); it != vec.end(); it++) {
    cout << *it << " ";
  }
  cout << endl << "---------------------" << endl;
}

int main() {
  vector<int> vec;
  vec.push_back(0);
  vec.push_back(10);
  vec.push_back(20);
  vec.push_back(30);
  vec.push_back(40);

  cout << "원본" << endl;
  print_vector_all(vec);

  // 벡터에 원소 삽입
  vec.insert(vec.begin() + 3, 25);    // vec[3] 앞에 25 추가
  cout << "insert 결과" << endl;
  print_vector_all(vec);

  // 벡터에서 원소 삭제
  vec.erase(vec.begin() + 3);         // vec[3] 제거
  cout << "erase 결과" << endl;
  print_vector_all(vec);

  return 0;
}
  • 값을 변결할 수 없는 정적 반복자 예시
#include <iostream>
#include <vector>

using namespace std;

int main() {
  vector<int> vec;
  vec.push_back(0);
  vec.push_back(10);
  vec.push_back(20);
  vec.push_back(30);
  vec.push_back(40);

  vector<int>::const_iterator const_it = vec.cbegin();
  cout << *const_it << endl;

  ++const_it;
  cout << *const_it << endl;

  const_it = vec.cend();
  --const_it;
  cout << *const_it << endl;
  
  //*const_it = 100;  // 값을 변경할 경우 컴파일 오류 발생
  return 0;
}
  • 거꾸로 동작하는 리버스 반복자 예시
#include <iostream>
#include <vector>

using namespace std;

int main() {
  vector<int> vec;

  for (int i = 0; i < 3; i++)
    vec.push_back(i);

  for (vector<int>::reverse_iterator it = vec.rbegin(); it != vec.rend(); it++)
    cout << *it << endl;

  return 0;
}

3. 리스트(std::list)

1) 연결 리스트(linked list)라는 데이터 구조를 구현한 컨테이너이며, 이중 연결 리스트로 구현되어 각 원소에는 다음 원소와 이전 원소를 가리키는 포인터가 있다. 따라서 데이터를 삽입하거나 삭제하는 데 효율적이며, 순서를 유지할 수 있다.

 

2) 임의 접근이 불가능하며 원소에 접근하려면 이전 원소들을 차례로 따라가야 한다.

#include <iostream>
#include <list> 

using namespace std;

int main() {
	list<int> myList;

	// 리스트 뒤쪽에 값 추가
	myList.push_back(2);
	myList.push_back(3);
	myList.push_back(4);

	// 리스트 앞쪽에 값 추가
	myList.push_front(1);
	myList.push_front(0);

	// 리스트 출력
	cout << "리스트 출력: ";
	for (const int& value : myList) {
		cout << value << " ";
	}

	cout << endl;

	myList.pop_front();   // 첫 번째 요소 제거
	myList.pop_back();  // 마지막 요소 제거

	// 삭제 후 리스트 출력
	cout << "삭제 후 리스트 출력: ";
	for (const int& value : myList) {
		cout << value << " ";
	}
	cout << endl;

	// 리스트 크기 확인
	cout << "리스트 크기: " << myList.size() << endl;

	// 리스트가 비었는지 확인
	cout << "리스트가 비었는가? " << (myList.empty() ? "예" : "아니오") << endl;

	return 0;
}

4. 덱(std::deque)

1) 벡터 컨테이너와 유사한 구조를 가지고 있으며, 멤버함수도 비슷하다.

 

2) 벡터와 달리 여러 개의 메모리 블록을 나눠서 저장하는 것이 특징이며, 저장할 공간이 부족하면 일정한 크기의 새로운 메모리 블록을 만들어서 연결한다. 따라서 벡터 컨테이너에서 발생하는 복사 저장이 발생하지 않는다. 즉 벡터의 단점을 보완하는 컨테이너이다.

 

3) 원소의 삽입과 삭제가 양쪽 끝에서 빈번하게 발생하는 경우 리스트(list)보다 효율적이다.

#include <iostream>
#include <deque>

using namespace std;

int main() {
	deque<int> myDeque;

	// 덱 뒤쪽에 값 추가
	myDeque.push_back(2);
	myDeque.push_back(3);
	myDeque.push_back(4);

	// 덱 앞쪽에 값 추가
	myDeque.push_front(1);
	myDeque.push_front(0);

	// 덱 출력
	cout << "deque 출력: ";
	for (const int& value : myDeque) {
		cout << value << " ";
	}
	cout << endl;

	myDeque.pop_front();   // 첫 번째 요소 제거
	myDeque.pop_back();  // 마지막 요소 제거

	// 삭제 후 덱 출력
	cout << "삭제 후 deque 출력: ";
	for (const int& value : myDeque) {
		cout << value << " ";
	}
	cout << endl;

	// 덱 크기 확인
	cout << "deque 크기: " << myDeque.size() << endl;

	// 덱이 비었는지 확인
	cout << "deque이 비었는가? " << (myDeque.empty() ? "예" : "아니오") << endl;

	// 덱의 첫 번째와 마지막 원소 출력
	cout << "deque 첫 번째 원소: " << myDeque.front() << endl;
	cout << "deque 마지막 원소: " << myDeque.back() << endl;

	return 0;
}

 

순차 컨테이너 비교

컨테이너  메모리 저장 위치  메모리 구조 / 관리 방식  임의 접근 (random access) 앞 삽입 / 삭제 뒤 삽입 / 삭제 중간 삽입 / 삭제 비고
🟦 std::array 스택(Stack) (기본적으로 자동 변수일 경우) ※ 동적할당 시 힙 고정 크기 배열 (연속된 메모리 블록) ✅ O(1) ❌ 불가능 (크기 고정) ❌ 불가능 ❌ 불가능 컴파일 타임에 크기 결정 (std::array<int, N>)
🟩 std::vector 힙(Heap) 동적 배열 (연속된 메모리, 필요시 재할당) ✅ O(1) ❌ O(N) ✅ amortized O(1) / 삭제 O(1) ❌ O(N) 재할당 시 포인터 무효화
🟨 std::deque 힙(Heap) 분할된 블록 배열 (chunk 구조) ✅ O(1) (약간 느림) ✅ O(1) ✅ O(1) ❌ O(N) 중간 접근 느림, double-ended queue
🟧 std::list 힙(Heap) 이중 연결 리스트 (노드 단위 분산) ❌ O(N) ✅ O(1) ✅ O(1) ✅ O(1) (iterator 위치에서) 임의 접근 불가, 메모리 비연속

연관 컨테이너

1. 데이터를 저장하고 특정 키를 사용하여 데이터를 효율적으로 검색할 수 있다.

 

2. 내부 구조는 트리나 해시 테이블과 같은 최적화된 데이터 구조를 사용하므로 검색 작업의 시간 복잡도가 일반적으로 로그 시간[O(logN)] 또는 상수 시간[O(1)]에 가깝다.

 

3. 대량의 데이터에서도 빠르게 검색할 수 있으며, 실시간 또는 반응형 시스템에서 매우 유용한다.

연관 컨테이너의 종류

1. 세트(std::set)

1) 고유한 값을 정렬된 순서로 저장한다.

 

2) 데이터 삽입(insert 함수), 데이터 삭제(erase, clear 함수), 데이터 검색(find 함수), 특정 데이터의 갯수 검색(count 함수), 전체 데이터의 갯수 검색(size 함수)의 주요 함수를 제공한다.

 

3) 내부적으로 이진 탐색 트리를 사용하여 원소를 저장하고 정렬된 순서(기본 오름차순)를 유지하는 정렬 컨테이너이기도 하다.

 

4) 이진 탐색 트리의 특성 덕분에 삽입, 삭제, 검색 작업의 시간 복잡도가 로그 시간[O(log N)]에 가깝다

#include <iostream>
#include <set>

using namespace std;

int main() {
  set<int> mySet;

  // 값 삽입
  mySet.insert(5);
  mySet.insert(2);
  mySet.insert(8);

  // 값 5가 세트에 있는지 확인
  if (mySet.find(5) != mySet.end()) {
    cout << "5는 set에 저장되어 있음" << endl;
  }

  // 세트를 순회하며 값 출력
  for (auto it = mySet.begin(); it != mySet.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;

  // 세트의 크기 출력
  int size = mySet.size();
  cout << "set 크기: " << size << endl;

  return 0;
}

2. 멀티 세트(std::multiset)

1) 정렬된 세트 자료 구조를 구현하는 연관 컨테이너이지만, 중복된 값을 포함한다는 차이가 있다.

 

2) 데이터를 삭제(erase 함수)할 때 중복된 값이 있다면 모두 제거된다.

#include <iostream>
#include <set>

using namespace std;

int main() {
  multiset<int> myMultiset;

  // 값 삽입
  myMultiset.insert(5);
  myMultiset.insert(2);
  myMultiset.insert(5);   // 중복된 값도 저장됨

  // 특정 값의 개수 출력
  int count = myMultiset.count(5);
  cout << "저장되어 있는 5의 갯수: " << count << endl;

  // 멀티 세트를 순회하며 값 출력
  for (auto it = myMultiset.begin(); it != myMultiset.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;

  // 멀티 세트의 크기 출력
  int size = myMultiset.size();
  cout << "multiset 크기: " << size << endl;

  return 0;
}

3. 맵(std::map)

1) 키 - 값 쌍을 저장하는 연관 컨테이너이다. 각 키는 고유해야하며 키를 기준으로 정렬된 순서로 저장된다.

 

2) 특정 키에 연관된 값을 검색하여, 값을 추가, 수정, 제거 등의 작업에 주로 사용된다.

 

3) 데이터 삽입(insert 함수)시 키 와 값을 묶어 std::pair 객체를 만들어주는 std::make_pair 함수를 사용한다.

 

4) 데이터 삭제(erase 함수)시 키 값만 넣어주면 삭제된다.

 

5) 특정 키로 검색(find 함수)하면 맵에서 특정 키에 해당하는 반복자를 반환하며, 이 반복자로 키에 해당하는 원소를 찾을 수 있다.

#include <iostream>
#include <string>
#include <map>

using namespace std;

int main() {
	map<string, int> scores;

	// 키-값 쌍 삽입
	scores.insert(make_pair("Bob", 85));
	scores.insert(make_pair("Jane", 90));
	scores.insert(make_pair("Tom", 70));

	// 맵 크기 출력
	cout << "map 크기: " << scores.size() << endl;

	// 특정 키에 해당하는 값 검색
	auto it = scores.find("Bob");
	if (it != scores.end()) {
		cout << "Bob의 점수 검색 결과: " << it->second << endl;
	}
	else {
		cout << "Bob의 점수는 저장되어 있지 않음" << endl;
	}
	cout << endl;

	// 특정 키에 해당하는 키-값 제거
	scores.erase("Bob");  //Bob 정보 제거

	// 맵 크기 출력
	cout << "Bob 정보 제거 후, map 크기: " << scores.size() << endl << endl;

	// 맵을 순회하며 모든 키와 값 출력
	cout << "---map 모든 원소 출력---" << endl;
	for (const auto& pair : scores) {
		cout << pair.first << ": " << pair.second << endl;
	}

	return 0;
}

4. 멀티 맵(std::multimap)

1) 키 - 값 쌍으로 구성된 맵을 구현하는 연관 컨테이너이지만, 중복된 키을 허용한다.

 

2) 내부적으로 키를 기준으로 정렬(기본 오름차순)되고 정렬 기준을 정의할 수 있다.

 

3) 이진 탐색 트리 자료 구조를 기반으로 구현되어 검색, 삽입, 삭제 등의 작업을 효율적으로 수행한다.

#include <iostream>
#include <string>
#include <map>

using namespace std;

int main() {
	multimap<string, int> scores;

	// 멀티 맵에 원소(키-값 쌍) 삽입
	scores.insert(make_pair("Bob", 85));
	scores.insert(make_pair("Jane", 90));
	scores.insert(make_pair("Tom", 70));
	scores.insert(make_pair("Bob", 100));  // 중복 키 허용

	// 멀티 맵의 크기를 출력
	cout << "multimap 크기: " << scores.size() << endl;

	// 특정 키에 해당하는 원소의 개수 구하기
	int count = scores.count("Bob");
	cout << "저장되어 있는 Bob 점수의 갯수: " << count << endl;

	// 특정 키를 가진 원소의 범위 구하기
	auto range = scores.equal_range("Bob");
	if (range.first != scores.end()) {
		cout << "Bob의 모든 점수: ";
		for (auto it = range.first; it != range.second; ++it) {
			cout << it->second << " ";
		}
		cout << endl;
	}
	else {
		cout << "Bob의 점수는 저장되어 있지 않음" << endl;
	}
	cout << endl;

	// 특정 키에 해당하는 모든 원소 삭제
	scores.erase("Bob");  //Bob 정보 제거

	// 멀티 맵의 크기 출력
	cout << "Bob 정보 제거 후, multimap 크기: " << scores.size() << endl << endl;

	// 멀티 맵을 순회하며 모든 키와 값 출력
	cout << "---multimap 모든 원소 출력---" << endl;
	for (const auto& pair : scores) {
		cout << pair.first << ": " << pair.second << endl;
	}

	return 0;
}

연관 컨테이너 비교

컨테이 내부 자료구조 메모리 저장 위치 메모리 구조 방식 중복 허용 정렬 여부 검색 시간 복잡도 삽입 시간 복잡도 삭제 시간 복잡도 특징 요약
🟩 std::set Red-Black Tree (균형 이진 탐색 트리) 힙(Heap) 노드 기반 연결 구조(key + 좌/우/부모 포인터) ❌  ✅ 자동 정렬 O(log N) O(log N) O(log N) 중복 없는 정렬된 key 집합
🟦 std::multiset Red-Black Tree 힙(Heap) 노드 기반 연결 구조 ✅  ✅ 자동 정렬 O(log N) O(log N) O(log N) 동일 key 여러 개 가능
🟥 std::map Red-Black Tree 힙(Heap) 노드 기반 연결 구조(key + value 쌍을 노드로 저장) ❌  ✅ key 기준 자동 정렬 O(log N) O(log N) O(log N) (key, value) 쌍 저장, 키는 유일
🟨 std::multimap Red-Black Tree 힙(Heap) 노드 기반 연결 구조 ✅  ✅ key 기준 자동 정렬 O(log N) O(log N) O(log N) 같은 키에 여러 value 가능
🟧 std::unordered_set Hash Table (버킷 + 체인 구조) 힙(Heap) 배열 + 연결 리스트(또는 오픈 어드레싱) ❌ 비정렬 평균 O(1)최악 O(N) 평균 O(1)최악 O(N) 평균 O(1)최악 O(N) 중복 없는 비정렬 해시 기반 집합
🟪 std::unordered_multiset Hash Table 힙(Heap) 배열 + 체인 연결 구조 ✅  ❌ 비정렬 평균 O(1)최악 O(N) 평균 O(1)최악 O(N) 평균 O(1)최악 O(N) 중복 허용 비정렬 해시 기반 집합
🟫 std::unordered_map Hash Table (key → value) 힙(Heap) 버킷 배열 + 체인 연결 구조 ❌  ❌ 비정렬 평균 O(1)최악 O(N) 평균 O(1)최악 O(N) 평균 O(1)최악 O(N) (key,value) 해시 매핑, 순서 없음
⬛ std::unordered_multimap Hash Table 힙(Heap) 버킷 배열 + 체인 연결 구조 ❌ 비정렬 평균 O(1)최악 O(N) 평균 O(1)최악 O(N) 평균 O(1)최악 O(N) 동일 키 다수 value 가능

컨테이너 어댑터

1. 다른 컨테이너를 기반으로 새로운 기능을 제공하는 컨테이너이다. 즉, 기존의 컨테이너를 감싸서 새로운 인터페이스를 제공한다.

 

2. 반복자(iterator) 를 지원하지 않는다. 내부 구조 은닉. 반복자 없음

컨테이너 어댑터의 종류

1. 스택(std::stack)

1) C++ 표준 템플릿 라이브러리에서 제공하는 컨테이너 어댑터이다.

 

2) 스택 자료 구조를 구현하는데 사용된다.

 

3) std::stack 컨테이너 어댑터는 std::deque컨테이너를 기반으로 LIFO 자료 구조를 제공한다.

 

4) 스택은 후입선출(Last In First Out : LIFO)로 데이터를 꺼내며, 데이터를 임시로 저장하거나 역순으로 처리할 때 주로 사용한다.

 

5) 반복자(iterator) 를 지원하지 않는다.

 

6) 스택이 만들어진 주된 이유와 목적

  • 함수 호출 관리 : 함수가 호출될 때 스택에 호출 정보(지역 변수, 반환 주소 등)를 저정하고, 함수가 반환될 때 스택에서 이 정보를 제거하여 이전 함수로 복귀합니다. 이로써 함수 호출과 반환을 효율적으로 추적하고 관리할 수 있다.
  • 역순 처리 : 스택은 데이터나 작업을 역순으로 처리할 때 유용합니다. 데이터를 스택에 쌓고 나중에 역순으로 꺼내어 처리하면 역순 처리를 구현할 수 있다.
  • 컴파일러와 언어 구현 : 컴파일러와 인터프리터는 함수 호출과 반환을 처리하기 위해 스택을 사용한다. 프로그래밍 언어의 호출 스택을 관리하여 함수 호출과 반환을 지원한다.
#include <iostream>
#include <stack>

using namespace std;

int main() {
  stack<int> myStack;

  // 스택에 데이터 추가
  myStack.push(1);
  myStack.push(2);
  myStack.push(3);

  // 스택의 맨 위쪽 값 확인
  cout << "맨 위 요소: " << myStack.top() << endl;

  // 스택에서 데이터 제거(맨 위쪽 데이터 꺼내기)
  myStack.pop();
  cout << "맨 위 요소 제거 후, 새로운 맨 위 요소: " << myStack.top()
    << endl;

  // 스택의 크기(데이터 개수) 확인
  cout << "스택 크기: " << myStack.size() << endl;

  // 스택이 비었는지 확인
  if (myStack.empty()) {
    cout << "스택이 비었습니다." << endl;
  }
  else {
    cout << "스택은 비어 있지 않습니다." << endl;
  }

  return 0;
}

2. 큐(std::queue)

1) C++ 표준 템플릿 라이브러리에서 제공하는 컨테이너 어댑터이다.

 

2) 큐 자료 구조를 구현하는 데 사용된다.

 

3) std::queue 컨테이너 어댑터는 std::deque 컨테이너를 기반으로 FIFO 자료 구조를 제공한다.

 

4) 큐는 선입선출(First In First Out : FIFO)로 데이터를 꺼내며, 데이터의 순서를 보장하고 먼저 들어온 데이터가 먼저 처리되어야 하는 상황에서 효과적으로 사용된다.

 

5) 반복자(iterator) 를 지원하지 않는다.

 

6) 큐이 만들어진 주된 이유와 목적

  • 작업 대기열 : 프로세스 스케줄링에서 CPU가 실행할 프로세스를 대기열에 넣어두고 먼저 도착한 프로세스를 먼저 실행할 때에 사용한다.
  • 이벤트 처리 : 사용자 입력, 네트워크 메시지, 다양한 이벤트를 큐에 저장하고 차례로 처리할 때도 유용하다.
  • 멀티스레딩 환경에서 동기화 : 여러 스레드가 동시에 실행되는 상황에서 스레드 간에 작업이나 데이터 교환을 조절하기 위해 큐를 활용할 수 있다. 하나의 스레드에서 생성된 작업을 큐에 넣고 다른 스레드에서 꺼내어 처리하는 방식으로 스레드 간에 동기화를 간단하게 구현할 수 있다.
  • 캐시 구현 : 캐시에서 데이터를 저장하고 검색할 때 큐로 구현할 수 있다. 가장 먼저 들어온 데이터가 먼저 나가는 큐 구조를 사용하여 데이터를 관리한다.
#include <iostream>
#include <queue>

using namespace std;

int main() {
  queue<int> myQueue;

  // 삽입하기
  myQueue.push(1);
  myQueue.push(2);
  myQueue.push(3);

  cout << "큐의 맨 앞: " << myQueue.front() << endl;  // front
  cout << "큐의 맨 뒤: " << myQueue.back() << endl;   // back

  // 꺼내기
  myQueue.pop();

  cout << "pop 후 맨 앞: " << myQueue.front() << endl;  // pop 후 front
  cout << "pop 후 맨 뒤: " << myQueue.back() << endl;   // pop 후 back

  // 비었는지 확인하기
  cout << "큐가 비어 있나요? " << (myQueue.empty() ? "네" : "아니오") << endl;

  // 크기 구하기
  cout << "큐의 크기: " << myQueue.size() << endl;

  return 0;
}

컨테이너 어댑터 비교

구분  내부 자료구조(기본 컨테이너) 메모리 구조 주요 접근 방식  검색 (탐색) 시간복잡도  추가 (push) 시간복잡도 삭제 (pop) 시간복잡도
stack 기본적으로 std::deque 사용(옵션: std::vector, std::list 가능) 동적 힙 메모리 (내부 컨테이너에 따라 다름) LIFO (Last-In-First-Out) ❌ 직접 탐색 불가 (내부 반복자 미지원) O(1) (끝에 push) O(1) (끝에서 pop)
queue 기본적으로 std::deque 사용(옵션: std::list 가능) 동적 힙 메모리 FIFO (First-In-First-Out) ❌ 직접 탐색 불가 O(1) (뒤에 push) O(1) (앞에서 pop)

반복자

1. 객체지향 프로그래밍에서 배열 같은 컨테이너의 내부 원소들을 순회하는 객체이며, 포인터와 비슷한 동작을 한다고 볼 수 있다.

 

2. 표준 템플릿 라이브러리에서 모든 컨테이너는 각자의 반복자를 제공한다. 대표적으로 begin, end 함수가 있다.

 

3. 장점은 컨테이너의 내부 정보를 외부에 노출시키지 않고도 원소에 접근할 수 있다.

 

4. 반복자는 배열이나, 벡터, 리스트, 큐처럼 선형 컨테이너에 적합하지만, 뒤로 이동할 수 없는 컨테이너나 무한한 개수의 원소를 가진 컨테이너에는 반복자를 사용할 수 없다.

#include <iostream>
#include <vector>

using namespace std;

int main() {
  vector<int> vec;

  for (int i = 0; i < 5; i++)
    vec.push_back(i);

  for (vector<int>::iterator it = vec.begin(); it != vec.end(); it++)
    // 포인터처럼 역참조 연산자(*)를 이용하여 해당 원소에 접근할 수 있다.
    cout << *it << endl;

  return 0;
}
반응형

'Programming Language > C++' 카테고리의 다른 글

자료 구조  (0) 2025.11.05
시간 복잡도  (0) 2025.11.05
용어 정리  (0) 2025.11.04
템플릿  (0) 2025.11.03
객체지향 설계 원칙  (0) 2025.11.03
반응형
용어 설명
얕은 복사 복사 대상이 주솟값이므로 참조하는 값이 서로 같다. 따라서 원본 값이 바뀌면 복사한 값도 바뀐 값을 참조한다.
깊은 복사 서로 다른 독립적인 메모리에 실제 값가지 복사하는 것이다.(컨테이너의 여러개의 원소를 복사해야하는 경우 표준 라이브러리의 copy 함수를 활용하면 코드의 간결함과 성능까지 확보할 수 있다.)
난수 생성 1. <random> 헤더 파일의 32bit 버전의 std::mt19937, 64bit 버전의 std::mt19937_64
2. 시간을 시작 숫자(시드값)로 활용하여 난수 생성
3. <random> 헤더 파일의 random_device 를 사용, 하드웨어 엔트로피를 시작 숫자(시드값)으로 활용하여 난수 생성
컨셉(Concept) 템플릿의 타입에 “어떤 연산이 가능한가”를 명시적으로 제약하는 기능으로,
코드의 안정성, 가독성, 컴파일 오류 가독성을 모두 향상시키는 C++20의 핵심 기능이다.

예시)
#include <concepts>   // concept 사용을 위해 필요
#include <iostream>

template <std::integral T>  // ✅ 정수형만 허용
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(3, 5) << '\n';   // OK
    // std::cout << add(3.2, 4.5);   // ❌ 컴파일 오류: double은 integral 아님
}

 

반응형

'Programming Language > C++' 카테고리의 다른 글

시간 복잡도  (0) 2025.11.05
컨테이너와 반복자  (0) 2025.11.05
템플릿  (0) 2025.11.03
객체지향 설계 원칙  (0) 2025.11.03
다형성을 활용한 디자인 패턴  (0) 2025.11.03
반응형

함수 템플릿

  • 함수 하나로 다양한 형식의 데이터를 같은 알고리즘으로 처리할 수 있다.
  • 개방 폐쇄 원칙(OCP)을 지키면서 여러가지 용도로 널리 쓸 수 있는 함수를 만드는 방법이다.
  • 매개변수의 데이터 형식을 함수를 작성하는 시점이 아닌, 사용(호출)하는 시점에 정할 수 있다.
  • 함수 템플릿이 포함된 소스 코드를 컴파일하면 해당 함수에 사용된 데이터 형식으로 함수의 실제 구현체가 만들어진다.
  • 모든 데이터 형식에 대응할 수 있는 알고리즘으로 정의해야한다. 그렇지 않으면 컴파일 오류가 발생한다.
  • 컴파일러는 함수 템플릿을 호출하는 구문을 만나면 인자로 전달한 값으로 템플릿 매개변수의 데이터 형식을 추론하고, 이 형식으로 완성된 함수를 오브젝트 코드로 만든다. 이 과정을 템플릿의 인스턴스화 라고 한다.
  • 템플릿 매개변수의 데이터 형식을 명시해 함수를 호출하면 컴파일러가 데이터 형식을 추론하지 않고 해당 형식으로 변환한 후에 곧바로 인스턴스화한다. 즉 형식 추론은 시간이 많이 소요되는 작업이기 때문에 명시적 호출을 지향한다.
  • 특정 데이터 형식만 다른 알고리즘으로 처리하게 할 수 있다. 이를 템플릿 특수화라고 한다. 함수 템플릿은 명시적 특수화, 부분 특수화 중 명시적 특수화만 사용할 수 있다.
  • 컴파일러가 함수 호출을 처리할 때는 오버로딩 해석 과정을 거치기 때문에 부분 특수화는 금지시켰다. 즉 오버로딩으로 해결이 가능하기 때문이다.

작업 예시

  • 명시적 특수화
#include <iostream>
#include <string>

using namespace std;

template <typename T>
T data_sum(T operand1, T operand2) {
  return operand1 + operand2;
}

// 템플릿 타입이 double 일 경우 실행할 함수를 재정의
template <>
double data_sum(double operand1, double operand2) {
  return  (int)operand1 + (int)operand2;
}

int main() {
  int data1 = 3, data2 = 5;
  double data3 = 4.5, data4 = 8.9;
  string data5 = "Hello, ", data6 = "World!";

  cout << "정수형 데이터 합: " << data_sum(data1, data2) << endl;
  cout << "실수형 데이터 합: " << data_sum(data3, data4) << endl;
  cout << "문자열 데이터 합: " << data_sum(data5, data6) << endl;

  return 0;
}

/*
실행 결과

정수형 데이터 합: 8
실수형 데이터 합: 12
문자열 데이터 합: Hello, World!
*/

클래스 템플릿

  • 템플릿 매개변수를 활용해 다양한 형식에 대응할 수 있는 범용 클래스를 만드는 방법이다.
  • 클래스 템플릿에서는 객체를 생성할 때 템플릿 매개변수의 형식을 명시해주어야 한다.(C++ 17 이후 컴파일러에서는 형식 추론이 가능하지만 컴파일러는 템플릿 매개변수의 형식을 완벽하게 추론해 낼 수 없기 때문에 직접 명시하는 것을 지향한다.)
  • 템플릿 매개변수의 일부만 지정할 수 있는 부분 특수화를 이용할 수 있다.
  • 클래스 템플릿의 선언과 정의를 별도의 파일로 분리하면 안되며, 한 파일에 있어야 한다. 왜냐하면 템플릿은 컴파일 시점에 인스턴스화 되기 때문에 다른 파일에 있을 경우 링크 오류가 발생한다.

작업 예시

  • 부분 특수화
#include <iostream>

using namespace std;

template <typename Type1, typename Type2>
class data_package {
public:
  data_package(Type1 first, Type2 second) : first(first), second(second) {}
  void print_out_element() {
    cout << "첫 번째 요소: " << first <<
      ", 두 번째 요소: " << second << endl;
  }
private:
  Type1 first;
  Type2 second;
};

// 부분 특수화 첫번째 매개변수는 string으로 고정하고 두번째만 템플릿 매개변수로 사용
template <typename T>
class data_package<string, T> {
public:
  data_package(string first, T second) : first(first), second(second) {}
  void print_out_element() {
    cout << first << "과 함께 입력된" <<
      ", 두 번째 요소: " << second << endl;
  }
private:
  string first;
  T second;
};
int main() {
  data_package<int, double> template_inst1(5, 10.5);
  data_package<string, int> template_inst2("문자열", 10);

  template_inst1.print_out_element();
  template_inst2.print_out_element();
}

 

반응형

'Programming Language > C++' 카테고리의 다른 글

컨테이너와 반복자  (0) 2025.11.05
용어 정리  (0) 2025.11.04
객체지향 설계 원칙  (0) 2025.11.03
다형성을 활용한 디자인 패턴  (0) 2025.11.03
프로그래밍 패러다임  (0) 2025.10.30

+ Recent posts