본문 바로가기
GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~

[C/C++ 프로그래밍 : 중급] 9.STL 컨테이너

by GDNGY 2023. 6. 7.

Chapter 9. STL 컨테이너

STL(Standard Template Library) 컨테이너는 C++ 표준 라이브러리의 일부로, 다양한 데이터 구조를 제공합니다. 이 컨테이너들은 자료형에 대해 일반화된(generic) 프로그래밍을 가능하게 하며, 배열, 연결 리스트, 스택, 큐, 트리 등 다양한 자료구조를 표준화된 형태로 사용할 수 있게 합니다. 컨테이너는 값을 저장하는 객체로, 일반적으로 STL 알고리즘과 함께 사용되어 데이터의 효율적인 처리를 도와줍니다.

 

반응형

 


[Chapter 9. STL 컨테이너]

 

9.1. STL 이해하기

9.1.1. STL이란 무엇인가

9.1.2. STL의 구성요소: 컨테이너, 반복자, 알고리즘

9.1.3. STL의 장점과 특징

 

9.2. 시퀀스 컨테이너

9.2.1. vector의 사용

9.2.2. list의 사용

9.2.3. deque의 사용

9.2.4. array의 사용

9.2.5. forward_list의 사용

 

9.3. 컨테이너 어댑터

9.3.1. stack의 사용

9.3.2. queue의 사용

9.3.3. priority_queue의 사용

 

9.4. 연관 컨테이너

9.4.1. set의 사용

9.4.2. multiset의 사용

9.4.3. map의 사용

9.4.4. multimap의 사용

 

9.5. 비순차 컨테이너

9.5.1. unordered_set의 사용

9.5.2. unordered_multiset의 사용

9.5.3. unordered_map의 사용

9.5.4. unordered_multimap의 사용

 

9.6. 컨테이너의 공통 멤버 함수

9.6.1. 컨테이너 멤버 함수의 종류와 특징

9.6.2. 컨테이너 멤버 함수 사용 예제

 

9.7. 컨테이너와 반복자의 관계 이해하기

9.7.1. 반복자의 개념과 필요성

9.7.2. 반복자의 종류와 사용 방법

9.7.3. 컨테이너와 반복자의 상호작용 이해하기


9.1. STL 이해하기

STL은 Standard Template Library의 약자로, C++에서 제공하는 표준 라이브러리입니다. 이 라이브러리는 컨테이너, 반복자, 알고리즘이라는 세 가지 주요 구성 요소로 이루어져 있습니다. 컨테이너는 다양한 데이터 구조를 표현하는 클래스를, 반복자는 컨테이너의 원소에 접근하는 방법을, 알고리즘은 이들 원소를 처리하는 방법을 제공합니다. STL은 이러한 구성 요소를 일관된 방법으로 사용할 수 있도록 설계되어 있어, 프로그래머로서의 생산성과 코드의 효율성을 크게 높일 수 있습니다.

9.1.1. STL이란 무엇인가

STL(Standard Template Library)은 C++ 표준 라이브러리의 일부로, 효율적인 알고리즘과 데이터 구조를 제공하는 일련의 템플릿 클래스와 함수들을 포함합니다. STL은 말 그대로 표준화된 템플릿 라이브러리로, 다양한 프로그램 개발에서 필요한 공통적인 작업들을 효율적으로 처리할 수 있도록 지원합니다.

 

이해를 돕기 위해, "템플릿"이란 무엇인지, 그리고 왜 STL이 중요한지에 대해 좀 더 자세히 알아보겠습니다.

 

템플릿이란, 데이터 타입에 의존하지 않고 일반화된 코드를 작성하는 C++의 기능 중 하나입니다. 쉽게 말해, 템플릿을 이용하면 하나의 코드로 다양한 데이터 타입을 처리할 수 있습니다. 예를 들어, 정수형 배열과 실수형 배열, 두 가지 배열의 합을 구하는 함수를 각각 따로 작성할 필요 없이, 템플릿을 이용하여 하나의 함수로 작성할 수 있습니다.

 

C++ 코드 예제는 다음과 같습니다:

 

[예제]

#include <iostream>

template <typename T>
T addElements(T arr[], int size) {
    T sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int intArr[] = {1, 2, 3, 4, 5};
    double doubleArr[] = {1.1, 2.2, 3.3, 4.4, 5.5};

    std::cout << "Sum of integer array: " << addElements(intArr, 5) << std::endl;
    std::cout << "Sum of double array: " << addElements(doubleArr, 5) << std::endl;

    return 0;
}

 

위 예제에서 addElements 함수는 템플릿 함수로 정의되었습니다. 이 함수는 매개변수로 배열과 배열의 크기를 받아 배열의 모든 원소들을 더한 후 그 결과를 반환합니다. 함수의 매개변수와 반환 타입을 T로 표기하였는데, 이 T는 '어떤 타입이든 될 수 있다'는 것을 의미합니다. 따라서 이 함수는 정수형 배열, 실수형 배열 등 어떤 타입의 배열에도 사용할 수 있습니다.

 

STL이 중요한 이유는, 개발자가 데이터 구조와 알고리즘을 처음부터 직접 구현하지 않아도 된다는 점에 있습니다. STL은 벡터, 리스트, 큐, 스택, 맵 등 다양한 데이터 구조와, 정렬, 검색, 복사 등 기본적인 알고리즘을 제공하므로 개발자는 이러한 기능을 직접 구현하는 데 시간을 들이지 않아도 됩니다. 이로 인해 개발 시간이 단축되고, 코드의 품질과 효율성이 향상됩니다.

 

이제 STL에서 제공하는 vector 컨테이너를 이용하는 방법에 대한 예제를 살펴봅시다.

 

[예제]

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;

    // vector에 원소 추가
    for (int i = 1; i <= 5; i++) {
        vec.push_back(i);
    }

    // vector의 원소 출력
    for (int i = 0; i < vec.size(); i++) {
        std::cout << vec[i] << ' ';
    }
    std::cout << std::endl;

    return 0;
}

 

위 코드에서는 vector 컨테이너를 사용하여 1부터 5까지의 숫자를 저장하고 출력하는 프로그램을 작성하였습니다. vector는 동적 배열을 나타내는 컨테이너로, 원소를 추가하거나 제거할 때 자동으로 크기가 조절됩니다. push_back 함수를 이용하여 원소를 추가하고, size 함수를 이용하여 vector의 크기를 알 수 있습니다. 이러한 vector의 메서드들은 STL에서 제공하는 메서드로, 개발자가 별도로 구현할 필요가 없습니다.

 

STL을 이용하면, 개발자는 필요한 기능을 빠르고 안정적으로 구현하는데 집중할 수 있습니다. 이것이 바로 STL이 C++ 프로그래밍에서 중요한 이유입니다.

 

9.1.2. STL의 구성요소: 컨테이너, 반복자, 알고리즘

STL은 크게 컨테이너, 반복자, 알고리즘의 세 가지 구성요소로 이루어져 있습니다. 이 세 가지 요소는 모두 서로 긴밀하게 연결되어 있어, 함께 사용될 때 STL의 진정한 힘을 발휘합니다.

 

1) 컨테이너의 정의와 종류

컨테이너는 이름 그대로 데이터를 담는 그릇이라고 할 수 있습니다. STL 컨테이너는 클래스 템플릿 형태로 제공되며, 동적으로 크기가 변경 가능한 컨테이너(예: vector, list, deque)와 고정된 크기를 가진 컨테이너(예: array) 등 다양한 형태가 있습니다. 또한, 특정한 데이터 구조를 나타내는 컨테이너들도 있습니다. 예를 들어, stack, queue, priority_queue는 각각 스택, 큐, 우선순위 큐 데이터 구조를 나타냅니다.

 

아래는 vector 컨테이너를 이용하는 예제입니다.

 

[예제]

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;

    // vector에 원소 추가
    for (int i = 1; i <= 5; i++) {
        vec.push_back(i);
    }

    // vector의 원소 출력
    for (int i = 0; i < vec.size(); i++) {
        std::cout << vec[i] << ' ';
    }
    std::cout << std::endl;

    return 0;
}

 

2) 반복자의 정의와 종류

반복자는 컨테이너의 원소들을 순회하거나 접근하는 방법을 제공하는 객체입니다. 반복자는 포인터와 유사하게 동작하며, 다양한 종류의 반복자가 있습니다. 예를 들어, forward iterator는 컨테이너의 원소를 앞으로만 순회할 수 있고, bidirectional iterator는 컨테이너의 원소를 앞뒤로 순회할 수 있습니다. 반복자를 이용하면 STL의 알고리즘을 일관된 방법으로 적용할 수 있습니다.

 

아래는 vector 컨테이너의 반복자를 이용하는 예제입니다.

 

[예제]

#include <iostream>
#include <vector>

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

    // vector의 반복자를 이용한 원소 출력
    for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << ' ';
    }
    std::cout << std::endl;

    return 0;
}

 

3) 알고리즘의 정의와 종류

