리스트 / 스택 /큐

1. 리스트 / 스택 / 큐

1) 연속된 자료구조/연결된 자료구조

(1) 연속된 자료구조

  • 모든 원소를 단일 메모리 청크에 저장
  • 각각의 원소는 모두 같은 타입 사용 → 같은 크기의 메모리 사용 →sizeof(type) 으로 표시
  • 시작주소(Base Address)에서 sizeof(type)을 사용하여 i번째 주소에 접근 가능
    • BA + i * sizeof(type)
  • 배열의 유형
    • 정적배열 : 선언된 블록이 끝나면 소멸, 스택 영역에 할당되어 자동으로 해제
    • 동적배열 : 배열의 생성 시점과 소멸 시점을 자유롭게 결정, 힙 영역에 할당되어 직접 해제 전까지 유지

(2) 연결된 자료구조

  • 노드를 사용하여 여러 개의 메모리 청크에 데이터를 저장 → 서로 다른 메모리 위치에 저장
  • 연결리스트
    • 각각의 노드는 저장할 데이터와 다음 노드를 가르키는 포인터를 가짐
    • 마지막 노드는 NULL을 통해 리스트의 마지막을 표현
    • 각 원소에 접근하기 위해서는 next 포인터를 이용하여 접근
    • 배열과 달리 원소의 삽입과 삭제를 매우 빠르게 수행 가능
  • 연결리스트에서 새 원소 삽입
    • 새로운 노드 생성
    • 각 노드의 next 포인터를 수정하여 삽입될 노드의 위치를 조정
  • 연결리스트에서 원소 삭제
    • 연결리스트에 연결되어 있지 않도록 next 포인터 수정
    • 이후 해당 노드 메모리 할당 해제

(3) 두 자료구조 비교

연속된 자료구조(배열) 연결된 자료구조(연결리스트)

모든 데이터가 연속적으로 저장 데이터는 노드에 저장, 노드는 메모리 곳곳에 저장
임의 원소에 즉각적으로 접근 임의 원소 접근하는 것은 선형복잡도를 가짐
데이터가 연속적으로 저장, 캐시지역성 효과로 데이터 순회가 빠름 캐시 지역성 효과가 없으므로 모든 데이터 순회가 느린편
데이터 저장을 위해 정확하게 데이터 크기만큼의 메모리 사용 각 노드에서 포인터 저장을 위해 여분의 메모리 사용

 

💡 C스타일의 배열의 제약사항

———————————————————————————————————————

  • 메모리 할당과 해제를 수동으로 처리 ( 해제하지 않은 경우 메모리 릭 발생가능 )
  • 배열 크기보다 큰 원소를 참조하는 것을 검사하지 못함 ( 세그먼테이션 결함, 메모리 손상 가능 )
  • 배열 중첩 사용 시 문법이 복잡하여 코드 이해에 어려움
  • 깊은 복사가 동작하지 않음

 

2) std::array

(1) array

  • 메모리를 자동으로 할당하고 해제
  • 원소의 타입과 배열 크기를 매개변수로 사용하는 클래스 탬플릿
std::array<int, 10> arr1; // 크기가 10인 int 타입 배열 선언
arr1[0] = 1; // 첫 번째 원소를 1로 설정
std::cout<<arr.at(0)<<std::endl; // at 연산자를 통해 사용가능 -> 예외처리에 효과적
  • 객체를 다른 함수에 전달하는 방식은 기본 데이터 타입과 유사
  • 값 또는 참조로 전달가능 / const를 함께 사용가능 → C처럼 포인터 연산이나 역참조 연산 X
void print(std::array<int,5> arr)
{
	for(auto ele : arr)
		std::cout<<ele<<",";
}
std::array<int,5> arr={1,2,3,4,5};
print(arr);

// 실행결과
// 1,2,3,4,5
  • std::array는 begin()과 end() 멤버함수 제공
