이번에는 우선순위 큐에 대해서 알아보자.

최근 코딩 테스트도 그렇고 가끔 잊을만하면 나오는 개념이다.

기본적으로 학부생 때 알고리즘 시간에 Heap 구현을 해본 적이 있어서 생각보다 쉽게 받아들여진 것 같다.

우선순위 큐 priority_queue

이름 그대로 우선순위로 정렬된 큐를 의미한다.

어떤 원소를 삽입하거나 삭제할 때 그 내부에서는 우선순위에 따라서 자동으로 정렬되어 보존된다.

내부적으로 Heap을 사용해서 정렬하기 때문에 삽입과 삭제에 대한 연산은 O(logN)에 이루어진다.

 

운영체제 시간에 선점하는 프로세스 스케줄링의 경우, 우선순위가 기존 ready큐에서 있던 다른 프로세스의 우선순위보다 높은 프로세스가 들어오면 그것부터 cpu를 할당하는 우선순위 스케줄링 알고리즘에서도 나온다.

 

사용법

#include <queue>

priority_queue<T(자료형, 구조체), container(컨테이너), cmp(비교함수)> pq;

priority_queue<int> pq; //내림차순(기본 less)
priority_queue<int, vector<int>, greater<int>> pq; //오름차순

struct pos {
    int x;
    int y;
    int d;
    
    //연산자 오버로딩(비교함수 내 입맛에 맛게 변경가능)
    bool operator()(pos a, pos b) {
    	if(a.x == b.x) {
        	return a.y > b.y; //x가 같다면, y가 더 작은 것이 먼저
        }
        return a.x < b.x; //x가 더 큰게 먼저
    }
};

priority_queue<pos, vector<pos>, cmp> pq; //사용자 정의

pq.push(1); //삽입, 내부적으로 우선순위에 따라 정렬된다.
pq.push(2);
pq.push(3);

pq.pop(); //가장 위(앞쪽)에 있는 원소를 삭제한다.
pq.top(); //가장 위(앞쪽)에 있는 원소를 살펴본다.

pq.size(); //크기 반환
pq.empty(); //비어있으면 true, 아니면 false

사실 우선순위 큐의 핵심은 다른 것들도 많지만 내부적으로 정렬하는 기준을 직접 구현하는 부분이 대부분이지 않을까 생각한다.

주의할 점은 대부분의 정렬은 < 이렇게 하면 오름차순으로 정렬되지만, 우선순위 큐는 반대라는 점이다. 헷갈리지 말자.

'CS > Data Structure' 카테고리의 다른 글

[C++] stoi  (0) 2021.05.06
[C++] multimap  (0) 2021.04.28
[C++] map  (0) 2021.04.28
[C++] unordered_set  (0) 2021.04.12
[C++] multiset  (0) 2021.04.12

stoi

c++의 매력을 느낄 수 있는 함수입니다.

바로 문자열을 정수로 바꿔주는 엄청난..

#include <string>

int a = stoi("1234"); // 1234
int b = stoi("-1234"); // -1234
int c = stoi("+1234"); // 1234

신기한 점은 문자열 앞에 '+''-' 부호가 있어도 그것을 반영해서 나타내 준다는 점!!

'CS > Data Structure' 카테고리의 다른 글

[C++] priority_queue  (0) 2021.08.04
[C++] multimap  (0) 2021.04.28
[C++] map  (0) 2021.04.28
[C++] unordered_set  (0) 2021.04.12
[C++] multiset  (0) 2021.04.12

multimap?

multimap은 기본적으로 map과 비슷하지만 다른 부분들이 있다.

비슷한 부분은 [map]에서 확인하자.

1. Key의 중복을 허용한다.

2 [] 연산자를 지원하지 않기 때문에 insert() 멤버 함수로 삽입해야 한다.

 

연습해보기

#include <iostream>
#include <map>
using namespace std;