알고리즘은 정렬, 검색, 복사 등 특정한 작업을 수행하는 함수들을 의미합니다. STL 알고리즘은 함수 템플릿 형태로 제공되며, 대부분의 알고리즘은 반복자를 통해 컨테이너의 원소에 접근합니다. 이로 인해 STL의 알고리즘은 다양한 컨테이너에 일관된 방법으로 적용될 수 있습니다.

 

아래는 vector 컨테이너에 STL의 정렬 알고리즘을 적용하는 예제입니다.

 

[예제]

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

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

    // STL의 정렬 알고리즘 적용
    std::sort(vec.begin(), vec.end());

    // vector의 원소 출력
    for (int i = 0; i < vec.size(); i++) {
        std::cout << vec[i] << ' ';
    }
    std::cout << std::endl;

    return 0;
}

 

이 정렬된 벡터를 출력하면 1, 2, 3, 4, 5 순서로 원소들이 정렬되어 출력됩니다. 이처럼 STL의 알고리즘은 반복자를 이용하여 컨테이너에 독립적으로 적용될 수 있으며, 이를 통해 프로그래머들은 복잡한 코드를 작성하지 않고도 다양한 작업을 수행할 수 있습니다.

 

[예제]

// 이전에 정렬된 벡터에 대해 이진 탐색 수행
if(std::binary_search(vec.begin(), vec.end(), 3)) {
    std::cout << "3 was found in the vector." << std::endl;
} else {
    std::cout << "3 was not found in the vector." << std::endl;
}

 

위 예제는 STL의 이진 탐색 알고리즘을 이용하여 벡터에서 특정 원소(3)를 찾는 예제입니다. binary_search는 찾고자 하는 원소가 컨테이너에 있으면 true를, 없으면 false를 반환합니다. 이처럼 STL의 알고리즘은 프로그래머가 일반적으로 필요로 하는 다양한 연산들을 간결하게 수행할 수 있게 도와줍니다. 

 

STL의 컨테이너, 반복자, 알고리즘 세 가지 구성요소는 서로 밀접하게 연관되어 있습니다. 컨테이너는 데이터를 저장하고, 반복자는 컨테이너의 데이터에 접근하며, 알고리즘은 반복자를 통해 데이터에 여러 작업을 수행합니다. 이 세 가지 요소가 잘 조화를 이루어야 STL을 효율적으로 활용할 수 있습니다.

 

9.1.3. STL의 장점과 특징

STL(Standard Template Library)은 그 이름에서도 알 수 있듯이 표준 라이브러리로, 이로 인해 프로그래머는 보다 안정적이고 신뢰성 있는 코드를 작성할 수 있습니다. STL은 효율적인 알고리즘과 컨테이너를 제공하며, 이들을 통해 복잡한 데이터 구조와 알고리즘을 쉽게 이해하고 활용할 수 있습니다. STL의 주요 장점과 특징에 대해 알아봅시다. 

  • 일관성 있는 인터페이스: STL의 모든 컴포넌트는 일관된 인터페이스를 제공합니다. 즉, 한번 STL의 사용 방법을 익히면 다양한 컨테이너와 알고리즘을 쉽게 사용할 수 있습니다.
  • 코드 재사용: STL은 컨테이너와 알고리즘을 템플릿 형태로 제공하므로, 프로그래머는 자신의 데이터 타입에 맞게 이들을 사용할 수 있습니다. 이로 인해 코드 재사용이 용이해집니다. 
  • 효율성: STL은 효율적인 알고리즘과 컨테이너를 제공합니다. 이를 통해 프로그래머는 성능 최적화에 신경 쓰지 않고도 빠르고 효율적인 코드를 작성할 수 있습니다.
  • 확장성: 프로그래머는 STL을 확장하여 자신만의 컨테이너나 알고리즘을 개발할 수 있습니다. 이를 통해 프로그래머는 자신의 문제에 최적화된 코드를 작성할 수 있습니다.
  • 추상화: STL은 반복자와 알고리즘 등을 통해 데이터 구조와 작업을 추상화합니다. 이를 통해 프로그래머는 더 높은 수준에서 문제를 생각할 수 있습니다.
  • 유지보수: STL은 표준 라이브러리이므로, 프로그래머는 자신이 작성한 코드가 다른 플랫폼에서도 동작할 것임을 확신할 수 있습니다. 또한, STL의 코드는 잘 작성되어 있으므로, 버그를 찾거나 코드를 유지보수하는데 도움이 됩니다.

이러한 장점들로 인해 STL은 C++ 개발자에게 필수적인 라이브러리입니다.


9.2. 시퀀스 컨테이너

시퀀스 컨테이너는 STL의 핵심 컴포넌트 중 하나로, 데이터를 선형적으로 저장합니다. 이를 통해 사용자는 데이터를 원하는 순서대로 저장하고 접근할 수 있습니다. 이번 장에서는 다양한 시퀀스 컨테이너, 즉 vector, list, deque, array, 그리고 forward_list에 대해 살펴보며, 각각의 사용법과 특징을 이해하겠습니다. 시퀀스 컨테이너를 사용하면 데이터의 추가, 삭제, 조회 등 다양한 작업을 효율적으로 수행할 수 있습니다.

 

9.2.1. vector의 사용

Vector는 STL 시퀀스 컨테이너의 한 종류로, 동적 배열을 구현한 것입니다. 

 

1) vector의 정의와 특징

Vector의 주요 특징 중 하나는 메모리를 연속적으로 할당한다는 점입니다. 이는 데이터에 빠르게 접근할 수 있다는 장점을 가집니다. 다시 말해, 인덱스를 사용하여 데이터를 접근하는 것이 가능하다는 뜻입니다.

 

그러나 이러한 연속 메모리 할당 방식은 한편으로 단점을 가집니다. Vector의 크기가 변경될 때마다 메모리를 새롭게 할당하고, 기존 데이터를 새 위치로 복사해야 합니다. 그래서 크기 변경에 대한 비용이 발생할 수 있습니다.

 

2) vector의 사용 예제

아래 코드는 vector의 기본 사용법을 보여줍니다.

 

[예제]

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec;  // 정수형 vector 생성

    // 데이터 추가
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
    }

    // 데이터 접근 및 출력
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << "Element [" << i << "]: " << vec[i] << std::endl;
    }

    return 0;
}

 

이 예제는 vector의 생성, 데이터의 추가, 그리고 데이터의 접근에 대해 보여줍니다. push_back() 함수는 vector의 끝에 새로운 요소를 추가하는 함수입니다. 이 함수를 사용하여 vector에 데이터를 쉽게 추가할 수 있습니다. 그리고 [] 연산자를 사용하여 특정 인덱스의 데이터에 접근할 수 있습니다.

 

3) vector의 심화적 사용법

Vector는 다양한 방법으로 사용될 수 있습니다. 예를 들어, vector의 크기를 변경하는 것이 가능합니다. 또한 iterator를 사용하여 데이터에 접근하는 것도 가능합니다. 아래 예제를 보겠습니다.

 

[예제]

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec;  // 정수형 vector 생성

    // 데이터 추가
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
    }

    vec.resize(15, 100);  // 크기를 15로 변경하고, 새로운 요소는 100으로 초기화

    // iterator를 사용한 데이터 접근 및 출력
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << "Element: " << *it << std::endl;
    }

    return 0;
}

 

이 예제에서는 resize() 함수를 사용하여 vector의 크기를 변경하고 있습니다. begin()과 end() 함수를 사용하여 vector의 시작부터 끝까지 반복하고 있습니다. 이렇게 iterator를 사용하면 특정 범위의 데이터에 접근하는 것이 가능해집니다. 이와 같은 심화적 사용법을 이해하고 활용하면, vector를 더욱 효과적으로 사용할 수 있습니다. 

 

9.2.2. list의 사용

List는 STL의 시퀀스 컨테이너 중 하나로, 이중 연결 리스트를 구현한 것입니다. 이중 연결 리스트란 각 노드가 이전 노드와 다음 노드에 대한 참조를 갖는 자료구조를 말합니다.

 

1) list의 특징

List의 주요 특징 중 하나는 임의의 위치에서의 삽입과 삭제가 상수 시간에 가능하다는 점입니다. 즉, list는 데이터의 삽입과 삭제가 빈번하게 발생하는 상황에서 효율적으로 사용할 수 있습니다. 

 

그러나 이런 장점은 다른 측면에서의 단점으로 나타날 수 있습니다. list는 메모리를 연속적으로 할당하지 않기 때문에, 인덱스를 통한 랜덤 접근이 불가능합니다. 즉, 특정 위치의 데이터에 접근하기 위해서는 처음부터 차례대로 접근해야 하므로 비효율적일 수 있습니다.

 

2) list의 사용 예제

아래 코드는 list의 기본 사용법을 보여줍니다.

 

[예제]

#include <list>
#include <iostream>

int main() {
    std::list<int> lst;  // 정수형 list 생성

    // 데이터 추가
    for (int i = 0; i < 10; ++i) {
        lst.push_back(i);
    }

    // 데이터 접근 및 출력
    for (auto it = lst.begin(); it != lst.end(); ++it) {
        std::cout << "Element: " << *it << std::endl;
    }

    return 0;
}

 