for(auto it = arr.begin(); it != arr.end(); it++)
{
	auto element = (*it);
	std::cout<<element<<' ';
}
  • 반복자를 통해 접근 가능

(2) array 원소 접근 함수

함수 설명

front() - 배열의 첫 번째 원소에 대한 참조를 반환
back() - 배열의 마지막 원소에 대한 참조를 반환
data() - 배열 객체 내부에서 실제 데이터 메모리 버퍼를 가르키는 포인터를 반환
  • 반환된 포인터를 이용하여 다양한 포인터 연산을 수행
  • 포인터를 함수 인자로 받는 예전 스타일의 함수 사용 시 유용 |

(3) 연습 문제 1 : 동적 크기 배열 구현하기

#include <iostream>
#include <algorithm>
#include <sstream>

template <typename T>
class dynamic_array
{
	T* data;
	size_t n;

public:
	dynamic_array(int n)
	{
		this->n = n;
		data = new T[n];
	}

	dynamic_array(const dynamic_array<T>& other)
	{
		n = other.n;
		data = new T[n];

		for (int i = 0; i < n; i++)
			data[i] = other[i];
	}

	T& operator[](int index)
	{
		return data[index];
	}

	const T& operator[](int index) const
	{
		return data[index];
	}

	T& at(int index)
	{
		if (index < n)
			return data[index];
		throw "Index out of range";
	}

	size_t size() const
	{
		return n;
	}

	~dynamic_array()
	{
		delete[] data; // 메모리 릭 방지
	}

	T* begin() { return data; }
	const T* begin() const { return data; }
	T* end() { return data + n; }
	const T* end() const { return data + n; }

	friend dynamic_array<T> operator+(const dynamic_array<T>& arr1, dynamic_array<T>& arr2)
	{
		dynamic_array<T> result(arr1.size() + arr2.size());
		std::copy(arr1.begin(), arr1.end(), result.begin());
		std::copy(arr2.begin(), arr2.end(), result.begin() + arr1.size());

		return result;
	}

	std::string to_string(const std::string& sep = ", ")
	{
		if (n == 0)
			return "";

		std::ostringstream os;
		os << data[0];

		for (int i = 1; i < n; i++)
			os << sep << data[i];

		return os.str();
	}
};

struct student
{
	std::string name;
	int standard;
};

std::ostream& operator<<(std::ostream& os, const student& s)
{
	return (os << "[" << s.name << ", " << s.standard << "]");
}

int main()
{
	int nStudents;
	std::cout << "1반 학생 수를 입력하세요: ";
	std::cin >> nStudents;

	dynamic_array<student> class1(nStudents);
	for (int i = 0; i < nStudents; i++)
	{
		std::string name;
		int standard;
		std::cout << i + 1 << "번째 학생 이름과 나이를 입력하세요: ";
		std::cin >> name >> standard;
		class1[i] = student{name, standard};
	}

	// 배열 크기보다 큰 인덱스의 학생에 접근
	try
	{
		// 아래 주석을 해제하면 프로그램이 비정상 종료합니다.
		// class1[nStudents] = student {"John", 8}; // 예상할 수 없는 동작

		class1.at(nStudents) = student{"John", 8}; // 예외 발생
	}
	catch (...)
	{
		std::cout << "예외 발생!" << std::endl;
	}

	// 깊은 복사
	auto class2 = class1;
	std::cout << "1반을 복사하여 2반 생성: " << class2.to_string() << std::endl;

	// 두 학급을 합쳐서 새로운 큰 학급을 생성
	auto class3 = class1 + class2;
	std::cout << "1반과 2반을 합쳐 3반 생성 : " << class3.to_string() << std::endl;

	return 0;
}

(4) 연습 문제 2 : 빠르고 범용적인 데이터 저장 컨테이너 만들기

#include <array>
#include <iostream>
#include <type_traits>

