set는 key와 value가 일치하는 map이다 라고 이해하면 된다.

set는 가끔 활용한다. (예: 찾은 몬스터 ID의 목록을 가져와야 하는데 그 목록중에 찾고싶은 몬스터가 있는지 빠르게 찾아야 할 때, 이미 찾은 몬스터인지 아닌지를 확인한다던지 하는 경우)

 

multimap과 multiset은 잘 활용하지 않음.

 

multimap은 [] 연산자를 지원하지 않는다.

key값으로 erase를 하는 경우 key의 내용이 다 날아간다.

equal_range: key값에 해당하는 iterator 2개를 pair로 반환해준다. (key의 begin, end)

lower_bound: key값의 begin을 반환해준다.

upper_bound: key값의 end를 반환해준다.

 

map과 set은 면접 질문으로 나오는 경우 보통 어떤 구조로 되어있는지 물어보기도 하지만 가끔 map과 multimap의 차이를 물어보는 경우도 있다.

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

[Modern C++] #1  (0) 2022.08.31
[STL] algorithm  (0) 2022.08.31
[STL] map  (0) 2022.08.31
[STL] deque  (0) 2022.08.31
[STL] list  (0) 2022.08.31

vector나 list같은 선형 자료구조의 가장 큰 단점은 데이터를 빠르게 찾을 수 있는 마땅한 방법이 없다.

 

map

균형 이진 트리(AVL)로 만들어졌다.

 

erase는 여러번 호출한다고 딱히 문제가 발생하지는 않는다. 삭제된 키의 개수를 반환해준다.

insert시 이미 key가 존재하는 경우 기존 값이 바뀌지는 않는다. pair<iterator, bool>를 반환해준다.

 

iterator는 pair로 key와 value를 묶어서 레퍼런스로 반환해준다.

 

[] 연산자 사용할 때 주의점은 대입을 하지 않더라도 key-value형태의 데이터가 추가된다.

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

[STL] algorithm  (0) 2022.08.31
[STL] set, multimap, multiset  (0) 2022.08.31
[STL] deque  (0) 2022.08.31
[STL] list  (0) 2022.08.31
[STL] vector  (0) 2022.08.30

double-ended queue

 

vector와는 다르게 데이터가 꽉 차면 새로운 추가 공간을 할당하여 추가 사용한다.

vector와 list의 혼합 형태같은 모양이다.

 

배열과 같은 임의 접근을 지원한다.

vector와 마찬가지로 배열 기반으로 동작하지만 메모리 할당 정책이 다르다.

마치 아파트의 동과 호수로 구분된 듯한 모양이다.

 

데이터의 처음/끝의 삽입 및 삭제는 빠르게 동작한다.

중간 삽입 및 삭제는 vector와 마찬가지로 값을 땡기거나 밀어내기 때문에 성능이 좋지 않다.

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

[STL] set, multimap, multiset  (0) 2022.08.31
[STL] map  (0) 2022.08.31
[STL] list  (0) 2022.08.31
[STL] vector  (0) 2022.08.30
[콜백 함수] 콜백 함수  (0) 2022.08.30

노드 방식으로 작동. 이중 연결 리스트로 구현되어있다.

vector와 다르게 임의 접근을 허용하지 않는다.

삭제 자체는 O(1)이지만 원소에 접근하는 시간은 O(N)이다. 탐색과 삭제는 분리해서 봐야한다.

 

연속된 공간이 아닌 노드 단위로 관리되기 때문에 삽입 및 삭제는 큰 오버헤드 없이 수행할 수 있지만 이전, 다음 노드의 정보들만 가지고 있으므로 임의 접근은 매우 느리기 때문에 공식적으로 지원하는 메소드는 없다.

 

begin은 맨 처음 값이라서 prev가 없어야 정상이겠지만 실제로는 end를 가리키는 더미 노드 주소값이 들어가있다.

[1] <-> [2] <-> .. [5] <-> [_Myhead : end()] <-> [1] <-> ... 이런 구성으로 되어있다.

하지만 연결이 되어있다고 해서 실제로 begin에서 prev로 접근하면 크래시가 발생한다. 잘못된 코드를 잡아주기 위해 연결한 것이지 실제로 접근해도 되는 것은 아니다.

 

iterator의 증감 연산자는 사용이 가능하지만 iterator + 1 같은 형식은 사용이 불가능하다.

 

custom list 간단하게 구현

더보기
#include <iostream>
#include <list>
using namespace std;

// list (연결 리스트)
// - list의 동작 원리
// - 중간 삽입/삭제
// - 처음/끝 삽입/삭제
// - 임의 접근