이 예제는 list의 생성, 데이터의 추가, 그리고 데이터의 접근에 대해 보여줍니다. push_back() 함수는 list의 끝에 새로운 요소를 추가하는 함수입니다. 이 함수를 사용하여 list에 데이터를 쉽게 추가할 수 있습니다. 그리고 iterator를 사용하여 데이터에 접근할 수 있습니다.

 

3) list의 심화적 사용법

List는 다양한 방법으로 사용될 수 있습니다. 예를 들어, list는 중간 위치에 새로운 요소를 추가하거나, 특정 요소를 삭제하는 것이 가능합니다. 아래 예제를 보겠습니다.

 

[예제]

#include <list>
#include <iostream>

int main() {
    std::list<int> lst;  // 정수형 list 생성

    // 데이터 추가
    for (int i = 0; i < 10; ++i) {
        lst.push_back(i);
    }

    auto it = lst.begin();
    std::advance(it, 5);  // iterator를 5칸 전진
    lst.insert(it, 100);  // 해당 위치에 100 추가

    // 데이터 삭제
    it = lst.begin();
    std::advance(it, 3);  // iterator를 3칸 전진
    lst.erase(it);  // 해당 위치의 요소 삭제

    // 데이터 접근 및 출력
    for (auto it = lst.begin(); it != lst.end(); ++it) {
        std::cout << "Element: " << *it << std::endl;
    }

    return 0;
}

 

이 예제에서는 insert()와 erase() 함수를 사용하여 특정 위치의 데이터를 추가하거나 삭제하는 방법을 보여줍니다. 이와 같이 list는 데이터의 삽입과 삭제가 자유롭다는 장점을 갖습니다. 또한, std::advance() 함수를 사용하여 iterator를 원하는 위치로 이동시킬 수 있습니다.

 

9.2.3. deque의 사용

deque(덱이라고 읽음)은 Double Ended Queue의 약자로 양쪽 끝에서 삽입과 삭제가 모두 가능한 STL 컨테이너입니다. vector와 마찬가지로 데이터를 연속적으로 저장하지만, 앞 뒤로의 자료 삽입 및 삭제가 가능하다는 점이 다릅니다.

 

deque의 정의와 특징

덱의 가장 큰 특징은 양방향 모두에서 데이터의 삽입과 삭제가 가능하다는 점입니다. 이로 인해 queue와 stack의 장점을 모두 갖게 되어 유연한 프로그래밍이 가능합니다. 하지만 덱의 중간 위치에 있는 원소에 접근하는 것은 비교적 느린 편이며, 메모리 할당도 vector보다 불규칙합니다.

 

deque의 사용 예제

 

[예제]

std::deque<int> d;

d.push_back(5); // 덱 뒤에 5 추가 {5}
d.push_front(3); // 덱 앞에 3 추가 {3, 5}
d.pop_back(); // 덱 뒤에 있는 원소 삭제 {3}
d.pop_front(); // 덱 앞에 있는 원소 삭제 {}

 

위의 예제에서 push_back(), push_front(), pop_back(), pop_front() 등의 함수를 사용하였습니다. 이 외에도 size(), empty(), back(), front(), clear() 등의 함수를 사용할 수 있습니다. vector와 거의 동일한 인터페이스를 제공하며, 기능적으로 더 확장된 형태라고 볼 수 있습니다.

 

덱은 스케줄링, 캐시 등의 알고리즘에서 유용하게 사용됩니다. 또한 덱은 연속적인 메모리 공간에 데이터를 저장하는 vector와 달리, 여러 개의 비연속적인 메모리 블록에서 객체를 관리하므로, 큰 데이터 세트를 다룰 때에도 메모리 부족 문제를 효과적으로 해결할 수 있습니다.

 

deque의 심화적 사용법

 

[예제]

#include <iostream>
#include <deque>

int main() {
    std::deque<int> d;
    for(int i = 0; i < 10; ++i) {
        if(i % 2 == 0) d.push_front(i);
        else d.push_back(i);
    }

    // 덱의 모든 원소를 출력
    for(int i = 0; i < d.size(); ++i) {
        std::cout << d[i] << ' ';
    }

    return 0;
}

 

위 예제는 0부터 9까지의 숫자 중, 짝수는 덱의 앞에, 홀수는 덱의 뒤에 삽입한 후, 덱의 모든 원소를 출력하는 코드입니다. 이처럼 deque는 양쪽 끝에서의 추가 및 삭제 연산이 필요할 때 유용하게 사용될 수 있습니다.

 

9.2.4. array의 사용

array의 정의와 특징

C++ STL에는 std::array라는 컨테이너가 있습니다. 이는 고정된 크기의 배열을 제공하며, 크기는 컴파일 타임에 결정되어야 합니다. std::array는 일반적인 C 스타일 배열과 비슷하나, STL 컨테이너들이 가지는 여러 편리한 메서드와 멤버 함수를 제공하며, 더 안전하고 사용하기 쉽습니다.

 

array의 사용 예제

 

[예제]

std::array<int, 5> arr = {1, 2, 3, 4, 5};

for(int i = 0; i < arr.size(); i++)
    std::cout << arr[i] << ' ';  // 1 2 3 4 5 출력

arr.fill(10);  // 모든 원소를 10으로 채움

for(auto it = arr.begin(); it != arr.end(); ++it)
    std::cout << *it << ' ';  // 10 10 10 10 10 출력


array의 심화적 사용법

 

std::array는 begin(), end(), size(), front(), back(), fill(), swap() 등의 멤버 함수를 제공합니다. 따라서 범위 기반 for 문 또는 STL 알고리즘과 같이 사용할 수 있습니다.

[예제]

#include <algorithm>  // sort 함수를 사용하기 위해 필요
std::array<int, 5> arr = {5, 3, 1, 4, 2};

std::sort(arr.begin(), arr.end());  // 배열의 원소를 오름차순으로 정렬

for(auto& e : arr)
    std::cout << e << ' ';  // 1 2 3 4 5 출력

 

위의 예제는 std::sort 알고리즘을 사용하여 std::array의 원소를 정렬하는 코드입니다. std::array는 기본적으로 연속된 메모리 공간에 원소를 저장하므로, 포인터 연산이나 기타 배열과 유사한 동작을 수행할 수 있습니다. 하지만 그 크기가 고정되어 있으므로, 동적으로 크기를 변경할 수는 없습니다. 이 점을 유의하여 사용해야 합니다.

 

9.2.5. forward_list의 사용

C++의 STL에서 제공하는 forward_list는 단방향 연결 리스트를 구현한 컨테이너입니다. 

 

forward_list의 정의와 특징

연결 리스트는 데이터를 메모리의 비연속적인 위치에 저장하며, 각 데이터 항목(노드)은 다음 노드에 대한 참조(포인터)를 갖습니다. forward_list는 이름에서 알 수 있듯이 한 방향으로만 이동할 수 있는 반면, list는 양방향 이동이 가능합니다.

 

forward_list의 사용 예제

 

[예제]

#include <forward_list>
std::forward_list<int> mylist = {10, 20, 30}; //int형의 forward_list 선언

mylist.push_front(5);  // 리스트의 맨 앞에 5를 추가

for(auto it = mylist.begin(); it != mylist.end(); ++it)
    std::cout << *it << ' ';  // 5 10 20 30 출력


forward_list의 심화적 사용법

 

forward_list는 단방향이기 때문에 push_front를 사용하여 앞쪽에 요소를 추가할 수 있지만, push_back은 제공하지 않습니다. 이는 forward_list가 각 요소를 추가하거나 제거할 때 O(1)의 시간 복잡도를 가지지만, 뒤쪽으로 이동하는 데는 O(N)의 시간이 소요되기 때문입니다.

[예제]

mylist.insert_after(mylist.before_begin(), 0);  // 리스트의 맨 앞에 0를 추가

for(auto& e : mylist)
    std::cout << e << ' ';  // 0 5 10 20 30 출력

 

insert_after 함수를 사용하면 주어진 위치 바로 다음에 요소를 삽입할 수 있습니다. 위치를 나타내는 이터레이터는 실제 위치가 아닌, 해당 위치 전의 위치를 가리킵니다. 따라서, 맨 앞에 요소를 추가하려면 before_begin() 이터레이터를 사용해야 합니다.

 

forward_list의 다른 멤버 함수로는 erase_after, pop_front, remove, remove_if, reverse 등이 있습니다. 이들 함수를 사용하면 연결 리스트를 효과적으로 관리할 수 있습니다. 이러한 특성 덕분에 forward_list는 노드 간의 이동이 주로 한 방향으로 이루어지는 경우 유용하게 사용될 수 있습니다.


9.3. 컨테이너 어댑터

컨테이너 어댑터는 기본 STL 컨테이너의 기능을 특정한 인터페이스에 맞게 변형하는 역할을 합니다. 어댑터의 종류에는 stack, queue, priority_queue가 있습니다. 이들은 내부에서 deque나 list 등을 사용하며, 컨테이너의 기능을 제한적이지만 편리하게 제공합니다. 예를 들어, stack은 LIFO(Last In First Out), queue는 FIFO(First In First Out) 형식으로 데이터를 처리합니다.