template <typename... Args>
auto build_array(Args &&... args) -> std::array<typename std::common_type<Args...>::type, sizeof...(args)>
{
    using commonType = typename std::common_type<Args...>::type;
    return {std::forward<commonType>(args)...};
}

int main()
{
    auto data = build_array(1, 0u, 'a', 3.2f, false);
    for (auto i : data)
        std::cout << i << " ";
    std::cout << std::endl;

    auto data2 = build_array(1, "Packt", 2.0);
}

3) std::vector

(1) std::array의 단점

  • array의 크기는 컴파일 시간에 결정되는 상수이어야해서 프로그램 실행 중에는 변경 불가능
  • 크기가 고정되어 원소를 추가하거나 삭제할 수 없음
  • 항상 스택 메모리를 사용

(2) std::vector - 가변 크기 배열

  • vector의 선언
std::vector<int> vec; // 크기가 0인 벡터 선언
std::vector<int> vec = {1,2,3,4,5}; // 지정한 초기값으로 이루어진 크기가 5인 벡터
std::vector<int> vec(10); // 크기가 10인 벡터
std::vector<int> vec(10,5); // 크기가 10이고, 모든 원소가 5로 초기화된 벡터 선언
  • vector 원소 추가
vec.push_back(1); // 새 원소를 벡터의 맨 뒤에 추가
vec.insert(vec.begin(),0); // 반복자 위치에 해당 원소 삽입
  • vector 원소 제거
vec.pop_back(); // 맨 마지막 원소 하나를 제거
vec.erase(vec.begin()); // 맨 처음 원소를 제거
  • vector의 나머지 유용한 멤버함수
clear() // 모든 원소를 제거하여 완전히 비어있는 벡터로 만든다
reverse(capacity) // 벡터에서 사용할 용량을 지정, 
									// 현재 용량보다 매개변수가 클 경우 매개변수 크기만큼 재할당
									// 벡터의 크기를 변경하지 않음 (용량만 증가)
shrink_to_fit() // 여분의 메모리 공간을 해제하는 용도로 사용
								// 벡터의 용량이 현재 벡터 크기와 같게 설정

(3) std::vector 할당자

  • 템플릿 매개변수에서 데이터 타입 다음에 할당자를 전달 가능
  • 사용자 정의 할당자를 통해 배열의 단점을 해결할 수 있음

4) std::forward_list

  • 연결 리스트에 대한 래퍼 클래스

(1) std::forward_list에서 원소 삽입과 삭제

  • 원소 삽입
std::forward_list<int> fwd_list = {1,2,3};
fwd_list.push_front(0); // 맨 앞에 0 추가 : {0,1,2,3}
auto it = fwd_list.begin();
fwd_list.insert_after(it,5); // 맨 처음 원소 뒤에 5 추가 : {0,5,1,2,3}
fwd_list.insert_after(it,6); // 같은 위치에 6 추가 : {0,6,5,1,2,3}
  • 원소 삭제
std::forward_list<int> fwd_list = {1,2,3,4,5};
fwd_list.pop_front(); // 맨 앞 원소를 삭제 : {2,3,4,5}
auto it = fwd_list.begin();
fwd_list.erase_after(it); // 맨 앞의 다음 원소 삭제 : {2,4,5}
fwd_list.erase_after(it,fwd_list.end()); // 맨 앞 원소 다음부터 맨 마지막 원소까지 삭제 : {2}

(2) std::forward_list의 기타 멤버 함수

  • remove() / remove_if() : 해당하는 원소를 찾아 삭제하는 멤버 함수
    • remove는 등호 연산을 통해 삭제
    • remove_if는 좀 더 유연한 조건부 삭제 수행

(3) 연습 문제 3: 연결 리스트에서 remove_if() 함수를 이용한 조건부 원소 삭제

#include <string>
#include <iostream>
#include <forward_list>

struct citizen
{
	std::string name;
	int age;
};

