하찮은 19학번 컴공대생
yoonbot's devlog

알고리즘 저장소 (Algorithm Library) ⏱/Data Structures (자료구조)

< C++ > Vectors (벡터)

yoonbot_code 2021. 8. 12. 02:49

해당 계시물은 내가 벡터에 대한 주제를 아래와 같이 이해 했으니 정확하지 않을 수도 있다. 

 

자료구조 처음 접하자마자 바로 배열 (Array)로 복습 들어가봤을 것이다. 그 중에서 고정 배열 (static arrays)와 통적 배열(dynamic)에 대해서 접해 봤을 것이다. 씨언어의 경우 통적 배열의 크기를 조절하려면 수동으로 할당 (malloc, realloc, dealloc) 대해서 배웠을 것이다. 씨플플 언어에서는 씨언어와 비해 더 쉽게 수동으로 new 와 delete 키워드로 메모리 할당이 비교적으로 더 수월하다.

 

또한 씨플플은 씨언어와 다르게  vector 템플릿 클래스로 자동으로 배열 메모리 할당이 가능하다. 이번 글은 vector로 메모리 할당을 살펴보고 다음 글에서는 array템플릿 클래스로 고정 배열을 최적화 시키는 법을 알아보겠다.

 

벡터를 선언하는 방법들은 다음과 같다.

 

#include <vector>
...
using namespace std;
vector<int> vi;          // int vi[0] 과 유사하다
int n;
cin >> n;
vector<double> vd(n);    // double vd[n] 과 유사하다

 

위에서 언급했듯이 벡터는 자동으로 배열을 할당 시키기 때문에 위의 예시처럼 벡터를 선언 할 때 사이즈를 0으로 설정해도 괜찮다. 요소들이 들어갈 수록 벡터는 자동으로 사이즈를 늘리고 요소들이 삭제될 수록 벡터는 자동으로 사이즈를 줄인다. 하지만 요소들을 추가하고 삭제하는 방법들도 살펴봐야한다.

 

1. 벡터 기능들 1 - 제어자 (Modifiers)

 

말 그대로 배열의 요소들을 제어하고 배열의 내용물을 수정할수 있다. 이 기능들 중에서 스텍의 팝, 푸시 기능들이 낯이 꽤 익혀있을 것이다.

 

1. insert(vectorName.begin() + index, value) - 해당 벡터의 위치에다 값을 저장하고 원래 있던 값은 한 칸 뒤로 이동하며 벡터의 크기가 한 칸 더 늘어진다. 

2. erase(vectorName.begin() + index) - 해당 벡터의 위치에 저장된 값을 저장하고 원래 있던 값은 한 칸 앞으로 이동하며 벡터의 크기가 한 칸 더 작아진다. 

3. swap(vectorName) - 벡터1과 벡터2의 내용물을 서로 바꿔치기 한다.

4. clear() - 배열의 내용물 전체 다 삭제한다.

5. emplace(vectorName.begin() + index, value) - insert 과 다른 방식으로 해당 벡터의 위치에다 값을 저장한다

6. emplace_back(value) - 배열의 끝 위치에 입력할 값을 삽입한다. 

7. push_back(value) - 입력할 값을 배열 맨 앞에다 삽입한다

8. pop_back() - 제일 끝에 저장되어있는 값을 삭제한다.

 

위 기능들을 응용하는 예시는 아래와 같다.

 

#include <vector>
using namespace std;