9.3.1. stack의 사용

C++ STL의 stack은 컨테이너 어댑터 중 하나로, 데이터를 LIFO(Last In First Out) 방식으로 저장합니다. 이는 가장 최근에 저장된 요소가 먼저 추출되는 특징을 가집니다. 이는 컴퓨터의 메모리 구조와 비슷하게 작동하며, 프로그램의 함수 호출 스택에서 볼 수 있습니다.

 

stack의 정의 : stack은 템플릿 클래스로서, 특정 타입의 요소를 저장할 수 있습니다. stack은 다른 컨테이너를 내부에 저장하고 있으며, 그 컨테이너의 요소에 LIFO 접근을 제공합니다.

 

[예제]

#include <stack> 
std::stack<int> s; // int 타입을 저장하는 stack

stack의 주요 함수들 : stack은 push, pop, top, size, empty 등의 함수를 제공합니다.

  • push(): stack의 가장 위에 요소를 추가합니다.
  • pop(): stack의 가장 위의 요소를 제거합니다. (값을 반환하지 않습니다)
  • top(): stack의 가장 위에 있는 요소를 참조합니다.
  • size(): stack에 저장된 요소의 수를 반환합니다.
  • empty(): stack이 비어있는지를 확인합니다.

[예제]

s.push(1); // stack에 1을 추가합니다. stack: {1}
s.push(2); // stack에 2를 추가합니다. stack: {1, 2}
s.pop();   // stack의 가장 위 요소를 제거합니다. stack: {1}
s.top();   // stack의 가장 위 요소를 반환합니다. 여기서는 1을 반환합니다.

 

stack의 사용 예제 : 아래 예제는 stack의 사용법을 보여주는 간단한 프로그램입니다.

 

[예제]

#include <iostream>
#include <stack>

int main() {
    std::stack<int> s;
    s.push(1);
    s.push(2);
    s.push(3);

    while(!s.empty()) {
        std::cout << s.top() << "\n";
        s.pop();
    }

    return 0;
}

 

위 프로그램은 1, 2, 3을 순서대로 stack에 추가한 후, stack이 빌 때까지 가장 위의 요소를 출력하고 제거하는 과정을 반복합니다. 따라서 출력 결과는 3, 2, 1 순서로 나타납니다.

 

stack은 재귀함수를 구현할 때 또는 후위 표기법 계산 등 다양한 알고리즘에 유용하게 쓰입니다. STL의 stack을 활용하면 복잡한 데이터 구조를 쉽게 구현할 수 있습니다.

 

9.3.2. queue의 사용

STL의 queue는 컨테이너 어댑터 중 하나로, 요소들을 FIFO(First In First Out) 방식으로 저장합니다. 즉, 먼저 들어온 요소가 먼저 나가는 구조를 가지고 있습니다. 이는 대기열에서 볼 수 있는 가장 기본적인 동작을 모방하고 있습니다.

 

queue의 정의 : queue는 템플릿 클래스로서, 특정 타입의 요소를 저장할 수 있습니다. queue는 다른 컨테이너를 내부에 저장하고 있으며, 그 컨테이너의 요소에 FIFO 접근을 제공합니다.

 

[예제]

#include <queue> 

std::queue<int> q; // int 타입을 저장하는 queue

 

queue의 주요 함수들 : queue는 push, pop, front, back, size, empty 등의 함수를 제공합니다.

  • push(): queue의 가장 뒤에 요소를 추가합니다.
  • pop(): queue의 가장 앞의 요소를 제거합니다. (값을 반환하지 않습니다)
  • front(): queue의 가장 앞에 있는 요소를 참조합니다.
  • back(): queue의 가장 뒤에 있는 요소를 참조합니다.
  • size(): queue에 저장된 요소의 수를 반환합니다.
  • empty(): queue가 비어있는지를 확인합니다.

[예제]

q.push(1); // queue에 1을 추가합니다. queue: {1}
q.push(2); // queue에 2를 추가합니다. queue: {1, 2}
q.pop();   // queue의 가장 앞 요소를 제거합니다. queue: {2}
q.front(); // queue의 가장 앞 요소를 반환합니다. 여기서는 2를 반환합니다.

 

queue의 사용 예제 : 아래 예제는 queue의 사용법을 보여주는 간단한 프로그램입니다.

 

[예제]

#include <iostream>
#include <queue>

int main() {
    std::queue<int> q;
    q.push(1);
    q.push(2);
    q.push(3);

    while(!q.empty()) {
        std::cout << q.front() << "\n";
        q.pop();
    }

    return 0;
}

 

위 프로그램은 1, 2, 3을 순서대로 queue에 추가한 후, queue가 빌 때까지 가장 앞의 요소를 출력하고 제거하는 과정을 반복합니다. 따라서 출력 결과는 1, 2, 3 순서로 나타납니다.

 

queue는 스케줄링, 이벤트 드리븐 프로그래밍, 캐시 알고리즘 등 다양한 알고리즘에 사용됩니다. STL의 queue를 사용하면 복잡한 데이터 구조를 쉽게 구현할 수 있습니다.

 

9.3.3. priority_queue의 사용

'priority_queue'는 STL 컨테이너 어댑터 중 하나로서, 요소를 우선순위에 따라 저장하고 관리합니다. 기본적으로 priority_queue는 'less' 연산자를 이용하여 요소를 비교하므로 큰 값이 높은 우선순위를 가집니다.

 

priority_queue의 정의 : priority_queue는 템플릿 클래스이므로, 특정 타입의 요소를 저장하며, 우선순위에 따라 자동 정렬합니다.


[예제]

#include <queue> 

std::priority_queue<int> pq; // int 타입을 저장하는 priority_queue


priority_queue의 주요 함수들 : priority_queue는 push, pop, top, size, empty 등의 함수를 제공합니다.

  • push(): priority_queue에 요소를 추가합니다. 추가 후에 자동으로 정렬됩니다.
  • pop(): priority_queue의 최상위 요소를 제거합니다. (값을 반환하지 않습니다)
  • top(): priority_queue의 최상위 요소를 반환합니다.
  • size(): priority_queue에 저장된 요소의 수를 반환합니다.
  • empty(): priority_queue가 비어있는지 확인합니다.

[예제]

pq.push(3); // priority_queue에 3을 추가합니다. pq: {3}
pq.push(1); // priority_queue에 1을 추가합니다. pq: {3, 1}
pq.push(2); // priority_queue에 2을 추가합니다. pq: {3, 2, 1}
pq.top();   // priority_queue의 최상위 요소를 반환합니다. 여기서는 3을 반환합니다.


priority_queue의 사용 예제: 아래 예제는 priority_queue의 사용법을 보여주는 간단한 프로그램입니다.

 

[예제]

#include <iostream>
#include <queue>

int main() {
    std::priority_queue<int> pq;
    pq.push(3);
    pq.push(1);
    pq.push(2);

    while(!pq.empty()) {
        std::cout << pq.top() << "\n";
        pq.pop();
    }

    return 0;
}

 

위 프로그램은 3, 1, 2를 순서대로 priority_queue에 추가한 후, priority_queue가 빌 때까지 최상위 요소를 출력하고 제거하는 과정을 반복합니다. 따라서 출력 결과는 3, 2, 1 순서로 나타납니다.

 

priority_queue는 다익스트라 알고리즘, 힙 정렬, 베스트 퍼스트 서치 등 다양한 알고리즘에 사용됩니다. STL의 priority_queue를 사용하면 복잡한 우선순위 로직을 쉽게 구현할 수 있습니다.


9.4. 연관 컨테이너

C++의 연관 컨테이너는 키(Key)를 기반으로 요소를 저장하고, 조회하는 컨테이너입니다. 이로 인해 빠른 검색 속도를 제공하며, 키에 따라 자동으로 정렬됩니다. 주요 연관 컨테이너에는 'set', 'multiset', 'map', 'multimap'이 있습니다. 'set'과 'map'은 중복 키를 허용하지 않지만, 'multiset'과 'multimap'은 중복 키를 허용합니다. 'set'과 'multiset'은 키만 저장하며, 'map'과 'multimap'은 키-값 쌍을 저장합니다.

9.4.1. set의 사용

'set'은 연관 컨테이너 중 하나로, 키의 유일성을 보장하는 구조입니다. 이는 동일한 값을 가진 요소를 여러 개 저장하지 않는다는 뜻입니다. 이렇게 저장된 요소는 특정 기준에 따라 자동으로 정렬됩니다. 기본적으로는 오름차순 정렬이지만, 사용자가 직접 비교 함수를 제공할 수도 있습니다.

1) set의 정의와 특징

기본적인 'set'의 정의는 다음과 같습니다:

[예제]

#include <set>

std::set<int> s;

 

위 코드에서는 정수 타입의 'set'을 정의했습니다. 이렇게 정의된 'set'에는 요소를 추가할 수 있고, 'set'이 이미 해당 요소를 포함하고 있지 않다면 요소가 추가됩니다.

 