template<typename T>
class Node
{
public:
	Node() : _next(nullptr), _prev(nullptr), _data(T()) {}
	Node(const T& value) : _next(nullptr), _prev(nullptr), _data(value) {}
public:
	Node*	_next;
	Node*	_prev;
	T		_data;
};

template<typename T>
class Iterator
{
public:
	Iterator() : _node(nullptr)
	{

	}

	Iterator(Node<T>* node) : _node(node)
	{

	}

	Iterator& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	Iterator operator++(int)
	{
		Iterator<T> temp = *this;
		_node = _node->_next;
		return temp;
	}
	
	Iterator& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	Iterator operator--(int)
	{
		Iterator<T> temp = *this;
		_node = _node->_prev;
		return temp;
	}

	T& operator*()
	{
		return _node->_data;
	}

	bool operator==(const Iterator& right)
	{
		return _node == right._node;
	}

	bool operator!=(const Iterator& right)
	{
		return _node != right._node;
	}

public:
	Node<T>* _node;
};

template<typename T>
class List
{
public:
	List() : _size(0)
	{
		_header = new Node<T>();
		_header->_next = _header;
		_header->_prev = _header;
	}

	~List()
	{
		while (_size > 0)
			pop_back();

		delete _header;
	}

	void push_back(const T& value)
	{
		AddNode(_header, value);
	}

	void pop_back()
	{
		RemoveNode(_header->_prev);
	}

	int size() const { return _size; }

private:
	Node<T>* AddNode(Node<T>* before, const T& value)
	{
		Node<T>* node = new Node<T>(value);

		Node<T>* prevNode = before->_prev;
		prevNode->_next = node;
		node->_prev = prevNode;

		node->_next = before;
		before->_prev = node;
		
		_size++;

		return node;
	}

	Node<T>* RemoveNode(Node<T>* node)
	{
		Node<T>* prevNode = node->_prev;
		Node<T>* nextNode = node->_next;
		prevNode->_next = nextNode;
		nextNode->_prev = prevNode;

		delete node;

		_size--;

		return nextNode;
	}

public:
	typedef Iterator<T> iterator;
	iterator begin() { return iterator(_header->_next); }
	iterator end() { return iterator(_header); }
	iterator insert(iterator it, const T& value)
	{
		Node<T>* node = AddNode(it._node, value);
		return iterator(node);
	}
	iterator erase(iterator it)
	{
		Node<T>* node = RemoveNode(it._node);
		return iterator(node);
	}


private:
	Node<T>* _header;
	int _size;
};

int main()
{
	List<int> li;
	List<int>::iterator eraseIt;

	for (int i = 0; i < 10; i++)
	{
		if (i == 5)
			eraseIt = li.insert(li.end(), i);
		else
			li.push_back(i);
	}

	li.pop_back();

	li.erase(eraseIt);

	for (List<int>::iterator it = li.begin(); it != li.end(); ++it)
		cout << *it << endl;

	return 0;
}

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

[STL] map  (0) 2022.08.31
[STL] deque  (0) 2022.08.31
[STL] vector  (0) 2022.08.30
[콜백 함수] 콜백 함수  (0) 2022.08.30
[콜백 함수] 템플릿 기초  (0) 2022.08.30

STL (Standard Template Library)

프로그래밍할 때 필요한 자료구조/알고리즘들을 템플릿으로 제공하는 라이브러리

 

컨테이너 : 데이터를 저장하는 하나의 객체 (자료구조)

 

'어떤 원리로 작동하길래 배열을 유동적으로 사용할 수 있는 것인가?'

 

1) 여유분을 두고 메모리를 할당한다.

-> Q1. 여유분은 얼만큼이 적당한가?

 

2) 여유분까지 꽉 차면 메모리를 증설한다.

-> Q2. 얼마나 증설해야할까?

-> Q3. 증설 시, 기존의 데이터를 어떻게 처리할까?

 

size() : 현재 저장된 데이터의 개수

capacity() : 현재 동적 배열의 최대 크기

reserve() : capacity를 임의의 크기로 지정. (면접 질문: reserve 사용 이유? -> capacity가 증가할 때마다 복사에 의한 오버헤드가 일어나기 때문에 미리 넉넉하게 할당)

resize() : size를 임의의 크기로 지정. capacity도 자동으로 늘어난다. resize로 지정한 경우 push_back이 아닌 배열로 접근해서 데이터를 조작해야 한다. 생성자에서 지정해도 동일하게 작동한다.

 

컴파일러마다 다르지만 size가 capacity를 초과하면 capacity를 1.5배씩 증설시키며 크기를 동적으로 확장시킨다.

capacity는 늘어날지언정 줄어들지는 않는다. clear를 사용해도 줄어들지 않는다.