int main(){
    vector<int> vec;
    vector<int> vec2 { 10, 11, 12, 13 };
    for (int i = 1; i <= 5; i++)
        vec.push_back(i);

    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  2  3  4  5

    vec.insert(vec.begin() + 1, 6) // vec[1] 에 값 2를 삽입
    for (auto i = vec.cbegin(); i != vec.cend(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  6  2  3  4  5

    vec.pop_back();
    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  6  2  3  4

    vec.erase(vec.begin() + 2);
    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  6  3  4

    vec.emplace(vec.begin(), 7)
    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  7  1  6  3  4

    vec.emplace_back(8);
    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  7  1  6  3  4  8

    vec.assign(3, 10);
    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  10  10  10  3  4  8

    vec.swap(vec2);
    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  10  11  12  13

    vec.clear();
    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력: NAN (아무것도 출력되지 않음)

    return 0;
}

 

여기서 insert 와 emplace 는 컨테이너에 데이터를 삽입 하는 행위가 다르다. insert 는 데이터를 삽입 시킬 때 내부에서 복제 하고 이동하는 동작들이 있다. 예를 들면 그 중에서 std::make_pair을 이용해야한다. 하지만, emplace 는 내부에 추가 동작이 다르면서도 더 간단하게 삽입이 가능하다. 

 

2. 벡터 기능들 2 - 반복자 (Iteration)

 

주로 for이나 while loop 반복문에 사용된다. 예를 들어 반복자가 vectorName.begin()에서 시작하고 vectorName.end()까지 for/while loop이 반복 할 수도 있고 주로 배열의 내용물을 제어하거나 출력을 하는데 사용된다.

 

1. begin() - 배열의 첫 번째 요소를 가르킨다.

2. end() - 배열의 끝 요소를 가르킨다.

3. rbegin() - 배열의 끝 처음으로 가르킨다.

4. rend() - 배열의 첫 번째 요소가 을 맨 마지막에 가르킨다.

5. cbegin() - 상수 반복자가 배열의 첫 번째 요소를 가르킨다.

6. cend() - 상수 반복자가 배열의 끝 요소를 가르킨다.

7. crbegin() - 상수 반복자가 배열의 끝 요소를 처음으로 가르킨다.

8. crend() - 상수 반복자가 배열의 첫 번째 요소를 맨 마지막에 가르킨다. 

 

위 기능들을 응용하는 예시는 아래와 같다.

 

#include <vector>
using namespace std;

int main(){
    vector<int> vec;

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

    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  2  3  4  5

    for (auto i = vec.cbegin(); i != vec.cend(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  2  3  4  5

    for (auto i = vec.rbegin(); i != vec.rend(); ++i)
        cout << *i << " ";
        // 결과 출력:  5  4  3  2  1

    for (auto i = vec.crbegin(); i != vec.crend(); ++i)
        cout << *i << " ";
        // 결과 출력:  5  4  3  2  1

    return 0;
}

 

여기서 cbegin()과 begin()의 차이점은 반복자 타입이다. 반복자가 cbegin, 혹은, crbegin일 경우 상수 타입이며 벡터에 사전에 저장된 값들을 변환 시킬 수 없다. 하지만 그냥 일반적인 begin - end일 경우 변수 타입이며 벡터의 내용물을 언제든지 변환이 가능하다.

 

3. 벡터 기능들 3 - 용량 관리 (Capacity Management)

 

이 기능들로 배열의 크기를 조절 할 수 있으며 배열의 크기에 대한 정보도 불러올 수 있게해 준다.

 

1. size() - 배열의 크기를 리턴 한다. 

2. max_size() - 배열에 저장할 수 있는 최대 요소 개수들을 리턴 한다. (항상 46116860184273987903 개의 요소들로 지정됨)

3. capacity() - 현재 배열에 저장할 수 있는 요소 개수들을 리턴 한다. 

4. resize(size) - 배열의 크기를 재조절 시켜준다. 

5. empty() - 배열이 현재 비어있는지 확인 시켜준다. 

6. shrink_to_fit() - 배열에 남는 공간이 없도록 배열을 크기 만큼 정확히 조절해 준다.

7. reserve(size) - 적어도 size 만큼 배열의 공간을 크게 조절 시켜주도록 한다. 

 

위 기능들을 응용하는 예시는 아래와 같다.

 

#include <vector>
using namespace std;

int main(){
    vector<int> vec;

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

    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  2  3  4  5

    cout << vec.size();
    // 결과 출력: 5

    cout << vec.capacity();
    // 결과 출력: 8

       cout << vec.max_size();
    // 결과 출력: 4611686018427387903

    vec.resize(4);
    cout << vec.size();
    // 결과 출력: 4

    vec.shrink_to_fit();
    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  2  3  4

    if (vec.empty() == false)
        cout << "Not empty";
        // 결과 출력: Not empty

    return 0;
}

 

4. 벡터 기능들 4 - 요소 접근/엑세스 (Element Access)

배열 대해서 처음 접근해 봤을 때 주로 index에서 저장된 배열의 값을 조회 하는 법을 배웠을 것이다. 이 와 마찬가지로 Index의 위치에 저장된 배열의 값을 조회 및 접근해 주는 기능들이다.

 

1. reference operator [g] - 이 기능은 널리 알려져 있는 arrayName[index] 방식으로 벡터의 값을 찾는다. 

2. at(g) - 위의 기능이랑 정확히 똑같은 기능을 가지고 있다. 

3. front() - 제일 첫 번째 요소의 위치에서 요소에 참조를 반환한다.

4. back() - 제일 끝 요소의 위치에서 요소에 참조를 반환한다. 

5. data() - 배열에는 다이렉트 포인터가 있으면서 data() 는 벡터의 다이렉트 포인터를 리턴해 준다. 

 

위 기능들을 응용하는 예시는 아래와 같다.

 

#include <vector>
using namespace std;

int main(){
    vector<int> vec;

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

    for (auto i = vec.begin(); i != vec.end(); ++i)
        cout << *i << " ";
        // 결과 출력:  1  2  3  4  5

    cout << vec[2];
    // 결과 출력: 3

    cout << vec.at(4); 
    // 결과 출력: 5

    cout << vec.front();
    // 결과 출력: 1

    cout << vec.back();
    // 결과 출력: 5

    cout << vec.data();
    // 결과 출력: 1

    return 0;
}

 

여기서 주의해야 할 점은 begin() 과 front()를 헷갈리지 말아야 할 것! begin()은 반복자이며 반복문 안에서만 이용이 가능하고 front()는 반복문 외에서 첫 번째 요소의 위치에서 요소에 참조를 반환한다. 

 

 

위에 설명한 기능들 중 1/5은 std c++ 11 확장이 필요함으로 std c++ 11이 제대로 설치가 되어있는지 확인하도록!