'set'의 특징은 다음과 같습니다:

 

'set'은 중복을 허용하지 않습니다. 즉, 동일한 값을 가진 요소는 하나만 저장됩니다.
'set'에 저장된 요소는 자동으로 정렬됩니다.

 

2) set의 사용 예제

 

[예제]

#include <iostream>
#include <set>

int main() {
    std::set<int> s;

    // 요소 추가
    s.insert(3);
    s.insert(1);
    s.insert(5);
    s.insert(2);

    // 요소 출력
    for (int i : s) {
        std::cout << i << ' ';
    }

    return 0;
}

 

위 코드는 'set'에 요소를 추가하고, 이를 출력하는 예제입니다. 출력 결과는 "1 2 3 5"입니다. 추가한 순서와 상관없이 요소가 정렬되어 출력되는 것을 확인할 수 있습니다.

 

3) set의 심화적 사용법

'set'은 중복된 요소를 허용하지 않는 것 외에도, 특정 요소가 'set'에 포함되어 있는지 여부를 빠르게 검사할 수 있습니다. 이는 다음과 같이 'find' 함수를 사용하여 수행할 수 있습니다.

 

[예제]

if (s.find(3) != s.end()) {
    std::cout << "3은 셋에 있습니다.";
} else {
    std::cout << "3은 셋에 없습니다.";
}

 

위 코드는 'set'에 3이 포함되어 있는지 확인하는 코드입니다. 'find' 함수는 찾는 요소의 위치를 반환하고, 요소를 찾지 못한 경우 'end'를 반환합니다. 따라서 'find'의 결과가 'end'와 다른 경우, 찾는 요소가 'set'에 포함되어 있는 것입니다.

 

이처럼 'set'은 중복을 허용하지 않는 저장 공간이 필요하거나, 특정 요소의 포함 여부를 빠르게 확인해야 하는 경우에 유용하게 사용할 수 있습니다.

 

9.4.2. multiset의 사용

'multiset'은 'set'과 유사하지만 중요한 차이점이 있습니다. 'multiset'은 동일한 값을 가진 여러 개의 요소를 저장할 수 있습니다. 이렇게 저장된 요소는 특정 기준에 따라 자동으로 정렬됩니다. 기본적으로는 오름차순 정렬이지만, 사용자가 직접 비교 함수를 제공할 수도 있습니다.

 

1) multiset의 정의와 특징

기본적인 'multiset'의 정의는 다음과 같습니다:

 

[예제]

#include <set>

std::multiset<int> ms;

위 코드에서는 정수 타입의 'multiset'을 정의했습니다. 이렇게 정의된 'multiset'에는 요소를 추가할 수 있고, 'multiset'은 해당 요소가 몇 번 추가되었는지를 기록합니다.  'multiset'은 중복을 허용합니다. 즉, 동일한 값을 가진 요소를 여러 개 저장할 수 있습니다. 'multiset'에 저장된 요소는 자동으로 정렬됩니다.


2) multiset의 사용 예제

 

[예제]

#include <iostream>
#include <set>

int main() {
    std::multiset<int> ms;

    // 요소 추가
    ms.insert(3);
    ms.insert(1);
    ms.insert(3);
    ms.insert(2);

    // 요소 출력
    for (int i : ms) {
        std::cout << i << ' ';
    }

    return 0;
}

 

위 코드는 'multiset'에 요소를 추가하고, 이를 출력하는 예제입니다. 출력 결과는 "1 2 3 3"입니다. 추가한 순서와 상관없이 요소가 정렬되어 출력되는 것을 확인할 수 있습니다.

 

3) multiset의 심화적 사용법

'multiset'은 'set'과 달리 같은 값을 가진 요소를 여러 개 저장할 수 있습니다. 이 때문에 특정 값이 'multiset'에 몇 번 포함되어 있는지 확인하려면 'count' 함수를 사용해야 합니다.

 

[예제]

std::cout << "3의 개수: " << ms.count(3) << std::endl;

 

위 코드는 'multiset'에 3이 몇 개 있는지 출력하는 코드입니다. 'count' 함수는 찾는 값의 개수를 반환합니다. 따라서 이를 사용하면 특정 값이 'multiset'에 몇 번 포함되어 있는지 쉽게 확인할 수 있습니다.

 

이처럼 'multiset'은 중복을 허용하는 저장 공간이 필요할 때 유용하게 사용할 수 있습니다. 'set'과 마찬가지로, 특정 요소의 포함 여부를 빠르게 확인할 수 있으며, 저장된 요소가 자동으로 정렬되는 특징도 가지고 있습니다. 이는 코드를 더 간결하게 작성하고 가독성을 높여줍니다.

 

또한, multiset은 사용자 정의 비교 함수를 지원합니다. 이를 사용하면 요소를 정렬하는 방법을 사용자가 직접 지정할 수 있습니다. 다음은 사용자 정의 비교 함수를 사용하여 요소를 내림차순으로 정렬하는 예입니다:

 

[예제]

// 사용자 정의 비교 함수
struct comp {
    bool operator() (const int& a, const int& b) const {
        return a > b;
    }
};

// 사용자 정의 비교 함수를 사용하여 multiset 선언
std::multiset<int, comp> ms;

// 요소 추가 및 출력
ms.insert(3);
ms.insert(1);
ms.insert(3);
ms.insert(2);

for (int i : ms) {
    std::cout << i << ' ';
}

 

위 코드를 실행하면 출력 결과는 "3 3 2 1"이 됩니다. 추가한 순서와 상관없이 요소가 내림차순으로 정렬되어 출력되는 것을 확인할 수 있습니다.

 

multiset은 중복을 허용하는 정렬된 컨테이너로, 이를 이해하고 사용하면 C++에서 더욱 다양하고 복잡한 자료 구조를 효과적으로 관리할 수 있습니다. 이상 multiset에 대한 설명을 마치겠습니다. 다음 섹션에서는 multiset의 확장 형태인 multimap에 대해 알아보겠습니다.

 

9.4.3. map의 사용

map은 STL에서 제공하는 연관 컨테이너 중 하나로, 키와 값의 쌍(pair)을 요소로 가지는 컨테이너입니다.

 

1) map의 정의와 특징

 map에서 가장 중요한 특징은 키에 대해 정렬이 유지된다는 것입니다. 이는 검색, 제거, 삽입 작업이 로그 시간에 수행될 수 있도록 보장합니다.

 

키는 컨테이너 내에서 유일해야 합니다. 두 개 이상의 요소가 동일한 키를 가질 수 없습니다. 그러나 다른 키와 연결된 동일한 값은 여러 번 나타날 수 있습니다.

 

2) map의 사용 예제

다음은 map을 사용하는 간단한 예입니다:

 

[예제]

#include <iostream>
#include <map>

int main() {
    // map 선언
    std::map<std::string, int> m;

    // 값 추가
    m["apple"] = 10;
    m["banana"] = 20;
    m["cherry"] = 30;

    // 값 접근 및 출력
    std::cout << "apple: " << m["apple"] << '\n';
    std::cout << "banana: " << m["banana"] << '\n';
    std::cout << "cherry: " << m["cherry"] << '\n';

    return 0;
}

 

이 코드는 string 키와 int 값을 가진 map을 선언하고, 몇 가지 과일과 그 수량을 맵에 추가합니다. map의 요소에 대한 접근은 대괄호 연산자([])를 사용하여 수행됩니다.

 

3) map의 심화적 사용법

이제 이전에 본 것보다 조금 더 복잡한 예제를 살펴보겠습니다. map에는 존재하지 않는 키로 접근하려고 하면 map은 해당 키와 기본 초기화된 값을 가진 새로운 요소를 자동으로 생성합니다. 이 특징은 다음 예제에서 볼 수 있습니다:

[예제]

#include <iostream>
#include <map>

int main() {
    std::map<std::string, int> m;

    // 이전에 없던 키로 접근
    std::cout << m["plum"] << '\n'; // 출력: 0

    // 키 "plum"이 map에 추가되어 있음을 확인
    for (const auto& pair : m) {
        std::cout << pair.first << ": " << pair.second << '\n';
    }

    return 0;
}

 

위 코드에서, 우리는 "plum"이라는 키로 map에 접근하려고 합니다. 이 키는 아직 map에 존재하지 않기 때문에, map은 "plum" 키와 기본값인 0을 가진 새로운 요소를 추가합니다. 따라서 처음에는 0을 출력하고, for loop를 통해 map 내의 모든 요소를 출력하면 "plum: 0"이 출력됩니다. 이 기능은 주의해서 사용해야 합니다. 존재하지 않는 키로 접근하면 예상치 못한 결과가 발생할 수 있습니다.

 

9.4.4. multimap의 사용

multimap은 map과 매우 유사한 STL 연관 컨테이너입니다.

 

1) multimap의 정의와 특징

map과 주요 차이점은 map에서는 각 키가 유일해야하는 반면, multimap에서는 동일한 키를 가진 여러 요소가 허용된다는 것입니다. 즉, multimap은 하나의 키에 여러 값이 연결될 수 있습니다.