std::ostream &operator<<(std::ostream &os, const citizen &c)
{
	return (os << "[" << c.name << ", " << c.age << "]");
}

int main()
{
	std::forward_list<citizen> citizens = {
		{"Kim", 22}, {"Lee", 25}, {"Park", 18}, {"Jin", 16}
	};

	auto citizens_copy = citizens; // 깊은 복사

	std::cout << "전체 시민: ";
	for (const auto &c : citizens)
		std::cout << c << " ";
	std::cout << std::endl;

	citizens.remove_if([](const citizen &c) {
		// 나이가 19세보다 작으면 리스트에서 제거합니다.
		return (c.age < 19);
	});

	std::cout << "투표권이 있는 시민: ";
	for (const auto &c : citizens)
		std::cout << c << " ";
	std::cout << std::endl;

	citizens_copy.remove_if([](const citizen &c) {
		return (c.age != 18);
	});

	std::cout << "내년에 투표권이 생기는 시민: ";
	for (const auto &c : citizens_copy)
		std::cout << c << " ";
	std::cout << std::endl;
}

5) 반복자

  • 반복자는 포인터와 비슷하지만 STL 컨테이너에 대한 공통적인 인터페이스를 제공
  • advance() : 해당 반복자와 거리 값을 인자로 받고 반복자를 거리 값만큼 증가
  • next() / prev(): 해당 반복자와 거리 값을 인자로 받고 지정한 거리만큼 떨어진 위치의 반복자 반환
  • 해당 반복자가 지원할 경우에만 동작 → 순방향 반복자에서는 prev() 사용 불가

(1) 연습 문제 4: 다양한 반복자에서 이동하기

#include <iostream>
#include <forward_list>
#include <vector>