vector<int>().swap(v);

하지만 이런식으로 임시 vector와 교환 해서 capacity가 0인 것으로 돌려받는 방법은 있다.

보통은 이렇게까지 할 이유는 없어서 잘 이용되지 않는다.

 

반복자 (Iterator)

내부적으로는 포인터를 들고 있다.

vector뿐 아니라 다른 컨테이너에도 공통적으로 있는 개념이다.

const_iterator, reverse_iterator 등이 있다.

 

원소의 중간 삽입 및 삭제 : vector는 반드시 메모리 블록에 연속되어 있어야 하기 때문에 중간 삽입/삭제는 매우 비효율 적으로 동작한다. (오버헤드가 크게 발생)

insert와 erase로 삽입 및 삭제를 할 수 있다.

insert와 erase는 삽입/삭제한 원소의 위치 반복자를 반환해준다.

erase의 경우 end 범위는 삭제에 포함되지 않는다.

 

erase로 삭제된 iterator는 더 이상 해당 컨테이너에 속하지 않는다. (모든 정보가 NULL로 기록된다)

 

원소의 처음/끝 삽입 및 삭제 : 처음 원소의 삽입 및 삭제는 위와 같은 이유이지만 끝 원소의 삽입 및 삭제는 괜찮다.

 

원소의 임의 접근 : 가능

 

custom vector 간단하게 구현

더보기
#include <iostream>
#include <vector>
using namespace std;

template<typename T>
class Iterator
{
public:
	Iterator() : _ptr(nullptr) {}
	Iterator(T* ptr) : _ptr(ptr) {}

	Iterator& operator++()
	{
		_ptr++;
		return *this;
	}

	Iterator operator++(int)
	{
		Iterator temp = *this;
		_ptr++;
		return temp;
	}

	Iterator& operator--()
	{
		_ptr--;
		return *this;
	}

	Iterator operator--(int)
	{
		Iterator temp = *this;
		_ptr--;
		return temp;
	}

	Iterator operator+(const int count)
	{
		Iterator temp = *this;
		temp._ptr += count;
		return temp;
	}

	Iterator operator-(const int count)
	{
		Iterator temp = *this;
		temp._ptr -= count;
		return temp;
	}

	bool operator==(const Iterator& right) { return _ptr == right._ptr;  }
	bool operator!=(const Iterator& right) { return _ptr != right._ptr;  }

	T& operator*() { return *_ptr; }

public:
	T* _ptr;
};

template<typename T>
class Vector
{
public:
	Vector() : _data(nullptr), _size(0), _capacity(0) {}
	~Vector()
	{
		if (_data != nullptr)
			delete[] _data;
	}

	void push_back(const T& val)
	{
		if (_size == _capacity)
		{
			int newCapacity = static_cast<int>(_capacity * 1.5f);
			if (newCapacity == _capacity) newCapacity++;

			reserve(newCapacity);
		}

		_data[_size] = val;
		_size++;
	}

	void reserve(const int capacity)
	{
		_capacity = capacity;
		T* newData = new T[_capacity];

		for (int i = 0; i < _size; i++)
			newData[i] = _data[i];

		if (_data != nullptr)
			delete[] _data;

		_data = newData;
	}

	T& operator[](const int pos)
	{
		return _data[pos];
	}

	int size() const { return _size; }
	int capacity() const { return _capacity; }

public:
	typedef Iterator<T> iterator;

	iterator begin() { return iterator(&_data[0]); }
	iterator end() { return begin() + _size; }

private:
	T* _data;
	int _size;
	int _capacity;
};

// vector (동적 배열)
// - vecotr의 동작 원리
// - 중간 삽입/삭제
// - 처음/끝 삽입/삭제
// - 임의 접근

int main()
{
	Vector<int> v;

	v.reserve(100);

	for (int i = 0; i < 100; i++)
	{
		v.push_back(i);
		cout << v.size() << " " << v.capacity() << endl;
	}

	for (int i = 0; i < v.size(); i++)
	{
		cout << v[i] << endl;
	}
	
	cout << "------------------" << endl;

	for (Vector<int>::iterator it = v.begin(); it != v.end(); ++it)
	{
		cout << *it << endl;
	}

	return 0;
}

 

clear를 구현하는 경우 데이터를 지울 필요 없이 그냥 size만 0으로 재설정 하면 된다.

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

[STL] deque  (0) 2022.08.31
[STL] list  (0) 2022.08.31
[콜백 함수] 콜백 함수  (0) 2022.08.30
[콜백 함수] 템플릿 기초  (0) 2022.08.30
[콜백 함수] 함수 객체  (0) 2022.08.30

+ Recent posts