multimap은 키에 대한 정렬 순서를 유지하며, 검색, 삽입, 삭제가 로그 시간에 이루어집니다. 키는 비교 가능한 타입이어야 하며, 값은 어떠한 타입이든 가능합니다.

 

2) multimap의 사용 예제

다음은 multimap을 사용하는 간단한 예입니다:

 

[예제]

#include <iostream>
#include <map>

int main() {
    // multimap 선언
    std::multimap<std::string, int> mm;

    // 값 추가
    mm.insert({"apple", 10});
    mm.insert({"apple", 20});
    mm.insert({"banana", 30});

    // 값 접근 및 출력
    auto range = mm.equal_range("apple");
    for (auto i = range.first; i != range.second; ++i) {
        std::cout << i->first << ": " << i->second << '\n';
    }

    return 0;
}

 

위 코드는 std::string 키와 int 값을 가진 multimap을 선언하고, "apple" 키에 두 개의 값을 추가합니다. multimap에는 대괄호 연산자가 없으므로 insert 함수를 사용해야 합니다.

 

3) multimap의 심화적 사용법

동일한 키를 가진 요소에 액세스하려면 equal_range 함수를 사용할 수 있습니다. 이 함수는 해당 키의 첫 번째 요소와 마지막 요소를 가리키는 반복자 쌍을 반환합니다. 이를 이용하여 동일한 키를 가진 모든 요소를 출력하는 코드를 작성할 수 있습니다. 위의 코드에서는 "apple" 키를 가진 모든 요소를 출력하고 있습니다.

 

이렇게 multimap은 map과 유사하면서도, 하나의 키에 대해 여러 값을 허용한다는 특징을 가지고 있어 복잡한 데이터 구조를 표현하는데 유용합니다.


9.5. 비순차 컨테이너

비순차 컨테이너는 원소의 추가, 삭제, 조회가 순차적이지 않은 컨테이너를 말합니다. 주요 비순차 컨테이너로는 std::stack, std::queue, std::priority_queue 등이 있습니다. 이들은 내부적으로 시퀀스 컨테이너를 사용하며, 각각의 컨테이너는 특정한 접근 정책(LIFO, FIFO 등)을 따릅니다. 비순차 컨테이너는 특수한 경우에 유용하게 사용될 수 있습니다.

9.5.1. unordered_set의 사용

unordered_set은 STL(Standard Template Library)의 비순차 컨테이너 중 하나로, set과 마찬가지로 유일한 값을 저장하지만, 내부적으로 해시 테이블을 사용하여 값들을 저장하므로 원소의 순서를 보장하지 않습니다. 따라서 이름에서 알 수 있듯이 unordered_set은 "정렬되지 않은 집합"을 의미합니다.

 

그럼 unordered_set이 왜 필요한가요? 이것은 unordered_set이 set과 비교하여 데이터를 검색하는 데에 더 빠른 시간 복잡도를 가지기 때문입니다. set은 내부적으로 레드-블랙 트리를 사용하여 데이터를 저장하므로 검색하는 데에 O(log n)의 시간이 필요하며, unordered_set은 해시 테이블을 사용하므로 일반적으로 O(1)의 시간에 원소를 검색할 수 있습니다.

 

이제 unordered_set을 사용하는 방법에 대해 알아보겠습니다.

 

[예제]

#include <iostream>
#include <unordered_set>

int main() {
    std::unordered_set<int> myset;

    myset.insert(10);
    myset.insert(5);
    myset.insert(15);
    myset.insert(10); // 중복된 값은 무시됩니다.

    for(auto it = myset.begin(); it != myset.end(); ++it) {
        std::cout << *it << " ";
    }

    if(myset.find(5) != myset.end()) { // 값 5를 찾습니다.
        std::cout << "\n5 found in set.";
    }
    return 0;
}

 

위의 예제는 unordered_set의 기본적인 사용법을 보여줍니다. unordered_set 객체를 선언하고, insert() 함수를 사용하여 값을 추가하며, find() 함수를 사용하여 특정 값을 찾습니다. 만약 find() 함수가 찾으려는 값을 찾지 못하면, 이 함수는 end()를 반환합니다.

 

단, unordered_set을 사용할 때 명심해야 할 점은, 이 컨테이너는 원소의 순서를 보장하지 않으며, 따라서 원소를 순회할 때마다 원소의 순서가 달라질 수 있다는 점입니다. 이것이 unordered_set을 사용하는 경우에 필요한 주의사항 중 하나입니다.

 

이것으로 unordered_set에 대한 기본적인 사용법을 알아보았습니다. 이 컨테이너는 데이터를 빠르게 검색할 수 있는 특징 때문에 알고리즘 문제를 풀거나 빅데이터를 처리하는 경우에 매우 유용하게 사용될 수 있습니다. 다음 섹션에서는 이 컨테이너의 다양한 사용 사례를 더 자세히 살펴보도록 하겠습니다.

 

9.5.2. unordered_multiset의 사용

unordered_multiset은 STL(Standard Template Library)의 비순차 컨테이너 중 하나로, 중복 값을 허용하는 특징을 가지고 있습니다. unordered_set과 달리, unordered_multiset은 동일한 값을 가진 원소를 여러 개 저장할 수 있습니다.

 

unordered_set처럼 해시 테이블을 기반으로 하므로 원소의 순서는 보장되지 않습니다. 그리고, 원소의 검색, 삽입, 삭제에 있어 일반적으로 O(1)의 시간 복잡도를 가지며, 최악의 경우에도 O(n)의 시간 복잡도를 가집니다.

 

아래의 예제 코드를 살펴보면, unordered_multiset의 사용법을 쉽게 이해할 수 있습니다.

 

[예제]

#include <iostream>
#include <unordered_set>

int main() {
    std::unordered_multiset<int> myset;

    myset.insert(10);
    myset.insert(20);
    myset.insert(20);
    myset.insert(30);

    for(auto it = myset.begin(); it != myset.end(); ++it) {
        std::cout << *it << std::endl;
    }

    std::cout << "Count of 20: " << myset.count(20) << std::endl;
    
    if(myset.find(20) != myset.end()) {
        std::cout << "20 found in set.";
    }
    
    return 0;
}

 

이 예제에서, unordered_multiset은 정수를 저장하며, 20이라는 값은 두 번 삽입됩니다. insert() 함수를 사용하여 값을 삽입하고, count() 함수를 사용하여 특정 값의 개수를 확인할 수 있습니다. find() 함수를 사용하여 특정 값이 unordered_multiset에 있는지 확인할 수 있습니다.

 

unordered_multiset은 중복 값을 허용하므로, 데이터 분석이나 통계 분석 등의 작업에서 유용하게 사용될 수 있습니다. 이 컨테이너는 해시 테이블을 기반으로 하므로 대용량의 데이터를 빠르게 처리할 수 있습니다.

 

그러나 unordered_multiset의 사용에 있어서 주의할 점은 원소의 순서가 보장되지 않는다는 점입니다. 따라서, 순서가 중요한 상황에서는 이 컨테이너를 사용하는 것이 적합하지 않을 수 있습니다.

 

이것으로 unordered_multiset에 대한 기본적인 사용법을 알아보았습니다. 다음 섹션에서는 이 컨테이너의 더 많은 사용 사례와 함께 그 세부 사항을 더 자세히 다루겠습니다.

 

9.5.3. unordered_map의 사용

unordered_map은 STL(Standard Template Library)의 비순차 컨테이너 중 하나로, key-value 쌍을 저장합니다. 내부적으로 해시 테이블을 사용하므로, 원소의 순서를 보장하지 않습니다. unordered_map은 데이터를 검색하는 데에 매우 효율적이며, 일반적으로 O(1)의 시간 복잡도를 가지며, 최악의 경우에도 O(n)의 시간 복잡도를 가집니다.

 

이제 unordered_map을 사용하는 방법에 대해 알아보겠습니다. 아래의 예제 코드를 살펴봅시다.

 

[예제]

#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> mymap;

    mymap["apple"] = 1;
    mymap["banana"] = 2;
    mymap["cherry"] = 3;

    for(auto it = mymap.begin(); it != mymap.end(); ++it) {
        std::cout << it->first << ": " << it->second << std::endl;
    }

    std::cout << "apple's value: " << mymap["apple"] << std::endl;
    
    if(mymap.find("banana") != mymap.end()) {
        std::cout << "banana found in map.";
    }
    
    return 0;
}

 

위의 예제에서 unordered_map은 문자열을 key로, 정수를 value로 사용합니다. key-value 쌍을 추가하는 방법은 간단하게 mymap[key] = value; 구문을 사용하면 됩니다. 그리고 find() 함수를 사용하여 key가 맵 안에 있는지 확인할 수 있습니다. 만약 find() 함수가 찾는 key가 맵 안에 없다면, 이 함수는 end()를 반환합니다.

 

단, unordered_map을 사용할 때 주의할 점은, 이 컨테이너는 원소의 순서를 보장하지 않는다는 것입니다. 따라서 원소를 순회할 때마다 원소의 순서가 달라질 수 있습니다.

 