int main()
{
	std::vector<std::string> vec = {
		"Lewis Hamilton", "Lewis Hamilton", "Nico Roseberg",
		"Sebastian Vettel", "Lewis Hamilton", "Sebastian Vettel",
		"Sebastian Vettel", "Sebastian Vettel", "Fernando Alonso"
	};

	auto it = vec.begin(); // 상수 시간
	std::cout << "가장 최근 우승자: " << *it << std::endl;

	it += 8; // 상수 시간
	std::cout << "8년전 우승자: " << *it << std::endl;

	advance(it, -3); // 상수 시간
	std::cout << "그후 3년 뒤 우승자: " << *it << std::endl;

	std::forward_list<std::string> fwd(vec.begin(), vec.end());

	auto it1 = fwd.begin();
	std::cout << "가장 최근 우승자: " << *it1 << std::endl;

	advance(it1, 5); // 선형 시간
	std::cout << "5년전 우승자: " << *it1 << std::endl;

	// std::forward_list는 순방향으로만 이동할 수 있으므로
	// 아래 코드는 에러가 발생합니다. 
	// advance(it1, -2);

(2) 연습 문제 5: 기본적인 사용자 정의 컨테이너 만들기

#include <iostream>
#include <algorithm>

struct singly_ll_node
{
	int data;
	singly_ll_node* next;
};

class singly_ll
{
public:
	using node = singly_ll_node;
	using node_ptr = node*;

private:
	node_ptr head;

public:
	void push_front(int val)
	{
		auto new_node = new node{val, NULL};
		if (head != NULL)
			new_node->next = head;
		head = new_node;
	}

	void pop_front()
	{
		auto first = head;
		if (head)
		{
			head = head->next;
			delete first;
		}
	}

	struct singly_ll_iterator
	{
	private:
		node_ptr ptr;

	public:
		singly_ll_iterator(node_ptr p) : ptr(p) {}

		int& operator*() { return ptr->data; }

		node_ptr get() { return ptr; }

		singly_ll_iterator& operator++() // 선행 증가
		{
			ptr = ptr->next;
			return *this;
		}

		singly_ll_iterator operator++(int) // 후행 증가
		{
			singly_ll_iterator result = *this;
			++(*this);
			return result;
		}

		friend bool operator==(const singly_ll_iterator& left, const singly_ll_iterator& right)
		{
			return left.ptr == right.ptr;
		}

		friend bool operator!=(const singly_ll_iterator& left, const singly_ll_iterator& right)
		{
			return left.ptr != right.ptr;
		}
	};

	singly_ll_iterator begin() { return singly_ll_iterator(head); }
	singly_ll_iterator end() { return singly_ll_iterator(NULL); }
	singly_ll_iterator begin() const { return singly_ll_iterator(head); }
	singly_ll_iterator end() const { return singly_ll_iterator(NULL); }

	singly_ll() = default;

	singly_ll(const singly_ll& other) : head(NULL)
	{
		if (other.head)
		{
			head = new node{0, NULL};
			auto cur = head;
			auto it = other.begin();
			while (true)
			{
				cur->data = *it;

				auto tmp = it;
				++tmp;
				if (tmp == other.end())
					break;

				cur->next = new node{0, NULL};
				cur = cur->next;
				it = tmp;
			}
		}
	}

	singly_ll(const std::initializer_list<int>& ilist) : head(NULL)
	{
		for (auto it = std::rbegin(ilist); it != std::rend(ilist); it++)
			push_front(*it);
	}
};

int main()
{
	singly_ll sll = {1, 2, 3};
	sll.push_front(0);

	std::cout << "첫 번째 리스트: ";
	for (auto i : sll)
		std::cout << i << " "; // 출력: 0 1 2 3
	std::cout << std::endl;

	auto sll2 = sll;
	sll2.push_front(-1);
	std::cout << "첫 번째 리스트를 복사한 후, 맨 앞에 -1을 추가: ";
	for (auto i : sll2)
		std::cout << i << ' '; // 출력: -1 0 1 2 3
	std::cout << std::endl;

	std::cout << "깊은 복사 후 첫 번째 리스트: ";

	for (auto i : sll)
		std::cout << i << ' '; // 출력: 0 1 2 3
	std::cout << std::endl;
}

(3) 실습 문제 1: 음악 재생 목록 구현하기 ( 원형 연결 리스트 구현)

#include <iostream>

template <typename T>
struct cir_list_node
{
	T* data;
	cir_list_node* next;
	cir_list_node* prev;

	~cir_list_node()
	{
		delete data;
	}
};

template <typename T>
struct cir_list
{
public:
	using node = cir_list_node<T>;
	using node_ptr = node*;

private:
	node_ptr head;
	size_t n;

public:
	cir_list() : n(0)
	{
		head = new node {NULL, NULL, NULL}; // 모두 NULL로 구성된 기본 노드
		head->next = head;
		head->prev = head;
	}

	size_t size() const
	{
		return n;
	}

	void insert(const T& value)
	{
		node_ptr newNode = new node {new T(value), NULL, NULL};
		n++;
		auto dummy = head->prev;
		dummy->next = newNode;
		newNode->prev = dummy;
		if (head == dummy)
		{
			dummy->prev = newNode;
			newNode->next = dummy;
			head = newNode;
			return;
		}
		newNode->next = head;
		head->prev = newNode;
		head = newNode;
	}

	void erase(const T& value)
	{
		auto cur = head, dummy = head->prev;
		while (cur != dummy)
		{
			if (*(cur->data) == value)
			{
				cur->prev->next = cur->next;
				cur->next->prev = cur->prev;
				if (cur == head)
					head = head->next;
				delete cur;
				n--;
				return;
			}
			cur = cur->next;
		}
	}

	struct cir_list_it
	{
	private:
		node_ptr ptr;

	public:
		cir_list_it(node_ptr p) : ptr(p) {}

		T& operator*()
		{
			return *(ptr->data);
		}

		node_ptr get()
		{
			return ptr;
		}

		cir_list_it& operator++()
		{
			ptr = ptr->next;
			return *this;
		}

		cir_list_it operator++(int)
		{
			cir_list_it it = *this;
			++(*this);
			return it;
		}

		cir_list_it& operator--()
		{
			ptr = ptr->prev;
			return *this;
		}

		cir_list_it operator--(int)
		{
			cir_list_it it = *this;
			--(*this);
			return it;
		}

		friend bool operator==(const cir_list_it& it1, const cir_list_it& it2)
		{
			return it1.ptr == it2.ptr;
		}

		friend bool operator!=(const cir_list_it& it1, const cir_list_it& it2)
		{
			return it1.ptr != it2.ptr;
		}
	};

	cir_list_it begin()
	{
		return cir_list_it {head};
	}

	cir_list_it begin() const
	{
		return cir_list_it {head};
	}

	cir_list_it end()
	{
		return cir_list_it {head->prev};
	}

	cir_list_it end() const
	{
		return cir_list_it {head->prev};
	}

	cir_list(const cir_list<T>& other) : cir_list()
	{
		// 아래 코드는 원소를 역순으로 삽입하지만, 
		// 원형 리스트이기 때문에 문제가 없습니다.
		for (const auto& i : other)
			insert(i);
	}

	cir_list(const std::initializer_list<T>& il) : head(NULL), n(0)
	{
		// 아래 코드는 원소를 역순으로 삽입하지만, 
		// 원형 리스트이기 때문에 문제가 없습니다.
		for (const auto& i : il)
			insert(i);
	}

	~cir_list()
	{
		while (size())
		{
			erase(*(head->data));
		}

		delete head;
	}
};

struct playlist
{
	cir_list<int> list;

	void insert(int song)
	{
		list.insert(song);
	}

	void erase(int song)
	{
		list.erase(song);
	}

	void loopOnce()
	{
		for (auto& song : list)
			std::cout << song << " ";
		std::cout << std::endl;
	}
};

int main()
{
	playlist pl;
	pl.insert(1);
	pl.insert(2);
	std::cout << "재생 목록 : ";
	pl.loopOnce();

	playlist pl2 = pl;
	pl2.erase(2);
	pl2.insert(3);
	std::cout << "두 번째 재생 목록 : ";
	pl2.loopOnce();
}

6) std::list

  • 이중 연결 리스트의 구조