int main()
{
	// less <
	// left child key < parent key <= right child key
	multimap<int, int> mm;

	//multimap은 [] operator를 제공하지 않는다.
	//mm[5] = 100;
	mm.insert(pair<int, int>(5, 100));
	mm.insert(pair<int, int>(3, 100));
	mm.insert(pair<int, int>(8, 30));
	mm.insert(pair<int, int>(3, 40));
	mm.insert(pair<int, int>(1, 70));
	mm.insert(pair<int, int>(7, 100));
	mm.insert(pair<int, int>(8, 50));

	for (auto iter = mm.begin(); iter != mm.end(); iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';

	cout << "mm.count(3): " << mm.count(3) << '\n';
	cout << "mm.count(9): " << mm.count(9) << '\n';
	cout << "mm.count(5): " << mm.count(5) << '\n';

	auto iter = mm.find(3);
	if (iter != mm.end())
		cout << "첫 번째 key 3에 매핑된 value: " << iter->second << '\n';
	else
		cout << "key 3이 multimap에 없습니다.\n";
	return 0;
}
(1, 70) (3, 100) (3, 40) (5, 100) (7, 100) (8, 30) (8, 50)
mm.count(3) : 2
mm.count(9) : 0
mm.count(5) : 1
첫 번째 key 3에 매핑된 value : 100
#include <iostream>
#include <map>
using namespace std;

int main()
{
	multimap<int, int> mm;

	mm.insert(pair<int, int>(5, 100));
	mm.insert(pair<int, int>(3, 100));
	mm.insert(pair<int, int>(8, 30));
	mm.insert(pair<int, int>(3, 40));
	mm.insert(pair<int, int>(1, 70));
	mm.insert(pair<int, int>(7, 100));
	mm.insert(pair<int, int>(8, 50));

	for (auto iter = mm.begin(); iter != mm.end(); iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';

	multimap<int, int>::iterator lower_iter = mm.lower_bound(3);
	multimap<int, int>::iterator upper_iter = mm.upper_bound(3);

	cout << "[lower_iter, upper_iter): ";
	for (auto iter = lower_iter; iter != upper_iter; iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';

	auto equal_iter = mm.equal_range(3);
	cout << "[equal_iter.first, equal_iter.second): ";
	for (auto iter = equal_iter.first; iter != equal_iter.second; iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';
	return 0;
}
(1, 70) (3, 100) (3, 40) (5, 100) (7, 100) (8, 30) (8, 50)
[lower_iter, upper_iter): (3, 100) (3, 40)
[equal_iter.first, equal_iter.second): (3, 100) (3, 40)

'CS > Data Structure' 카테고리의 다른 글

[C++] priority_queue  (0) 2021.08.04
[C++] stoi  (0) 2021.05.06
[C++] map  (0) 2021.04.28
[C++] unordered_set  (0) 2021.04.12
[C++] multiset  (0) 2021.04.12

map?

1. 연관 컨테이너이며 노드 기반 컨테이너

2. 특정 정렬 기준으로 원소(Key)자동으로 정렬된다.

3. 균형 이진트리의 장점을 가지고 있어 찾기, 삭제, 삽입성능 O(lgN)을 보장한다.

 

자주 사용하는 멤버 함수

기본적으로 연관 컨테이너 set과 똑같지만 차이점을 주자면

[] 연산자를 제공한다.

[] 연산자를 이용해서 원소(key, value)를 삽입하거나, key에 매핑된 value를 갱신할 수 있다.

아래에는 자주 사용하는 부분들을 정리했다.

insert(pair<int,int>(key,value)) map에 pair 객체로 key와 value를 저장한다.
map[key] = value map에 key에 해당하는 원소가 없다면 (key, value) 저장하고
key에 해당하는 원소가 있다면 value를 갱신한다.
erase() 삭제
find() 찾기
lower_bound() key를 처음 찾는 곳의 반복자를 리턴
upper_bound() key 다음 원소의 반복자를 리턴
equal_range() [lower_bound(), upper_bound()) 를 pair 쌍으로 반복자를 리턴

연습해보기

삽입은 insert() 혹은 [] 연산자를 이용해서 삽입한다.

#include <iostream>
#include <map>
using namespace std;

int main()
{
	map<int, int> m; // (key, value) less <

	// 임시 pair 객체 생성 후 저장
	m.insert(pair<int, int>(5, 100));
	m.insert(pair<int, int>(3, 100));
	m.insert(pair<int, int>(8, 30));
	m.insert(pair<int, int>(4, 40));
	m.insert(pair<int, int>(1, 70));
	m.insert(pair<int, int>(7, 100));

	pair<int, int> pr(9, 50);
	m.insert(pr);

	for (auto iter = m.begin(); iter != m.end(); iter++)
		cout << "(" << (*iter).first << ',' << (*iter).second << ") ";
	cout << '\n';

	// iterator는 -> 연산자 오버로딩되어 있으므로
	// 포인터처럼 멤버를 -> 연산자로 접근이 가능
	for (auto iter = m.begin(); iter != m.end(); iter++)
		cout << "(" << iter->first << ',' << iter->second << ") ";
	cout << '\n';

	pair<map<int, int>::iterator, bool> ret;
	ret = m.insert(pair<int, int>(9, 50));
	if (true == ret.second)
		cout << "key: " << ret.first->first << ", value: " << ret.first->second << " success\n";
	else
		cout << "key: " << ret.first->first << ", value: " << ret.first->second << " failure\n";

	return 0;
}
(1, 70) (3, 100) (4, 40) (5, 100) (7, 100) (8, 30) (9, 50)
(1, 70) (3, 100) (4, 40) (5, 100) (7, 100) (8, 30) (9, 50)
key: 9, value : 50 failure
#include <iostream>
#include <map>
using namespace std;

int main()
{
	map<int, int> m;

	m[5] = 100; // key 5, value 100 insert
	m[3] = 100;
	m[8] = 30;
	m[4] = 40;
	m[1] = 70;
	m[7] = 100;
	m[9] = 50;

	for (auto iter = m.begin(); iter != m.end(); iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';

	m[5] = 200; // update;

	for (auto iter = m.begin(); iter != m.end(); iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';
	return 0;
}
(1, 70) (3, 100) (4, 40) (5, 100) (7, 100) (8, 30) (9, 50)
(1, 70) (3, 100) (4, 40) (5, 200) (7, 100) (8, 30) (9, 50)

검색은 find() 연산을 이용한다.

key에 해당하는 원소가 존재한다면 그 위치의 반복자를 리턴하고 아니라면 end() 반복자를 리턴한다.

lower_bound()는 key를 처음으로 찾은 지점의 반복자를 리턴

upper_bound()는 key를 찾은 다음 원소의 반복자를 리턴

equal_range()[lower_bound(), upper_bound()) 를 반복자를 쌍으로 리턴한다.

#include <iostream>
#include <map>
using namespace std;

int main()
{
	map<int, int> m;

	m[5] = 100;
	m[3] = 100;
	m[8] = 30;
	m[4] = 40;
	m[1] = 70;
	m[7] = 100;
	m[9] = 50;

	for (auto iter = m.begin(); iter != m.end(); iter++)
		cout << "(" << iter->first << ',' << iter->second << ") ";
	cout << '\n';

	// key를 기준으로 찾는다.
	auto iter = m.find(5);
	if (iter != m.end())
		cout << "key 5에 매핑된 value: " << iter->second << '\n';
	else
		cout << "key 5가 없습니다.\n";

	map<int, int>::iterator lower_iter = m.lower_bound(5);
	map<int, int>::iterator upper_iter = m.upper_bound(5);

	cout << "[lower_iter, upper_iter): ";
	for (auto iter = lower_iter; iter != upper_iter; iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';

	auto iter2 = m.equal_range(5);
	cout << "[iter2.first, iter.second): ";
	for (auto iter = iter2.first; iter != iter2.second; iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';
	return 0;
}
(1, 70) (3, 100) (4, 40) (5, 100) (7, 100) (8, 30) (9, 50)
key 5에 매핑된 value : 100
[lower_iter, upper_iter): (5, 100)
[iter2.first, iter.second): (5, 100)

정렬 기준을 변경해보자.

map도 기본적인 정렬 기준은 less이지만

key를 내림차순으로 정렬하고 싶다면 greater로 변경해서 사용하면 된다.

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main()
{
	// greater >
	map<int, string, greater<int>> m;

	// [] operator
	m[2] = "two";
	m[5] = "five";
	m[3] = "three";
	m[8] = "eight";
	m[4] = "four";
	m[6] = "six";
	m[1] = "one";
	m[7] = "seven";
	m[9] = "nine";

	for (auto iter = m.begin(); iter != m.end(); iter++)
		cout << "(" << iter->first << "," << iter->second << ") ";
	cout << '\n';

	for (int i = 0; i < m.size(); i++)
		cout << m[i] << ' ';
	cout << '\n';
	return 0;
}
(9, nine) (8, eight) (7, seven) (6, six) (5, five) (4, four) (3, three) (2, two) (1, one)
one two three four five six seven eight nine

 

'CS > Data Structure' 카테고리의 다른 글

[C++] stoi  (0) 2021.05.06
[C++] multimap  (0) 2021.04.28
[C++] unordered_set  (0) 2021.04.12
[C++] multiset  (0) 2021.04.12
스택의 응용 - 후위 표기식  (0) 2021.04.09

특징

1. 자동적으로 정렬되지 않는다.

2. hash 사용으로 bucket 으로 인해 메모리 사용량 증가

3. insertion, deletion, find O(1)에 수행, 다만 충돌이 너무 많이 일어나면 O(N)

(그러면 기존 set 보다 더 느리게 수행될 수 있다는 말이다.)

4. hash 자료구조를 사용한다.

 

원소의 개수가 많을수록 해시 충돌이 일어날 확률도 많아지게 되므로

경우에 따라 사용하자.

 

원소의 개수가 적고 빠른 성능 : unordered_set

원소의 개수가 많고 안정성 : set

Hash function

정의는 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수.

unordered_set 에 특정 value 를 찾거나 넣을 때 해당 value 를 hash function 에 넣고 나온 리턴 값인

hash value 를 얻는다.

 

기본적으로  hash value 는 bucket 에 들어가는 index 보다 크기 때문에 bucket 의 size 로 나머지 연산을 하여서 최종 index 를 얻고 그 bucket[index] 에 해당 value 를 저장하게 된다.

 

1. value -> hash function -> hash value(해시 값)

2. hash value % bucket count = index

3. bucket[index] 에 linked list 로 value 를 넣어준다.

 

이때 서로 다른 value 라도 우연히 hash value 가 같을 수 있는데

이를 hash collision(해시 충돌)이라고 한다.

 

O(1) ??

insertion, deletion, find 가 어떻게 O(1)이 나올까?

문자열 "abc" 를 unordered_set 에 삽입한다고 하면.

1. "abc" 는 hash function 으로 리턴 값인 hash value : 23103023124 를 얻는다. O(1)

2. hash value % bucket count = 4 (index) O(1)

3. bucket[4] -> "abc" (Linked List에 삽입은 O(1))

따라서 insertion 은 O(1) 에 가능하다.

 

문자열 "abc" 를 unordered_set 에서 찾거나 삭제한다면?

1. "abc" 는 hash function 으로 리턴 값인 hash value : 23103023124 를 얻는다. O(1)

2. hash value % bucket count = 4 (index) O(1)

3. bucket[4] 에서 "abc" 찾는다.  O(1)

따라서 find, deletion 도 O(1) 에 가능하다.

 

그런데, element 가 너무 많아지게 되면 hash collision 이 빈번하게 발생하여

서로 다른 값임에도 불구하고 같은 bucket 에 연결된다면 O(N) 이 아닐까?

element 가 많아지게 되면 hash 자체 내부에서 rehashing 이 일어나 bucket count 를 늘려 O(1) 에 수행할 수 있게 변경된다. 다만 rehashing 의 경우 O(N) 이 소요되기 때문에 이를 막기 위해 선언할 때

reserve 를 활용하여 rehashing 의 빈도수를 줄일 수 있다.

#include <iostream>
#include <string>
#include <unordered_set>
using namespace std;

int main()
{
	unordered_set<string> s;

	s.insert("abc");
	s.insert("def");
	s.insert("ghi");
	s.insert("jkl");

	// string(input) -> hash function -> hash value
	cout << "abc: " << hash<string>{}("abc") << endl;
	cout << "def: " << hash<string>{}("def") << endl;
	cout << "ghi: " << hash<string>{}("ghi") << endl;
	cout << "jkl: " << hash<string>{}("jkl") << endl << endl;

	// hash value % bucket_count -> index(hash table mapping)
	cout << "bucket count : " << s.bucket_count() << endl;
	cout << "abc bucket : " << s.bucket("abc") << endl;
	cout << "def bucket : " << s.bucket("def") << endl;
	cout << "ghi bucket : " << s.bucket("ghi") << endl;
	cout << "jkl bucket : " << s.bucket("jkl") << endl << endl;

	// 더 많은 input이 들어오면, rehashing이 발생한다.
	// rehashing 은 O(N)이 소모되므로,
	// 이를 방지하고자 할 때 미리 할당해주는 reserve 를 사용한다.
	s.insert("dasq");
	s.insert("dasdawq");
	s.insert("rdewq");
	s.insert("vkbas");
	s.insert("treq");
	s.insert("gfoas");
	s.insert("bdea");
	s.insert("dela");
	s.insert("jgela");
	cout << "bucket count : " << s.bucket_count() << endl;
	return 0;
}

 

abc: 440920331
def: 3310976652
ghi: 992499321
jkl: 3286144074

bucket count : 8
abc bucket : 3
def bucket : 4
ghi bucket : 1
jkl bucket : 2

bucket count : 64

참고:)

youtu.be/MsDz50o3jHY

'CS > Data Structure' 카테고리의 다른 글

[C++] multimap  (0) 2021.04.28
[C++] map  (0) 2021.04.28
[C++] multiset  (0) 2021.04.12
스택의 응용 - 후위 표기식  (0) 2021.04.09
[C++] set  (0) 2021.04.04

특징

set의 경우 key 값이 중복되지 않지만,

multiset의 경우 key 값이 중복될 수 있다.

대부분의 특징들은 set과 동일하므로 바로 예제를 통해서 하나씩 배워보자.

삽입

set과 마찬가지로 insert(key); 를 이용해서 삽입할 수 있다.

#include <iostream>
#include <set>
using namespace std;

int main()
{
	multiset<int> ms;
	// less <
	// left node < parent node <= right node
	ms.insert(50);
	ms.insert(30);
	ms.insert(80);
	ms.insert(80);
	ms.insert(30);
	ms.insert(70);
	auto iter = ms.insert(10); // 저장된 위치만 가리키는 반복자 리턴

	cout << "*iter : " << *iter << '\n';

	for (iter = ms.begin(); iter != ms.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	// greater >
	// left node >= parent node > right node
	multiset<int, greater<int>> ms2;
	for (iter = ms.begin(); iter != ms.end(); iter++)
		ms2.insert(*iter);

	for (iter = ms2.begin(); iter != ms2.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	return 0;
}
*iter : 10
10 30 30 50 70 80 80
80 80 70 50 30 30 10

다양한 검색 연산들

count(key) : key가 몇 개 있는지 리턴

find(key) : key의 반복자를 리턴, 없다면 end() 리턴

lower_bound(key) : 찾는 key의 시작 반복자 리턴

upper_bound(key) : 찾는 key의 다음 반복자 리턴

#include <iostream>
#include <set>
using namespace std;

int main()
{
	multiset<int> ms;
	ms.insert(50);
	ms.insert(30);
	ms.insert(80);
	ms.insert(80);
	ms.insert(30);
	ms.insert(70);
	ms.insert(10);

	for (auto iter = ms.begin(); iter != ms.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	cout << "ms.count(30) : " << ms.count(30) << '\n';
	cout << "ms.count(100) : " << ms.count(100) << '\n';

	auto iter = ms.find(30);
	if (iter != ms.end())
		cout << "30 exist\n";
	else
		cout << "30 not exist\n";

	iter = ms.find(100);
	if (iter != ms.end())
		cout << "100 exist\n";
	else
		cout << "100 not exist\n";

	multiset<int>::iterator lower_iter;
	multiset<int>::iterator upper_iter;

	lower_iter = ms.lower_bound(30);
	upper_iter = ms.upper_bound(30);
	cout << "lower_iter : " << *lower_iter << '\n';
	cout << "upper_iter : " << *upper_iter << '\n';

	cout << "[lower_iter, upper_iter) -> ";
	for (iter = lower_iter; iter != upper_iter; iter++)
		cout << *iter << ' ';
	cout << '\n';
	return 0;
}
10 30 30 50 70 80 80
ms.count(30) : 2
ms.count(100) : 0
30 exist
100 not exist
lower_iter : 30
upper_iter : 50
[lower_iter, upper_iter) -> 30 30

equal_range()

lower_bound와 upper_bound를 따로 찾을 필요 없이 한 쌍으로 리턴해준다.

#include <iostream>
#include <set>
using namespace std;

int main()
{
	multiset<int> ms;
	ms.insert(50);
	ms.insert(30);
	ms.insert(80);
	ms.insert(80);
	ms.insert(30);
	ms.insert(70);
	ms.insert(10);

	for (auto iter = ms.begin(); iter != ms.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	auto iter = ms.equal_range(30);
	for (auto p = iter.first; p != iter.second; p++)
		cout << *p << ' ';
	cout << '\n';
	return 0;
}
10 30 30 50 70 80 80
30 30

다양하게 연습해보기

#include <iostream>
#include <set>
using namespace std;

int main()
{
	multiset<int, greater<int> > mset;
	multiset<int, greater<int> > ::iterator itr;

	mset.insert(40);
	mset.insert(40);
	mset.insert(40);
	mset.insert(30);
	mset.insert(60);
	mset.insert(20);
	mset.insert(50);
	mset.insert(50);
	mset.insert(10);

	cout << "mset : ";
	for (itr = mset.begin(); itr != mset.end(); ++itr)
		cout << *itr << ' ';
	cout << endl;

	cout << "Size : " << mset.size() << endl;

	cout << "[mset.begin(), mset.find(40)) erase\n";
	mset.erase(mset.begin(), mset.find(40));

	cout << "mset : ";
	for (itr = mset.begin(); itr != mset.end(); ++itr)
		cout << *itr << ' ';
	cout << endl;

	cout << "40 counts : " << mset.count(40) << endl;

	cout << "Max-Size : " << mset.max_size() << endl;

	cout << "erase 40\n";
	mset.erase(40);
	cout << "mset : ";
	for (itr = mset.begin(); itr != mset.end(); ++itr)
		cout << *itr << ' ';
	cout << endl;

	mset.clear();

	mset = { 1,2,3,4,5,5,5,3,3,9,10 };
	cout << "mset : ";
	for (itr = mset.begin(); itr != mset.end(); ++itr)
		cout << *itr << ' ';
	cout << endl;

	cout << "lower_bound(5) : " << *mset.lower_bound(5) << endl;
	cout << "upper_bound(5) : " << *mset.upper_bound(5) << endl;
	for (auto i = mset.lower_bound(5); i != mset.upper_bound(5); ++i)
		cout << *i << ' ';
	cout << endl;

	auto p = mset.equal_range(5);
	for (auto i = p.first; i != p.second; ++i)
		cout << *i << ' ';
	cout << endl;
	return 0;
}

 

mset : 60 50 50 40 40 40 30 20 10
Size : 9
[mset.begin(), mset.find(40)) erase
mset : 40 40 40 30 20 10
40 counts : 3
Max-Size : 214748364
erase 40
mset : 30 20 10
mset : 10 9 5 5 5 4 3 3 3 2 1
lower_bound(5) : 5
upper_bound(5) : 4
5 5 5
5 5 5

'CS > Data Structure' 카테고리의 다른 글

[C++] multimap  (0) 2021.04.28
[C++] map  (0) 2021.04.28
[C++] unordered_set  (0) 2021.04.12
스택의 응용 - 후위 표기식  (0) 2021.04.09
[C++] set  (0) 2021.04.04

스택의 응용 중 하나인 후위 표기식에 대해서 배워보자.

후위 표기식

 

우리가 자주 사용하는 표기식은 중위 표기식(infix expression)을 따른다.

5 + 3

 

후위 표기식(postfix expression)연산자가 피연산자 뒤에 나오는 것을 말한다.

5  3 +

 

후위 표기식 계산법

 

7 4 -3 * 1 5 + / * 

스택 자료구조를 이용해서 계산할 수 있다.

핵심은 두 수를 뽑을 때의 순서가 중요하다.

 

방법은 다음과 같다.

1) 피연산자(즉, 숫자)가 들어오면 무조건 출력

2) 연산자가 들어오면 스택의 최상단에서 두 값을 뽑아 연산자에 맞게 계산하고 다시 스택에 넣어준다.

 

단계별로 확인해보자.

input : 7 4 -3 * 1 5 + / * 

Stack
7
7 4
7 4 -3
7 -12
( * 연산자가 들어오면 스택의 최상단에서 두 값을 뽑아 계산한다.)
이 때의 순서는 4 * (-3)이 맞는 표현이며, (-3) * 4는 맞지 않는 표현이다.
7 -12 1
7 -12 1 5
7 -12 6
(+ 연산자, 1 + 5 = 6을 스택에 넣어준다.)
7 -2
( / 연산자, -12 / 6 = -2를 스택에 넣어준다.)
-14
( * 연산자, 7 * (-2) = -14를 스택에 넣어준다.)

 

중위 표기식을 후위 표기식으로 변환 (괄호가 없는 ver.)

 

조금 더 깊게 들어가 보자.

 

우리가 잘 알고 있는 중위 표기식후위 표기식으로 어떻게 변환할까?

=> 스택 자료구조를 사용하여 해결할 수 있다.

 

풀어쓰면 다음과 같다.

1) 중위 표기식을 처음부터 순서대로 읽으면서 피연산자는 즉시 출력한다.

2) 모든 연산자는 일단 스택에 push 한다.

단, 이때 스택 내에 우선순위가 자신보다 더 높은 연산자가 있다면 pop 하여 출력하고

이후 push 한다. 여기서 더 높은 연산자는 정말로 높은 우선순위 연산자일 수도 있고,

혹은 우선순위가 같은 연산자도 먼저 들어온 연산자가 더 높은 우선순위를 가지게 된다.

 

핵심 방법은 다음과 같다.

1) 피연산자는 출력한다.

2) 연산자라면 아래와 같이 고려한다.

2-1) stack.top() < 들어오는 연산자이면, 들어오는 연산자를 스택에 push 한다.

2-2) stack.top() >= 들어오는 연산자이면, 이 조건에 맞는 연산자들은 다 pop 하여

결과에 추가하고, 들어오는 연산자를 스택에 push 한다.

 

단계별로 확인해보자.

input : 2 - 10 / 5 * 6 + 4

Stack Result
  2
- 2
  2 10
- / 2 10
- / 2 10 5
- * 2 10 5 /
- * 2 10 5 / 6
+ 2 10 5 / 6 * -
+ 2 10 5 / 6 * - 4
  2 10 5 / 6 * - 4 +
중위 표기식을 후위 표기식으로 변환 (괄호가 있는 ver.)

 

그렇다면 괄호가 있으면 어떻게 처리할까?

 

핵심 방법은 다음과 같다.

1) 여는 괄호가 있으면 무조건 스택에 push 한다.

2) 닫는 괄호가 나오면 스택에서 여는 괄호가 나올 때까지 pop 하여 출력하고,

마지막 여는 괄호도 pop 해준다, 이때 닫는 괄호는 스택에 push 하지 않는다.

 

단계별로 확인해보자.

input : 3 + ( ( 4 * 7 ) / 2 )

Stack Result
  3
+ 3
+ 3
+ ( ( 3
+ ( (  3 4
+ ( ( * 3 4
+ ( ( * 3 4 7
+ ( 3 4 7 *
+ ( / 3 4 7 *
+ ( 3 4 7 * 2
+ 3 4 7 * 2 /
  3 4 7 * 2 / +
참고:)

 

YouTube

'CS > Data Structure' 카테고리의 다른 글

[C++] multimap  (0) 2021.04.28
[C++] map  (0) 2021.04.28
[C++] unordered_set  (0) 2021.04.12
[C++] multiset  (0) 2021.04.12
[C++] set  (0) 2021.04.04

특징

집합은 요소의 값이 요소를 식별하기 때문에 각 요소가 고유(유니크) 해야 하는 연관 컨테이너 유형이다.

 

1. 중복 값이 올 수 없다. (유니크하다, -> 중복 제거 기능)

2. 삽입하는 순서에 상관없이 무조건 정렬된다.

3. Balanced Tree(균형 이진 트리) 중 R-B Tree를 통해 구현되어 있다.

4. 연관 컨테이너이며 key를 변경할 수 없다.

5. 삽입, 삭제, 검색 O(lgN)의 성능을 가지고 있다.

추가적으로 count(), lower_bound(), upper_bound(), equal_range()O(lgN)에 수행된다.

자주 사용하는 멤버 함수

empty() 비어있다면 true, 아니면 false
size() 저장된 원소의 개수 리턴
max_size() set이 가질 수 있는 최대 크기를 반환 (214748364)
   
insert() 삽입
erase() 삭제
swap() 스왑
clear() 저장된 요소들을 모두 삭제
emplace() move()를 사용해 객체를 저장
emplace_hint() 삽입될 위치에 대한 힌트를 제공해서 더 빠르게 삽입한다.
   
find() 찾는 값이 있다면 해당 위치의 iterator 를 반환하고,
없다면 end()를 반환한다.
count() size()와 동일하게 저장된 요소의 개수를 리턴
lower_bound() value와 같거나 큰 것의 iterator 반환
찾는 원소의 시작 반복자 리턴
upper_bound() value를 초과하는 iterator 반환
찾는 원소의 다음 원소 반복자 리턴
equal_range() [lower_bound, upper_bound) pair 리턴

너무 많으니까 하나씩 예제를 따라 하면서 배워보자.

삽입

기본적으로 삽입은 insert()를 이용한다.

insert(숫자);

insert(반복자, 숫자);

#include <iostream>
#include <set>
using namespace std;

int main()
{
	set<int> s; // less <
	// left node < parent node < right node

	s.insert(50);
	s.insert(30);
	s.insert(80);
	s.insert(40);
	s.insert(10);
	s.insert(70);
	s.insert(90);

	set<int>::iterator iter; // set의 양방향 반복자 (왼쪽도 갈 수 있고, 오른쪽도 갈 수 있고)
	for (iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	s.insert(50); // 중복된 원소를 삽입하지만 실패.

	auto p = s.insert(50); // pair<iter, boo> 타입
	if (p.second)
		cout << "50 삽입 성공!\n";
	else
		cout << "50 삽입 실패!\n";
	
	for (iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';
	return 0;
}
10 30 40 50 70 80 90
50 삽입 실패!
10 30 40 50 70 80 90
#include <iostream>
#include <set>
using namespace std;

int main()
{
	set<int> s;
	pair<set<int>::iterator, bool> pr;

	s.insert(50);
	s.insert(30);
	s.insert(80);
	s.insert(40);
	s.insert(10);
	s.insert(70);
	pr = s.insert(90);

	set<int>::iterator iter;
	for (iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	s.insert(pr.first, 85); // 해당 위치에 빠르게 원소를 넣을 수 있다.
	for (iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';
	return 0;
}
10 30 40 50 70 80 90
10 30 40 50 70 80 85 90

정렬 기준

기본적인 정렬 기준은 less이며 < 연산으로 한다.

less 기준이면 left child node < parent node < right child node

greater 기준이면 left child node > parent node > right child node

#include <iostream>
#include <set>
using namespace std;

int main()
{
	set<int, greater<int>> s; // greater >
	// left node > parent node > right node

	s.insert(50);
	s.insert(30);
	s.insert(80);
	s.insert(40);
	s.insert(10);
	s.insert(70);
	s.insert(90);
	
	for (auto iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';
	return 0;
}
90 80 70 50 40 30 10

검색(찾기)

set에서 자주 사용하는 find() 연산이다.

find(key) : key를 검색해서 key를 가리키는 반복자를 리턴한다.

만약에 찾는 key가 없으면 end() 반복자를 리턴한다.

#include <iostream>
#include <set>
using namespace std;

int main()
{
	set<int> s;

	s.insert(50);
	s.insert(30);
	s.insert(80);
	s.insert(40);
	s.insert(10);
	s.insert(70);
	s.insert(90);

	for (auto iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	int target = 30;
	auto iter = s.find(target);
	if (iter != s.end())
		cout << target << " exist\n";
	else
		cout << target << " not exist\n";

	target = 100;
	iter = s.find(target);
	if (iter != s.end())
		cout << target << " exist\n";
	else
		cout << target << " not exist\n";
	return 0;
}
10 30 40 50 70 80 90
30 exist
100 not exist

Count()

검색과 비슷한 성격을 가지고 있다.

count(key) : key 가 연관 컨테이너에 몇 개가 있는지를 리턴한다.

set에서는 key들이 중복을 허용하지 않기 때문에 0 or 1이지만

multiset에서는 key들이 중복을 허용하기 때문에 0 or N 이 된다.

#include <iostream>
#include <set>
using namespace std;

int main()
{
	set<int> s;

	s.insert(50);
	s.insert(30);
	s.insert(80);
	s.insert(40);
	s.insert(10);
	s.insert(70);
	s.insert(90);

	for (auto iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	cout << "s.count(50) : " << s.count(50) << '\n';
	cout << "s.count(100) : " << s.count(100) << '\n';
	return 0;
}
10 30 40 50 70 80 90
s.count(50) : 1
s.count(100) : 0

Upper_bound() Lower_bound()

특정 구간을 한 번에 리턴 하고 싶을 때?

혹은 해당 key가 존재하는지 검사할 때도 사용한다.

중요한 점은 upper_bound() == lower_bound 이면 내가 찾는 key는 존재하지 않는 것이다.

[lower_bound(), upper_bound()) 닫힌 구간, 열린 구간을 표시하기도 한다.

#include <iostream>
#include <set>
using namespace std;

int main()
{
	set<int> s;

	s.insert(50);
	s.insert(30);
	s.insert(80);
	s.insert(40);
	s.insert(10);
	s.insert(70);
	s.insert(90);

	for (auto iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	set<int>::iterator iter_lower = s.lower_bound(30);
	set<int>::iterator iter_upper = s.upper_bound(30);
	cout << *iter_lower << '\n';
	cout << *iter_upper << '\n';

	iter_lower = s.lower_bound(55);
	iter_upper = s.upper_bound(55);
	// lower_bound() == upper_bound() 이면
	// 찾는 원소가 존재하지 않는다는 것을 의미함.
	if (iter_lower != iter_upper)
		cout << "55 exist\n";
	else
		cout << "55 not exist\n";

	return 0;
}
10 30 40 50 70 80 90
30
40
55 not exist

equal_range()

위에서 연습했듯이 lower_bound와 upper_bound 쌍을 리턴해준다.

#include <iostream>
#include <set>
using namespace std;

int main()
{
	set<int> s;

	s.insert(50);
	s.insert(30);
	s.insert(80);
	s.insert(40);
	s.insert(10);
	s.insert(70);
	s.insert(90);

	for (auto iter = s.begin(); iter != s.end(); iter++)
		cout << *iter << ' ';
	cout << '\n';

	// equal_range 는 lower_bound() 와 upper_bound() 쌍으로 반환
	auto iter = s.equal_range(30);
	cout << *iter.first << '\n';
	cout << *iter.second << '\n';

	iter = s.equal_range(55);
	if (iter.first != iter.second)
		cout << "55 exist\n";
	else
		cout << "55 not exist\n";

	return 0;
}
10 30 40 50 70 80 90
30
40
55 not exist

다양하게 연습해보기

#include <iostream>
#include <set>
using namespace std;

int main()
{
	set<int> s{ 1,2,3,4,5 };

	s.insert(3); // 1 2 3 4 5
	s.insert(5); // 1 2 3 4 5
	s.insert(7); // 1 2 3 4 5 7
	s.insert(-1);// -1 1 2 3 4 5 7

	if (s.find(7) != s.end())
		cout << "7 found!\n";
	else
		cout << "7 not found!\n";

	cout << "s.empty() : " << s.empty() << endl;
	cout << "s.size() : " << s.size() << endl;
	cout << "s.max_size() : " << s.max_size() << endl << endl;

	set<int> s2 = { 1,2,3,4,5,6,7,8,9,10 };
	pair<set<int>::iterator, set<int>::iterator> iter = s2.equal_range(4);
	cout << "s2.equal_range(4) : " << *iter.first << ' ' << *iter.second << endl;

	set<int>::iterator lower_iter = s2.lower_bound(3);
	set<int>::iterator upper_iter = s2.upper_bound(3);
	cout << "lower_iter : " << *lower_iter << endl;
	cout << "upper_iter : " << *upper_iter << endl;

	// 3 ~ 7 삭제
	lower_iter = s2.lower_bound(3);
	upper_iter = s2.upper_bound(7);
	s2.erase(lower_iter, upper_iter);
	for (int n : s2) cout << n << ' ';
	cout << endl << endl;

	set<int> s3;

	s3.emplace(10);
	s3.emplace(20);
	s3.emplace(-1);
	s3.emplace(-5);
	s3.emplace(7);

	set<int>::iterator s3_iter = s3.emplace_hint(s3.end(), 21); // 삽입 속도 빠름
	s3.emplace_hint(s3_iter, 1); // 삽입 속도 느림
	for (int n : s3) cout << n << ' ';
	cout << endl << endl;

	// insert와 emplace 반환값은 pair<iterator, bool>
	// pair.first 삽입한 위치에 대한 iterator
	// pair.second 삽입 성공 여부에 대한 bool
	set<int> s4;
	pair<set<int>::iterator, bool> chk = s4.insert(10);
	if (chk.second)
		cout << "10 insert success!\n";
	else
		cout << "10 insert failure!\n";

	set<pair<int, int>> s5;
	s5.insert({ 2,3 });
	s5.insert({ 1,5 });
	s5.insert({ 1,2 });
	s5.emplace(5, 6);
	for (auto n : s5) cout << n.first << ' ' << n.second << endl;

	return 0;
}
7 found!
s.empty() : 0
s.size() : 7
s.max_size() : 214748364

s2.equal_range(4) : 4 5
lower_iter : 3
upper_iter : 4
1 2 8 9 10

-5 -1 1 7 10 20 21

10 insert success!
1 2
1 5
2 3
5 6

'CS > Data Structure' 카테고리의 다른 글

[C++] multimap  (0) 2021.04.28
[C++] map  (0) 2021.04.28
[C++] unordered_set  (0) 2021.04.12
[C++] multiset  (0) 2021.04.12
스택의 응용 - 후위 표기식  (0) 2021.04.09

+ Recent posts