unordered_map은 알고리즘 문제를 풀거나 빅데이터를 처리하는 경우에 매우 유용하게 사용될 수 있습니다. 이 컨테이너는 데이터를 빠르게 검색하고, 쉽게 추가하거나 삭제할 수 있다는 특징을 가지고 있습니다. 이것으로 unordered_map에 대한 기본적인 사용법을 알아보았습니다. 다음 섹션에서는 이 컨테이너의 더 많은 사용 사례와 함께 그 세부 사항을 더 자세히 다루겠습니다.

 

9.5.4. unordered_multimap의 사용

unordered_multimap은 C++ STL(Standard Template Library)의 비순차 컨테이너 중 하나로, unordered_map과 마찬가지로 키-값 쌍을 저장하지만, 하나의 키에 대해 여러 값이 연결될 수 있는 특징을 가지고 있습니다. 이는 중복 키를 허용한다는 의미입니다.

 

그럼 간단한 예제를 통해 unordered_multimap의 사용법을 알아봅시다.

 

[예제]

#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_multimap<int, std::string> mymap;

    mymap.insert(std::make_pair(1, "Apple"));
    mymap.insert(std::make_pair(2, "Banana"));
    mymap.insert(std::make_pair(1, "Mango"));
    mymap.insert(std::make_pair(3, "Peach"));

    for(auto it = mymap.begin(); it != mymap.end(); ++it) {
        std::cout << it->first << " -> " << it->second << std::endl;
    }

    auto range = mymap.equal_range(1);
    for(auto i = range.first; i != range.second; ++i) {
        std::cout << i->first << " -> " << i->second << std::endl;
    }

    return 0;
}

 

위 코드에서는 unordered_multimap에 정수 키와 문자열 값을 갖는 쌍들을 삽입하였고, 키가 1인 쌍은 두 개 있습니다. 이렇게 unordered_multimap은 동일한 키를 갖는 원소를 여러 개 저장할 수 있습니다.

 

equal_range(key)는 주어진 키에 대응하는 원소의 범위를 반환하는 함수입니다. 이를 통해 동일한 키를 갖는 모든 원소를 쉽게 찾을 수 있습니다.

 

이렇게 unordered_multimap은 원소의 삽입, 삭제, 검색에 O(1)의 시간 복잡도를 가지며, 최악의 경우에도 O(n)입니다. 그러나, 마찬가지로 unordered_multimap은 원소의 순서를 보장하지 않습니다. 따라서 순서에 의존하는 작업에는 부적합할 수 있습니다.

 

또한, unordered_multimap은 해시 테이블을 기반으로 하므로, 메모리 사용이 상대적으로 높을 수 있습니다. 그러나 대용량의 데이터를 빠르게 처리하는데 유용하며, 키-값 쌍을 저장하는 데 있어 중복 키를 허용한다는 특징을 가지고 있습니다.

 

이렇게 unordered_multimap에 대한 기본적인 사용법과 특성을 알아보았습니다.


9.6. 컨테이너의 공통 멤버 함수

모든 STL 컨테이너는 몇 가지 공통적인 멤버 함수를 가지고 있습니다. size()는 컨테이너의 요소 수를 반환하며, empty()는 컨테이너가 비어 있는지 확인하는데 사용됩니다. clear() 함수는 컨테이너의 모든 요소를 제거합니다. 이 외에 insert(), erase(), begin(), end() 등도 공통적으로 제공되며, 컨테이너의 요소에 접근하거나 조작하는 데 필요한 기본 동작을 제공합니다.

9.6.1. 컨테이너 멤버 함수의 종류와 특징

STL 컨테이너는 공통적으로 다양한 멤버 함수를 제공합니다. 이들은 컨테이너의 요소에 접근하고, 수정하고, 제거하는 데 사용됩니다. 이 섹션에서는 주요 멤버 함수의 종류와 그 특징에 대해 논의하겠습니다.

 

size()와 max_size(): size() 함수는 컨테이너에 현재 저장된 요소의 수를 반환합니다. max_size()는 컨테이너가 저장할 수 있는 요소의 최대 수를 반환합니다. 이 수는 시스템 및 컨테이너 종류에 따라 다릅니다.

 

[예제]

std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << "Size: " << vec.size() << std::endl;  // 출력: Size: 5
std::cout << "Max size: " << vec.max_size() << std::endl;  // 출력: Max size: 1073741823 (실제 값은 시스템에 따라 다를 수 있습니다)

 

empty() : empty() 함수는 컨테이너가 비어 있는지를 확인하는데 사용됩니다. 컨테이너가 비어 있으면 true, 그렇지 않으면 false를 반환합니다.

 

[예제]

std::vector<int> vec;
std::cout << std::boolalpha << vec.empty() << std::endl;  // 출력: true
vec.push_back(1);
std::cout << vec.empty() << std::endl;  // 출력: false

 

begin(), end(), rbegin(), rend() : 이들 함수는 컨테이너의 시작과 끝을 가리키는 반복자를 반환합니다. begin()과 end()는 컨테이너의 처음부터 끝까지 순차적으로 접근하는 데 사용되며, rbegin()과 rend()는 컨테이너의 끝에서부터 시작까지 역순으로 접근하는 데 사용됩니다.

[예제]

std::vector<int> vec = {1, 2, 3, 4, 5};
for(auto iter = vec.begin(); iter != vec.end(); ++iter) {
    std::cout << *iter << " ";
}
std::cout << std::endl;
for(auto riter = vec.rbegin(); riter != vec.rend(); ++riter) {
    std::cout << *riter << " ";
}
std::cout << std::endl;


insert(), erase(), push_back(), pop_back() 등 : 이들 함수는 컨테이너에 요소를 추가하거나 제거하는 데 사용됩니다. 각 함수의 동작 방식은 컨테이너의 종류에 따라 달라질 수 있습니다.

 

이 외에도 STL 컨테이너는 다양한 멤버 함수를 제공하고 있으며, 각 함수의 동작은 컨테이너의 종류에 따라 다를 수 있습니다. 따라서 각 컨테이너의 문서를 참조하여 해당 함수의 동작을 이해하는 것이 중요합니다.

 

9.6.2. 컨테이너 멤버 함수 사용 예제

이전 섹션에서는 STL 컨테이너의 주요 멤버 함수들을 간략하게 소개했습니다. 이번 섹션에서는 그 멤버 함수들을 실제 코드에 적용하는 방법을 자세히 살펴보겠습니다.

 

아래에는 std::vector를 사용하여 size(), max_size(), empty(), begin(), end(), rbegin(), rend(), insert(), erase(), push_back(), pop_back() 함수를 사용하는 C++ 코드가 있습니다.

 

[예제]

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;

    // push_back()을 이용한 요소 추가
    for(int i = 1; i <= 5; ++i) {
        vec.push_back(i);
    }

    // size()와 max_size() 함수 사용 예
    std::cout << "Size: " << vec.size() << std::endl;  // 출력: Size: 5
    std::cout << "Max size: " << vec.max_size() << std::endl;  // 출력: Max size: 1073741823 (실제 값은 시스템에 따라 다를 수 있습니다)

    // empty() 함수 사용 예
    std::cout << std::boolalpha << "Empty: " << vec.empty() << std::endl;  // 출력: Empty: false

    // begin(), end() 함수 사용 예
    std::cout << "Elements: ";
    for(auto iter = vec.begin(); iter != vec.end(); ++iter) {
        std::cout << *iter << " ";
    }
    std::cout << std::endl;

    // rbegin(), rend() 함수 사용 예
    std::cout << "Elements in reverse: ";
    for(auto riter = vec.rbegin(); riter != vec.rend(); ++riter) {
        std::cout << *riter << " ";
    }
    std::cout << std::endl;

    // insert() 함수 사용 예
    vec.insert(vec.begin() + 2, 10);
    std::cout << "After insertion: ";
    for(auto elem : vec) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;  // 출력: After insertion: 1 2 10 3 4 5 

    // erase() 함수 사용 예
    vec.erase(vec.begin() + 2);
    std::cout << "After erasing: ";
    for(auto elem : vec) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;  // 출력: After erasing: 1 2 3 4 5 

    // pop_back() 함수 사용 예
    vec.pop_back();
    std::cout << "After pop_back: ";
    for(auto elem : vec) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;  // 출력: After pop_back: 1 2 3 4 

    return 0;
}

 

이렇게 STL 컨테이너의 멤버 함수를 사용하면 컨테이너에 저장된 요소에 대해 다양한 작업을 수행할 수 있습니다. 각 함수의 동작 방식과 사용법을 이해하면 여러분의 코드를 훨씬 더 효율적으로 작성할 수 있습니다. 각 컨테이너의 동작과 함수가 다르므로 해당 컨테이너의 문서를 참조하여 해당 함수의 동작을 이해하는 것이 중요합니다. 이제 다음 섹션에서는 컨테이너가 제공하는 멤버 함수들에 대해 더 자세히 알아보겠습니다.


9.7. 컨테이너와 반복자의 관계 이해하기