(1) std::list의 멤버 함수

  • std::forward_list와 유사하거나 같은 멤버함수
  • insert() / emplace() → forward_list의 insert_after / emplace_after와 대응
  • push_back() / emplace_back() / pop_back() → forward_list에서 사용할 수 없는 멤버함수

(2) 연습 문제 6 : std::list의 삽입 또는 삭제 함수 사용하기

#include <iostream>
#include <list>

int main()
{
	std::list<int> list1 = {1, 2, 3, 4, 5};
	list1.push_back(6);                   // {1, 2, 3, 4, 5, 6}
	list1.insert(next(list1.begin()), 0); // {1, 0, 2, 3, 4, 5, 6}
	list1.insert(list1.end(), 7);         // {1, 0, 2, 3, 4, 5, 6, 7}
	list1.pop_back();                     // {1, 0, 2, 3, 4, 5, 6}

	std::cout << "삽입 & 삭제 후 리스트: ";
	for (auto i : list1)
		std::cout << i << " ";
}

(3) 양방향 반복자

  • std::list는 역방향 이동이 가능하지만 임의 접근 반복자처럼 한 번에 이동하지는 못함
  • 따라서 선형 시간 복잡도를 가지고 있음

(4) 반복자 무효화

  • vector의 경우 push_back()과 insert() 함수 사용 시 기존에 사용하던 반복자와 포인터 참조는 모두 무효화
  • list의 경우 삽입, 삭제 동작에서 원소 이동이 필요없어 반복자 무효화되지 않는다
  • 다만 특정 원소 삭제의 경우, 삭제되는 원소를 가리키던 반복자는 무효화

