Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

<a href="/modules/moniwiki/wiki.php/manSearch?google=none&name=STL">STL</a>(2) - Seuences 컨테이너

STL(2) - Seuences 컨테이너

윤 상배

dreamyun@yahoo.co.kr


차례
1절. Container 에 대해서
2절. Sequence 컨테이너
2.1절. Sequence 컨테이너란
2.1.1절. vector
2.1.1.1절. 특징
2.1.1.2절. 선언
2.1.1.3절. 자주 사용되는 멤버 함수들
2.1.2절. deque
2.1.2.1절. 특징
2.1.2.2절. 선언
2.1.2.3절. 자주 사용되는 멤버 함수들
2.1.3절. list
2.1.3.1절. 특징
2.1.3.2절. 선언
2.1.3.3절. 자주 사용되는 멤버 함수들
3절. 결론

1절. Container 에 대해서

STL 을 설명하면서 등장하는 컨테이너(저장소)란 객체를 원소로 저장할수 있는 영역을 말한다. 이는 특정 데이타 타입만을 저장할수 있는 배열과는 상당히 틀린개념이다. C++(혹은 객체지향플밍) 을 해봤다면 알겠지만 단순히 말해서 객체란 묘사될수 있는 모든 사물을 말한다. 이러한 객체를 원소로 저장할수 있다는 특성때문에 컨테이너는 모든 종류의 데이타저장이 가능하도록 되어있다. 이러한 특성은 앞의 STL(1)에서 언급했듯이 C++ Template 을 이용해서 구현된다(더 자세히 말하자면 Class Template).

STL 은 다양한 자료구조로 사용될수 있는 약 10가지 정도의 컨테이너들을 제공한다. 여기에는 배열을 일반화 시킨 vector, queue 로 사용가능한 deque, 요소를 가르키는 첨자로 int 형대신에 다른 형(char * 이나 string)을 사용할수 있는 map 등.. 이있으며, 이러한 다양한 컨테이너들을 사용하여서 쉽게 확장 가능한 자료구조를 단 몇줄의 코딩으로 쉽게 구현할수 있다.

이러한 10가지 정도 되는 컨테이너들은 다시 Sequence 컨테이너와, Association 컨테이너로 크게 나눌수 있다. 앞으로 이 두가지 종류의 컨테이너를 기준으로 해서 다양한 컨테이너들의 사용방법에 대해서 알아보도록 할것이다. 그러나 이번문서에서는 Sequence 컨테이너만을 다룰 것이며 Association 컨테이너에 대한 내용은 다음 기사에서 다루게 될것이다.


2절. Sequence 컨테이너

2.1절. Sequence 컨테이너란

Sequence 컨터이너에는 vector, deque, list, stack 등이 포함되어 있다. 이들을 Sequence 라고 부르는 이유는 배열과 같이 선형적으로 데이타가 나열되기 때문이다. 각 컨테이너의 이름에서 눈치를 챘겠지만 deque 는 queue 자료구조를 위해서, list 는 linked list, stack 는 stack 자료구조를 지원할수 있는 컨테이너이다. 이들 Sequence 컨테이너들은 배열과 매우 비슷한 성질을 지니고 있으며, 배열처럼 첨자로 접근이 가능하다.