STL에서 반복자는 컨테이너의 요소에 접근하는 브리지 역할을 합니다. 반복자를 사용하면 컨테이너의 특정 위치를 가리키거나 컨테이너를 순회할 수 있습니다. 각 컨테이너 타입은 자신만의 반복자 타입을 정의하며, 이를 이용해 컨테이너의 시작부터 끝까지 순회하거나 특정 위치의 요소를 수정하거나 조회할 수 있습니다. 반복자를 이해하는 것은 STL을 효과적으로 사용하는 데 중요한 요소입니다.

9.7.1. 반복자의 개념과 필요성

STL(Standard Template Library)에선 '반복자(iterator)'라는 개념이 매우 중요합니다. 이는 컨테이너에 저장된 요소에 대해 접근하는 방법을 제공하기 때문입니다. 반복자는 특정 컨테이너에 저장된 요소를 가리키는 객체라고 볼 수 있습니다.

 

반복자의 필요성은 컨테이너가 제공하는 기본적인 인터페이스를 확장하려는 욕구에서 비롯됩니다. 기본적으로 컨테이너는 삽입, 삭제, 조회 등의 연산을 제공합니다. 하지만 이러한 연산들은 종종 부족하게 느껴질 수 있습니다. 예를 들어, 컨테이너 내부에서 특정 위치의 요소에 대한 참조를 가져오거나, 컨테이너를 순회하려면 어떻게 해야 할까요? 이런 문제를 해결하기 위해 등장한 것이 바로 반복자입니다.

 

반복자를 사용하면 컨테이너의 모든 요소를 효과적으로 순회할 수 있습니다. 컨테이너에서 제공하는 begin() 및 end() 함수를 사용하면 컨테이너의 첫 번째 요소와 마지막 요소를 가리키는 반복자를 얻을 수 있습니다.

 

[예제]

std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << ' ';
}

 

이 코드는 vector vec의 모든 요소를 출력합니다. it는 반복자로, 컨테이너의 요소를 가리키는 역할을 합니다. *it는 반복자가 현재 가리키는 요소에 대한 참조를 제공하므로, 이를 통해 해당 요소의 값을 출력할 수 있습니다.

 

반복자의 사용법을 익히면 STL 컨테이너를 더욱 쉽고 효과적으로 다룰 수 있습니다. 이로써 다양한 상황에서 효율적인 프로그래밍이 가능해집니다.

 

9.7.2. 반복자의 종류와 사용 방법

STL(Standard Template Library)에서는 다양한 유형의 반복자를 제공합니다. 이들은 컨테이너와 함께 사용되어 컨테이너의 요소를 쉽고 효과적으로 순회할 수 있습니다. 각 반복자의 특징을 이해하고 적절히 사용하면, STL 컨테이너를 보다 유연하게 활용할 수 있습니다.

 

다음은 STL에서 제공하는 주요 반복자의 종류와 사용 방법에 대해 설명합니다.

 

  • 입력 반복자(Input Iterators): 읽기만 가능한 반복자입니다. 한 번 읽은 요소는 다시 읽을 수 없습니다.
  • 출력 반복자(Output Iterators): 쓰기만 가능한 반복자입니다. 한 번 쓴 요소는 다시 쓸 수 없습니다.
  • 전진 반복자(Forward Iterators): 읽기와 쓰기가 모두 가능한 반복자입니다. 또한, 한 번 순회한 요소를 다시 순회할 수 있습니다.
  • 양방향 반복자(Bidirectional Iterators): 전진 반복자의 모든 기능을 가지며, 더 나아가 이전 요소로 이동할 수 있는 기능이 추가되었습니다.
  • 랜덤 접근 반복자(Random Access Iterators): 양방향 반복자의 모든 기능을 가지며, 임의의 위치로 바로 점프하는 기능이 추가되었습니다.

이러한 다양한 반복자들이 제공하는 이유는 컨테이너에 따라 효율적인 요소 접근 방식이 다르기 때문입니다. 예를 들어, 연결 리스트는 요소 사이를 순방향 또는 양방향으로 이동하는 것이 적합하며, 벡터는 랜덤 접근이 가능합니다.

 

이제 vector에서 반복자를 어떻게 사용하는지에 대한 예를 살펴보겠습니다.

 

[예제]

std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    *it = *it * 2;  // 요소의 값을 2배로 변경
}

 

위 예제에서는 vector의 반복자를 사용하여 컨테이너의 모든 요소를 2배로 만드는 작업을 수행했습니다. vector의 반복자는 랜덤 접근 반복자이므로, 순회 외에도 임의의 요소에 즉시 접근할 수 있습니다.

 

반복자를 이해하고 사용하면 컨테이너의 요소를 보다 효과적으로 조작할 수 있습니다. 이는 STL 컨테이너의 강력한 기능을 최대한 활용할 수 있도록 해줍니다.

 

9.7.3. 컨테이너와 반복자의 상호작용 이해하기

여기에서는 컨테이너와 반복자가 서로 어떻게 작동하는지 이해하고, 실제 코드 예제를 통해 그 관계를 이해하는 것이 목표입니다. 반복자를 통해 컨테이너의 원소에 접근하고 수정하는 방법을 배우게 될 것입니다.

1) 컨테이너와 반복자의 상호작용 이해

컨테이너는 원소들의 집합을 저장하고, 반복자는 그 원소들을 순회하는 데 사용됩니다. 반복자는 포인터와 유사한 개념으로, 원소들에 대한 접근 권한을 제공합니다. 어떤 반복자는 읽기만 가능할 수 있고(const_iterator), 어떤 반복자는 읽고 쓰기가 가능할 수 있습니다(iterator). 반복자를 통해 원소를 수정하려면 컨테이너에 non-const 반복자를 사용해야 합니다.

 

반복자를 얻는 가장 간단한 방법은 컨테이너의 begin()와 end() 함수를 사용하는 것입니다. begin() 함수는 컨테이너의 첫 번째 원소를 가리키는 반복자를 반환하고, end() 함수는 컨테이너의 마지막 원소 다음을 가리키는 반복자를 반환합니다. 이는 컨테이너가 비어 있을 때를 대비하여, 실제 마지막 원소가 아닌 마지막 원소 다음을 가리키도록 설계되었습니다. end()가 반환하는 반복자는 사용하지 않고, 단지 범위의 끝을 나타내는 용도로 사용됩니다.

 

2) 컨테이너와 반복자 상호작용의 예제

아래에 std::vector 컨테이너와 반복자를 사용하는 간단한 예제를 제시하겠습니다.

 

[예제]

std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int>::iterator it = vec.begin();

while (it != vec.end()) {
    std::cout << *it << " ";  // 반복자를 통한 원소 접근
    ++it;  // 반복자 이동
}

 

위의 예제에서는 std::vector의 반복자를 이용해 벡터의 모든 원소를 출력합니다. 여기서 it는 벡터의 반복자로, *it는 반복자가 가리키는 원소의 값을 의미합니다. 반복자 it는 벡터의 처음부터 시작해서, ++it를 통해 다음 원소로 이동하고, 벡터의 끝(vec.end())에 도달할 때까지 원소를 출력합니다.

 

3) 컨테이너와 반복자의 심화적인 상호작용

이번에는 좀 더 복잡한 예제를 보겠습니다. 이 예제에서는 std::list 컨테이너와 그 반복자를 사용합니다. std::list는 이중 연결 리스트를 구현한 STL 컨테이너로, 양방향 반복자를 지원합니다. 이 예제에서는 반복자를 사용하여 리스트의 원소를 거꾸로 출력해 보겠습니다.

 

[예제]

std::list<int> lst = {1, 2, 3, 4, 5};
std::list<int>::reverse_iterator rit = lst.rbegin();

while (rit != lst.rend()) {
    std::cout << *rit << " ";  // 반복자를 통한 원소 접근
    ++rit;  // 반복자 이동
}

 

위의 예제에서는 std::list의 역방향 반복자(reverse_iterator)를 이용해 리스트의 모든 원소를 거꾸로 출력합니다. 이는 양방향 반복자를 지원하는 컨테이너에서만 가능한 작업입니다. rbegin()은 리스트의 끝 원소를 가리키는 반복자를 반환하고, rend()는 리스트의 첫 원소 이전을 가리키는 반복자를 반환합니다.

 

이 예제를 통해 컨테이너와 반복자 사이의 상호 작용을 보다 심화하여 이해할 수 있게 되었습니다. 다양한 컨테이너와 그에 맞는 반복자를 이해하고 사용하면, C++ 프로그래밍에서 효율적이고 강력한 코드를 작성할 수 있게 됩니다.

 

 

 

2023.06.03 - [GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~] - [C/C++ 프로그래밍 : 중급] 8. 예외 처리와 오류 처리

 

[C/C++ 프로그래밍 : 중급] 8. 예외 처리와 오류 처리

Chapter 8. 예외 처리와 오류 처리 예외 처리는 프로그램이 실행 중에 발생하는 예외적인 상황들, 즉 에러를 대처하는 방법에 대해 알아봅니다. 또한, 사용자가 직접 예외를 정의하고 사용하는 방

gdngy.tistory.com

 

반응형

댓글