(5) 실습 문제 2 : 카드게임 시뮬레이션

#include <iostream>
#include <vector>
#include <array>
#include <sstream>
#include <algorithm>
#include <random>
#include <chrono>

struct card
{
	int number;

	enum suit
	{
		HEART,
		SPADE,
		CLUB,
		DIAMOND
	} suit;

	std::string to_string() const
	{
		std::ostringstream os;
		
		if (number > 0 && number <= 10)
			os << number;
		else
		{
			switch (number)
			{
			case 1:
				os << "Ace";
				break;
			case 11:
				os << "Jack";
				break;
			case 12:
				os << "Queen";
				break;
			case 13:
				os << "King";
				break;
			default:
				return "Invalid card";
			}
		}
		
		os << " of ";
		
		switch (suit)
		{
		case HEART:
			os << "hearts";
			break;
		case SPADE:
			os << "spades";
			break;
		case CLUB:
			os << "clubs";
			break;
		case DIAMOND:
			os << "diamonds";
			break;
		}
		
		return os.str();
	}
};

struct game
{
	std::array<card, 52> deck;
	std::vector<card> player1, player2, player3, player4;

	void buildDeck()
	{
		for (int i = 0; i < 13; i++)
			deck[i] = card {i + 1, card::HEART};
		for (int i = 0; i < 13; i++)
			deck[i + 13] = card {i + 1, card::SPADE};
		for (int i = 0; i < 13; i++)
			deck[i + 26] = card {i + 1, card::CLUB};
		for (int i = 0; i < 13; i++)
			deck[i + 39] = card {i + 1, card::DIAMOND};
	}

	void dealCards()
	{
		unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
		std::shuffle(deck.begin(), deck.end(), std::default_random_engine(seed));
		player1 = {deck.begin(), deck.begin() + 13};
		player2 = {deck.begin() + 13, deck.begin() + 26};
		player3 = {deck.begin() + 26, deck.begin() + 39};
		player4 = {deck.begin() + 39, deck.end()};
	}

	bool compareAndRemove(std::vector<card>& p1, std::vector<card>& p2)
	{
		if (p1.back().number == p2.back().number)
		{
			p1.pop_back();
			p2.pop_back();
			return true;
		}
		return false;
	}

	void playOneRound()
	{
		if (compareAndRemove(player1, player2))
		{
			compareAndRemove(player3, player4);
			return;
		}
		else if (compareAndRemove(player1, player3))
		{
			compareAndRemove(player2, player4);
			return;
		}
		else if (compareAndRemove(player1, player4))
		{
			compareAndRemove(player2, player3);
			return;
		}
		else if (compareAndRemove(player2, player3))
		{
			return;
		}
		else if (compareAndRemove(player2, player4))
		{
			return;
		}
		else if (compareAndRemove(player3, player4))
		{
			return;
		}

		unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
		std::shuffle(player1.begin(), player1.end(), std::default_random_engine(seed));
		std::shuffle(player2.begin(), player2.end(), std::default_random_engine(seed));
		std::shuffle(player3.begin(), player3.end(), std::default_random_engine(seed));
		std::shuffle(player4.begin(), player4.end(), std::default_random_engine(seed));
	}

	bool isGameComplete() const
	{
		return player1.empty() || player2.empty() || player3.empty() || player4.empty();
	}

	void playGame()
	{
		while (not isGameComplete())
		{
			playOneRound();
		}
	}

	int getWinner() const
	{
		if (player1.empty())
			return 1;
		if (player2.empty())
			return 2;
		if (player3.empty())
			return 3;
		if (player4.empty())
			return 4;
	}
};

int main()
{
	game newGame;
	newGame.buildDeck();
	newGame.dealCards();
	newGame.playGame();
	auto winner = newGame.getWinner();
	std::cout << winner << "번 플레이어가 이겼습니다!" << std::endl;
}