컨테이너들은 대부분 공통의 인터페이스(함수)를 제공한다. 함수의 이름도 똑같고, 하는일도 같은경우가 상당히 많다. 그러므로 하나의 컨테이너를 사용하는 법만 확실하게 익혀도, 나머지 컨테이너들은 별로 어렵지 않게 익힐수 있을것이다. (예를들어 vector 에서 마지막에 원소 입력을 위해서 쓰이는 push_back() 은 list, deque, stack 등에도 존재하며, 동일한 일을 한다.


2.1.1절. vector

2.1.1.1절. 특징

vector 은 map 과 함께 가장 자주 사용되는 컨테이너이다. 단순히 배열을 모든 자료형을 원소로 가질수 있도록 일반화 시킨것이라고 보면된다. 배열과 같이 첨자연산이 가능하다. 각각의 요소에 랜덤하게 접근이 가능하며, vector 의 마지막 요소를 삭제하는데는 상수시간이 소요된다. 반면 처음과 중간에 있는 요소를 삭제하는데에는 linear time(선형시간) 이 소요된다.

처음과 중간에 있는 요소를 삭제하는데에 linear time 이 걸리는 이유는 배열과 매우 비슷하게 만약 중간에 있는 요소를 삭제 했을경우 그 뒤에 있는 요소들을 모두 한칸씩 앞으로 밀어넣어야 하는 연산을 필요로 하기 때문이다(즉 마지막에 요소를 추가하거나 삭제할때 효율적임).

그러므로 만약 중간에 있는 요소를 자주 삭제하는 연산이 필요할경우에 vector 를 사용하는건 매우 비효율적이 될수 있다. 이럴경우에는 list 같은 다른 컨테이너를 사용해야 할것이다.

STL에서 제공하는 다른 컨테이너들과 마찬가지로 vector 도 자신이 알아서 메모리 관리를 한다.


2.1.1.2절. 선언

vector 컨테이너는 다음과 같이 선언되어 있다.

표 1. 템플릿 Parameters

Parameter설명기본값
T백터의 원소로 들어가게될 값의 (type)이다. 이 값은 객체가 될수도 있다. 
Alloc벡터의 할당기(allocator)이다. 이 할당기를 통해서 자동으로 메모리 관리를 한다.alloc
보통은 기본 할당기를 사용하게 됨으로 다음과 같이 벡터를 만들어서 사용할수 있다.
vector<int> myint;
vector<class myobj> myobj;
vector<vector <myint> > v_myint; 
					
위의 선언을 보면 알겠지만 int 같은 자료형은 물론이고 class, 또다른 컨테이너까지도 멤버 요소로 가질수 있다. vector의 vector 을 사용하면 마치 2차원 배열처럼 사용할수 있을것이다.

또한 vector 은 최초에 생성될때, 기본적인 크기와 채워질 값을 지정할수 있다.

vector<string> mydata(20, "hello");
					
위의 경우 mydata 의 크기는 20만큼 크기가 미리 할당되고, "hello"로 모두 초기화 될것이다.


2.1.1.3절. 자주 사용되는 멤버 함수들

위에서 보면 vector 를 단지 특정 (type)에 관계없이 멤버 요소를 가질수 있는 배열 정도로만 치부할수도 있을것이다. 그러나 vector 은 클래스 템플릿의 특징을 살려서 매우 다양한 함수들을 제공하며, 우리는 이러한 함수들을 통해서 별도로 코딩할필요 없이 원하는 작업을 할수 있게 된다.

이러한 함수들은 굉장히 많기 때문에 비교적 사용빈도가 높은 것들만을 설명하도록 하겠다.


2.1.1.3.1절. 위치 관련

vector 에서 원소에 접근하기 위한 가장 간단한 방법은 배열과 같이 배열첨자를 이용하는 방법이다. 그러나 이와는 달리 포인터와 같이 접근이 가능하다. 이러한 접근을 위해서 iterator 라는 것을 제공한다.

iterator 는 포인터와 매우 비슷하게 원소의 위치를 가르키는 역활을 한다. iterator 는 다음과 같이 선언되어서 사용할수 있다.

#include <vector>
#include <string>
 
int main()
{
    vector<string> mydata;
 
    // iterator 을 만든다. 
    // iterator 로 순환(참조)할 컨테이너의 타입에 맞게 만들어준다. 
    vector<string>::iterator mi; 
 
    mydata.push_back("hello");
    mydata.push_back("world");
    mydata.push_back("ok");
 
    mi = mydata.begin();  // mydata  의 첫번째 원소의 위치를 가르킨다. 
    while ( mi != mydata.end())
    {
        cout << *mi <<endl;
        mi++;
    }
    cout << mydata[0] << endl;   // 배열과 같이 첨자로 접근 가능하다. 
}
						
begin()은 vector 컨테이너의 처음위치를 가르키는 iterator 를 넘겨주며 end()를 통해서 마지막위치를 가르키는 iterator 를 가져올수 있다. 위의 예를 보면 iterator 가 pointer 와 아주 유사함을 알수 있다. 포인터 처럼 다음 위치를 가르킬수도 있으며 증가연산도 사용할수 있다.
iterator begin();
iterator end();
						
이들 begin() 과 end()는 다른 모든 컨테이너에도 동일하게 적용된다.


2.1.1.3.2절. 삽입/삭제 관련

vector 은 삽입과 관련해서 2개의 함수를 제공한다. insert 와 push_back 인데, 이들함수는 다른 sequence 컨테이너에 같은이름으로 제공되고 있으며, 자주 사용되는 함수들임으로 익혀두도록 하자. push_back 는 컨테이너의 마지막에 원소를 삽입할때 사용하며, insert 는 임의의 지역에 원소를 삽입하고자 할때 사용한다.

앞에서 말했다 시피, vector 의 경우 중간에 원소를 삽이시키는건 상당히 비효율적이다. 삭제할때와 마찬가지로 중간에 insert 시킬경우 insert 위치의 뒤에 있는 모든 원소의 위치가 재 조정되기 때문이다. 즉 insert 위치의 뒤에 100개의 원소가 더 있다면, 100번의 복사가 더 일어나게 된다.

void push_back(const T&);
iterator insert(iterator pos, const T& x);
						
push_back() 는 그냥 T 타입의 데이타를 밀어 넣기만 하면 그걸로 끝이다. insert 는 insert 할 위치를 iterator 를 통해서 지정해 주어야 한다.
vector<string> mydata;
 
mydata.push_back("hello");
mydata.push_back("world");
mydata.push_back("ok");
 
// hello 다음에 "yundream" 을 삽입한다.
mydata.insert(mydata.begin()+1, "yundream");  
 
// 혹은 iterator 을 선언해서 삽입가능하다.  
vector<string>::iterator mi;
mi = mydata.begin(); 
mydata.insert(mi + 2, "hahaha");
						
삭제관련 함수로는 pop_back() 과 erase(), clear()가 있다. pop_back()는 마지막 원소를 삭제할때 사용하며, erase()는 임의의 위치에 있는 원소를 삭제할때 사용된다. erase() 는 또한 지정된 범위에 있는 원소들을 지울수도 있다. clear()는 모든 원소를 지울때 사용된다.
void pop_back();
iterator erase(iterator pos);
iterator erase(iterator first, iterator last);
						
clear()는 erase(begin(), end()) 와 동일하게 쓰일수 있을것이다.

swap()을 이용하면 현재 벡터의 내용을 다른 벡터로 대신 채울수 있다.

void swap(vector&); 
						
vector<string> ok1, ok2;
...
...
ok1.swap(ok2); // ok1 의 내용을 ok2로 덮어쓴다.  
						


2.1.1.3.3절. 원소의 크기관련

size()를 통해서 현재 vector 컨테이너에 몇개의 원소가 들어있는지 계산해 낼수 있다. empty()를 이용하면 현재 원소가 있는지 없는지를 알수 있다. 실제로 size() 를 통해서 원소가 비어있는지 알수도 있지만, empty()가 더 효율적이므로 단지 컨테이너가 비어있는지만 확인하고자 한다면 empty() 를 사용하도록 한다.

size_type size();
bool empty();
						
iterator 대신 size()를 이용해서 vector 컨테이너의 모든 원소를 가져올수도 있다.
vecotr<string> mydata;
mydata.push_back("1");
mydata.push_back("2");
mydata.push_back("3");
mydata.push_back("4");
for(int i = 0; i < mydata.size(); i++)
{
    cout << mydata[i] << endl;	
}
						

vector 의 경우 원소의 갯수가 늘어나면 자동적으로 컨테이너의 크기가 조절된다고 했는데, 하나의 원소가 들어올때 마다 컨테이너의 크기를 조절되는 것보다는, 어느정도 크기를 미리할당 해놓고 원소를 받아들이는게 훨씬더 효율적일 것이다.

이처럼 초기에 vector 의 크기를 어느정도 지정해주어서 크기조절(메모리할당)을 최소한도로 일어도록 조절할수 있다. reserve() 함수를 이용해서 이러한 일들을 할수 있다. 한마디로 말해서 사용자가 사용될 메모리의 양을 미리 계산해서 메모리의 크기를 재할당(reallocation)하는 거라고 보면 된다. 현재 할당된 메모리의 양(받아들일수 있는 원소의 크기)은 capacity() 함수를 이용해서 알아낼수 있다. reserve()에 의한 메모리 할당은 성능에 중요한 영향을 미치므로 vector 뿐만 아니라 다른 컨테이너를 쓸때는 어느정도의 원소를 이용할것인지를 예측해서 적당한 크기를 할당하도록 하자.

void reserve(size_type n);
size_type capacity() const;
						
만약 reserve() 로 크기를 충분히 잡지 않아서 원소의 갯수가 초과했을경우에는 현재 capacity()*2 만큼의 메모리가 한꺼번에 할당된다. 처음에 reserve(20) 을 잡았다면 원소가 20을 초과했을경우에는 20*2 만큼이 할당될것이다. 이것이 초과된다면 다음번에는 40*2 만큼이 할당된다.


2.1.2절. deque

2.1.2.1절. 특징

이름에서도 알수 있겠지만 queue 자료구조를 위해 최적화 되었다. 기본적인 사용방법은 vector 와 매우 같으며, vector 와는 달리 컨테이너의 맨 앞에서 행해지는 삽입/삭제에 대해서 매우 빠른 연산(상수시간)이 가능하다. 보통의 queue 자료구조는 맨앞에 원소의 삽입을 하지 않는게 원칙이지만 deque 는 맨앞에 원소의 삽입도 지원한다.

이는 자료구조의 맨의 요소를 빈번하게 읽고/삭제 해야하는 queue 의 특성을 효율적으로 지원할수 있어야 하기 때문이다.

이외에는 vector 와 완전히 사용방법이 똑같다고 보면 된다. vector 과 마찬가지로 각 요소에 랜덤하게 접근가능하다. vector 로 만들어진 코드를 dequeu 로 변경하고 싶으면 선언에서 vector 만 deque 로 변경시켜 주면 된다.

하지만 중간에 요소를 삽입/삭제하는데에는 vector 와 같이 선형시간이 소비된다. 중간에 요소 삽입 ,삭제 작업이 많이 요구된다면 vector 이나 deque 대신 list 를 쓰는게 더욱 효율적이다. vector 과 마찬가지로 생성되면서 할당기가 만들어져서 자동적으로 메모리를 관리한다.


2.1.2.2절. 선언

vector와 같다.

표 2. 템플릿 Parameters

Parameter설명기본값
Tdeque의 원소로 들어가게될 값의 (type)이다. 이 값은 객체가 될수도 있다. 
Allocdeque의 할당기(allocator)이다. 이 할당기를 통해서 자동으로 메모리 관리를 한다.alloc
기본할당기를 사용함으로 다음과 같이 deque 를 만들어서 사용할수 있다.
deque<int> myint;
deque<class myobj> myobj;
deque<vector <myint> > v_myint; 
					


2.1.2.3절. 자주 사용되는 멤버 함수들

모든 사용방법 심지어는 함수 이름들까지도 vector와 똑같다. 위의 vector 를 설명한곳에서 vector 을 deque로만 변경시키면 된다.

vector 에서는 제공되지 않는 함수로 pop_front()와, push_front()가 있다. pop_front()는 맨앞의 원소를 삭제할때, push_front() 는 맨앞에 원소를 삽입할때 사용한다.

void push_front(const T&);
void pop_front();
					
궁금한게 하나 있다. 왜 vector 에서는 push_front 나 pop_front 를 지원하지 않는것일까. 지원하려고 맘만 먹으면 insert()나 erase() 등을 이용해서 충분히 구현할수 있을건데.. 그 이유는 vector에서 이러한 연산은 대단히 비효율적이기 때문이다. 그래서 처음부터 프로그래머 이러한 비효율적인 접근을 차단하기 위한 목적으로 이들 함수를 만들어 놓지 않은 것이다.
deque<int> myque;
 
myque.push_back(1);
myque.push_back(2);
myque.push_back(3);
myque.push_back(4);
myque.push_back(5);
 
cout << myque.size() << ":" << myque[0] << endl;
myque.push_front(0);
cout << myque.size() << ":" << myque[0] << endl;
 
myque.pop_front();
cout << myque.size() << ":" << myque[0] << endl;
myque.pop_front();
cout << myque.size() << ":" << myque[0] << endl;
 
deque<int>::iterator mi;
mi = myque.begin();
 
while( mi != myque.end())
{
    cout << *mi << endl;
    *mi++;
}
					


2.1.3절. list

2.1.3.1절. 특징

STL 에서 제공하는 list 는 double linked list 를 일반화 시킨 템플릿 버젼이라고 생각하면 된다. double linked list 의 특징을 또한 그대로 가지고 있다. vector 와 deque 는 임의 접근이 가능하다고 했다. 이는 vector, deque 가 배열의 특징을 가지기 때문으로 첨자를 이용한 임의 접근이 가능하다. 그러나 list 는 이러한 임의 접근을 할수가 없다(doublke linked list 도 마찬가지). 대신에 중간에서의 요소 삽입/삭제를 매우 효율적으로 처리할수 있다. vector, deque 와 같은 메모리 연산이 일어나지 않기 때문이다. 단지 다음요소를 가르킬수 있도록 포인터만 재조정해 주면 되기 때문이다.

임의 접근이 안되므로 당연히 첨자를 이용해서는 요소에 접근할수 없다. 단지 이터레이터를 이용해서 접근이 가능하다. double linked list 의 요소에 접근하기 위해서는 포인터를 이용해야 하는것과 마찬가지이다.

임의 접근이 포기되는대신에 linked list 의 특징을 이용할수 있는 다양한 다른 기능들이 함수로 제공되어 진다.

그러나 list 도 단점이 있는데, 특 특정 원소에 접근하기 위해서는 처음부터 원소를 쭉 찾아서 접근해야 한다는 점이다. 임의 접근이 안된다. 그러므로 원소들이 서로 멀리 떨어져 있는 상황에서 연산을 하게 되면 비효율적이 될수가 있다.


2.1.3.2절. 선언

다른 vector 와 deque 와 동일하다.

표 3. 템플릿 Parameters

Parameter설명기본값
Tdeque의 원소로 들어가게될 값의 (type)이다. 이 값은 객체가 될수도 있다. 
Allocdeque의 할당기(allocator)이다. 이 할당기를 통해서 자동으로 메모리 관리를 한다.alloc


2.1.3.3절. 자주 사용되는 멤버 함수들

list 의 linked list 로써의 특징때문에 원소의 삽입/삭제에 대해서 가장 폭넓은 지원을 해준다. push_back(), push_pop(), pop_front(), pop_back() 등의 모든 함수들을 지원하며, 중간에 요소를 입력하기 위한 erase(), insert() 도 지원한다. 물론 erase 와 insert 는 vector 과 deque 에서도 지원하지만 효율에서 많은 차이가 난다. list 는 이러한 모든 연산을 상수시간에 끝낼수 있다. 링크만 다시 만들면 되기 때문이다.

이렇게 메모리 재배치를 통하지 않고 링크 재배치만을 통해서 요소의 삽입/삭제가 가능하다는 장점때문에 다른 컨테이너에서는 제공하지 않는 몇가지 특별한 함수들을 제공한다.

void reverse();
void splice(iterator position, list<T, Alloc>& x);
					
reverse() 는 원소를 역으로 재배치하고, splice() 는 iterator 로 주어진 포지션부터 x list 를 결합시킨다. 이러한 모든 재배치 과정은 단지 링크만 재조합하는 것으로 상수시간에 이루어진다.
#include <list>
 
int main()
{
    list<int> mylist;
    list<int> mylist2;
 
    mylist.push_back(0);
    mylist.push_back(1);
 
    mylist2.push_back(2);
    mylist2.push_back(3);
 
    // mylist 마지막에 mylist2 를 붙인다. 
    mylist.splice(mylist.end(), mylist2);
 
    list<int>::iterator mi;
    mi = mylist.begin();
 
    while( mi != mylist.end())
    {
        cout << *mi << endl;
        *mi++;
    }
 
    // mylist 를 역으로 재배치 한다. 
    mylist.reverse();
    mi = mylist.begin();
 
    while( mi != mylist.end())
    {
        cout << *mi << endl;
        *mi++;
    }
}
					


3절. 결론

이상 sequence 컨테이너에 대해서 알아보았다. 보면 알겠지만, 얼마나 유연하게 자료구조를 구현할수 있는가를 알수 있을것이다. 링크드 리스트이건 큐이건 스택이건 간에 고민없이 구현가능하다.

이와 함께 나중에 사용될 저너릭 알고리즘을 사용하면 자료구조에 각종 알고리즘까지 적용시켜서 사용할수 있다.

컨테이너를 사용할때 가장중요한것은 작업에 가장 효율적인 컨테이너를 사용해야 한다는 것이다. 사실 vector 만으로도 list, deque 를 구현가능하다. 그럼에도 list 와 deque 가 존재하는 것은 각각의 자료구조에 대한 요소의 연산에 적당한 컨테이너가 따로 존재하기 때문이다.

배열을 일반화 시켜서 간단하게 사용하고 싶다면 vector 을 queue 와 같이 처음 과 마지막에 요소를 추가/삭제 시켜야 한다면 deque 를 linked list 와 같은 복잡한 요소의 연산을 필요로 한다면 list 를 사용하면 된다.

여기에서 다룬 내용들은 Sequence 컨테이너의 기능중 일부분이다. 자세한 내용은 STL 홈페이지 를 참고하도록 하자.