7) std::deque

  • 양방향 큐 (double-ended queue) 구조

(1) deque의 구조

  • deque의 경우, 양방향으로 매우 빠르게 확장 가능/ 모든 원소에 임의 접근을 제공
  • 모든 메모리 청크 주소를 연속적인 메모리 구조에 저장할 경우 임의 접근 가능
  • 따라서, 맨 앞에 원소 추가 시 메모리 청크의 앞의 메모리 여유 공간이 있을 경우 청크 할당 후 첫 번째 메모리 주소로 설정
  • 이 때 새로 할당하는 메모리 공간은 새로 할당하지만 이전에 있던 메모리는 그대로 유지
  • 벡터와 같이 임의 접근 반복자 제공 / 사용자 정의 반복자 사용 가능

8) 컨테이너 어뎁터

(1) std::stack

  • std::deque를 기본 컨테이너로 사용
  • LIFO의 특징 제공
  • 모든 연산이 O(1)의 시간 복잡도
  • 어뎁터 반복자는 따로 제공하지 않음

(2) std::queue

  • FIFO의 특징 제공
  • std::deque를 기본 컨테이너로 사용
  • 모든 연산이 O(1)의 시간 복잡도
  • 어뎁터 반복자는 따로 제공하지 않음

(3) std::priority_queue

  • 우선순위 큐는 힙 구조를 제공
  • 따라서 최소 / 최대 원소 접근이 제일 빠름
  • 삽입/삭제는 O(log N) 시간 복잡도로 제공
  • std::vector를 기본 컨테이너로 사용
  • 어뎁터 반복자는 따로 제공하지 않음

(4) 실습 문제 3 : 사무실 공유 프린터의 인쇄 대기 목록 시뮬레이션

#include <iostream>
#include <queue>

class Job
{
	int id;
	std::string user;
	int pages;

	static int count;

public:
	Job(const std::string& u, int p) : user(u), pages(p), id(++count) {}

	friend std::ostream& operator<<(std::ostream& os, const Job& j)
	{
		os << "id: " << j.id << ", 사용자: " << j.user << ", 페이지수: " << j.pages << "장";
		return os;
	}
};

int Job::count = 0;

template <size_t N>
class Printer
{
	std::queue<Job> jobs;

public:
	bool addNewJob(const Job& job)
	{
		if (jobs.size() == N)
		{
			std::cout << "인쇄 대기열에 추가 실패: " << job << std::endl;
			return false;
		}

		std::cout << "인쇄 대기열에 추가: " << job << std::endl;
		jobs.push(job);
		return true;
	}

	void startPrinting()
	{
		while (not jobs.empty())
		{
			std::cout << "인쇄 중: " << jobs.front() << std::endl;
			jobs.pop();
		}
	}
};

int main()
{
	Printer<5> printer;

	Job j1("광희", 10);
	Job j2("정다", 4);
	Job j3("현수", 5);
	Job j4("유미", 7);
	Job j5("채원", 8);
	Job j6("시원", 10);

	printer.addNewJob(j1);
	printer.addNewJob(j2);
	printer.addNewJob(j3);
	printer.addNewJob(j4);
	printer.addNewJob(j5);
	printer.addNewJob(j6); // 인쇄 대기열이 가득 차있어서 추가할 수 없음
	printer.startPrinting();

	printer.addNewJob(j6); // 인쇄 대기열이 비었으므로 추가 가능
	printer.startPrinting();
}

 

 

출처 : 길벗 (코딩테스트를 위한 자료 구조와 알고리즘 with C++)

 

'Computer Science > Data Structure & Algorithm' 카테고리의 다른 글

그리디 알고리즘  (0) 2023.07.02
분할 정복  (0) 2023.07.01
해시 테이블과 블룸 필터  (0) 2023.06.29
트리/힙/그래프(2) - 힙&그래프  (0) 2023.06.28
트리/힙/그래프(1) - 트리  (0) 2023.06.27