구글링으로 원형큐 구현을 보면 이상하게 원형큐의 사이즈가 정해져있고, 꽉차면 원형큐의 크기가 커지지않고 채울수 없는 구현만 봤다. 내가 못본걸수도 있지만,,,

이건 너무 이상해서 언제든 크기조절이 가능한 큐를 구현해보았다.

 

기존 원형큐의 단점은 위에 설명했듯이 큐에 데이터가 꽉차면 더이상 값을 넣을 수 없고, 그렇다고 큐의 사이즈를 키울 수도 없고, 빈공간을 효율적으로 관리하지만, 큐의 사이즈를 초기값설정후 다시 늘릴 수 없는 것이다.

 

분명 개발중 큐의 크기를 추정하기 애매할 때가 있을 거라 생각하고 원형큐의 장점인 빈 공간활용을 살리되, 언제든 벡터(vector)처럼 큐의 크기를 늘릴수 있는 기능을 추가하고 싶었다.


	void Reserve(size_t capacity) override
	{
		//기존 캐퍼시티가 더 크다면 재할당을 하지 않습니다
		if (this->my_capacity >= capacity)
			return;

		Data* temp = new Data[capacity];

		//Debug
		for (size_t i = 0; i < capacity; ++i)
		{
			temp[i] = NULL;
		}

		//0번째 인덱스부터 rear까지 새로 할당한 배열에 복사합니다
		for (size_t i = 0; i < _rear; ++i)
		{
			temp[i] = this->my_base[i];
		}

		//컨테이너에 원소가 있다면 새로 할당한 주소로 옮겨줍니다
		if (this->my_capacity > 0 &&
			this->my_size > 0 &&
			_rear <= _front)
		{
			//front부터 마지막인덱스까지 새로 할당한 배열에 복사합니다
			for (size_t i = 0; i < this->my_capacity - _front; ++i)
			{
				temp[capacity - i - 1] = this->my_base[this->my_capacity - i - 1];
			}
			_front += capacity - this->my_capacity;

			delete[] this->my_base;
		}

		
		this->my_capacity = capacity;
		this->my_base = temp;
	}

	//마지막 위치에 원소를 추가합니다
	//Enqueue()
	void PushBack(const Data& data)
	{
		//rear의 위치가 배열의 최대 크기를 넘었는지 확인
		if (_rear >= this->my_capacity)
		{
			_rear = 0;
		}

		//컨테이너에 비어있는 자리가 있는지 확인
		//rear와 front가 동일한 인덱스를 가리키면 큐가 꽉찼거나 비어있음을 의미합니다
		if (_rear == _front)
		{
			//큐가 꽉찼으면 재할당합니다
			if (this->my_size > 0)
			{
				this->Reserve(this->my_capacity + this->additive_capacity);
			}
		}

		//추가합니다
		this->my_base[_rear++] = data;
		++this->my_size;
	}

	//가장 처음으로 들어온 원소를 반환합니다
	//디버깅을 위해 bool로 반환의 실패여부를 체크합니다
	//Dequeue
	const bool PopFront(Data& out_data)
	{
		//rear와 front가 동일한 인덱스를 가리키면 큐가 꽉찼거나 비어있음을 의미합니다
		if (_rear == _front)
		{
			//큐가 비어있으면 Pop을 하지 않습니다
			if (this->my_size <= 0)
			{
				return false;
			}
		}

		if (_front >= this->my_capacity)
		{
			_front = 0;
		}

		--this->my_size;

		//return this->my_base[_front++];

		//Debug
		Data& data = this->my_base[_front++];
		Data data_new = data;
		out_data = data_new;
		data = NULL;

		return true;
	}

PushBack()을 보면 데이터가 꽉 찰때Reserve()함수를 사용하는데 이것말고는 기존 원형큐과 똑같고 PopFront()도 변경점은 없다.

 

자세히 봐야할것은 Reserve()함수인데 이 함수는 이전에 벡터(vector)구현할 때 소개했던것과 동일하지만, 기존 데이터를 재할당한 배열에 추가하는 방법이 조금 달라졌다.

 

데이터가 앞쪽에 그대로 있다면 이런식으로 재할당한 배열에 추가된다

벡터와 동일하게 데이터가 추가되지만

만약 뒷 공간이 꽉차서 앞 공간까지 넘어오게 되었을때는 이런식으로 동작한다

이건 벡터와 동일하다.

 

rear가 뒷공간을 넘어서 앞으로 넘어오게된 상황에서 어떻게 재할당이 이뤄지는것이 핵심이다

rear와 front사이의 공간을 확보한다.

이말은 추가데이터의 위치를 가리키는 rear의 뒷부분을 확보한다고 봐도 된다

그러기 위해 front를 기준으로 뒷부분의 데이터를 옮긴다.

 

데이터가 꽉차서 자동으로 재할당이 될 경우는 이렇게 된다.

rear와 front가 겹치고, 데이터가 있다는 뜻은 꽉찼다는 뜻이므로 rear와 front가 겹쳐있을 때 재할당이 이루어진다.

front위치 이후의 3개의 데이터가 계속 뒤로 밀리고 있으며 rear위치뒤에 빈공간을 계속 생성하는게 보일것이다.

 

벡터와 마찬가지로 크기재할당은 가급적 피하는것이 좋다. 

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

원형큐의 구현은 어렵지 않다.

Enqueue하는데 뒷 공간이 없다면 앞으로 오면되고,,,

Dequeue또한 마찬가지이고...

 

그래서 딱 하나만 추가하면 된다

큐가 꽉찼거나 비어있음을 판단

이것만 구현한다면 원형큐도 이젠 쉽다!

 

	MyQueue_FixedCircle(size_t i_init_size)
	{
		//my_base가 nullptr지만
		//사이즈및 캐퍼시티가 0이므로 원소를 추가할때 Reserve()함수에서 할당이 이뤄집니다
		this->my_size = 0;
		this->my_capacity = i_init_size;

		Data* temp = new Data[this->my_capacity];

		this->my_base = temp;

		//Debug
		for (size_t i = 0; i < this->my_capacity; ++i)
			this->my_base[i] = NULL;
	}
    //마지막 위치에 원소를 추가합니다
	//Enqueue()
	const bool PushBack(const Data& data)
	{
		//rear의 위치가 배열의 최대 크기를 넘었는지 확인
		if (_rear >= this->my_capacity)
		{
			_rear = 0;
		}

		//컨테이너에 비어있는 자리가 있는지 확인
		//rear와 front가 동일한 인덱스를 가리키면 큐가 꽉찼거나 비어있음을 의미합니다
		if (_rear == _front)
		{
			//큐가 꽉찼으면 데이터를 채울 수 없습니다
			if (this->my_size > 0)
			{
				return false;
			}
		}

		//추가합니다
		this->my_base[_rear++] = data;
		++this->my_size;
		return true;
	}

	//가장 처음으로 들어온 원소를 반환합니다
	//디버깅을 위해 bool로 반환의 실패여부를 체크합니다
	//Dequeue
	const bool PopFront(Data& out_data)
	{
		//rear와 front가 동일한 인덱스를 가리키면 큐가 꽉찼거나 비어있음을 의미합니다
		if (_rear == _front)
		{
			//큐가 비어있으면 Pop을 하지 않습니다
			if (this->my_size <= 0)
			{
				return false;
			}
		}

		if (_front >= this->my_capacity)
		{
			_front = 0;
		}

		--this->my_size;

		//return this->my_base[_front++];

		//Debug
		Data& data = this->my_base[_front++];
		Data data_new = data;
		out_data = data_new;
		data = NULL;

		return true;
	}

다른 원형큐 구현을 보면 %연산자를 쓰고, isEmpty() isFull()등의 함수를 추가로 구현하던데 나는 그렇게 하지않고 최대한 직관적으로 구현했다.

EnQ나 DeQ를 하면 rear, front를 ++해주고 최대 사이즈를 넘어가면 0으로 돌아가게 하고,

내 원형큐는 데이터의 개수를 카운팅하고 있어서 rear==front라면 데이터개수를 판단해 큐가 비었는지 꽉찼는지 알 수 있다.

 

생성자를 보면 원형큐의 크기를 '무조건' 설정해줘야한다. 중간에 늘리거나 줄일 수 없다.

다른 원형큐 구현을 보면 const size_t QUEUE_SIZE = 1000; 뭐 이런식으로 하드코딩 해놓기도 하는데 똑같으면 재미없으니까 사용자가 원하는대로 크기를 조절할 수 있게 해놓았다

 

생성자 코드의 자세한 내용은 여기서 Reserve()구현부를 보면 확인할 수 있다.

https://forestbird0.tistory.com/38

 

[C++] 나만의 Vector만들기 (2) - 생성자, 소멸자, 캐퍼시티, Reserve

생성자와 소멸자 코드 MyVector() { //my_base가 nullptr지만 //사이즈및 캐퍼시티가 0이므로 원소를 추가할때 Reserve()함수에서 할당이 이뤄집니다 this->my_base = nullptr; this->my_capacity = this->my_size = 0; } MyVecto

forestbird0.tistory.com

 

다음 포스팅은 여기서 더 발전된 '동적 원형 큐'인데 대단한건 아니고 사이즈를 언제든 늘릴 수 있게 구현한 원형큐다.

 

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

이전 포스팅에서 구현했던 '동적 배열 큐'(선형큐)의 단점을 보완한 '원형 큐'.

원형큐는 선형큐의 단점인 '빈 공간 낭비'를 해결해서, 비교적 효율적으로 빈 공간을 관리한다.

원형큐를 간단하게 설명하면 데이터를 추가할 때 뒷 공간이 꽉차면 다시 비어있는 앞공간을 메꾸는 원리이다.

 

선형큐처럼 front와 rear변수로 데이터의 입출력위치를 관리한다

front: Dequeue()호출 시 꺼낼 데이터의 위치를 가리킴.(큐에 데이터가 없다면 빈 공간을 가리킴.)

rear: Enqueue()호출 시 데이터를 추가할 빈 공간을 가리킴.(큐에 데이터가 꽉차면 빈 공간이 아니라 데이터를 가리킴)

나는 front는 꺼내야할 데이터의 위치를, rear는 데이터가 추가될 빈공간의 위치를 가리키게 했는데,

구현하는 사람에 따라서 다르게 작성할 수도있다. 

 

여기서 데이터를 En,Dequeue하게 되면...

rear는 항상 빈 공간을 가리켜야하고, front는 항상 데이터를 가리켜야한다.

rear가 데이터를, front가 빈공간을 가리키는것은 큐에 데이터가 꽉차있거나 비었다는 뜻이다.

 

여기서 데이터를 3개이상 추가한다고 했을 때, 선형큐같은 경우 큐의 크기가 커져야 한다.

원형큐라면?

숫자 7데이터가 큐의 처음 위치로 오게 되었다.

이 모양을 선형으로 표현하면 아래의 모습으로 나온다

 

여기서 데이터를 하나 더 추가하면?

이런 모습이 되고 front와 rear가 같은곳을 가리키고 있다.

이렇게 front와 rear가 같은곳을 가리킨다면 큐가 비어있거나, 꽉찼다는 의미이다

 

꽉 차게 되면 Enqueue를 할 수 없다.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

큐(Queue)는 FIFO(First-In First-Out)형식으로 먼저들어온 데이터가 먼저 빠져나가게 된다.

스택을 구현해봤다면 큐도 쉽게 만들 수 있다.

하지만 이런식으로 구현하면 Dequeue를 할 때마다 앞공간이 비워지게 되고 데이터는 계속 쌓이게 된다.

시간이 지나면 데이터는 1개밖에 없는데 큐의 크기는 훨씬 커져 있어서 빈 공간을 낭비하고 있게 된다.

빈 공간을 채울려면 Pop할 때마다 모든 데이터를 앞 쪽으로 한칸씩 옮겨야 하는데 데이터가 무수히 많다면 성능에 좋지 않을것이다.

 

그래서 나온 것이 원형 큐(circular queue)이다.

간단하게 설명하면 원형 큐에 데이터를 넣을 때 뒷공간에 빈자리가 없다면, 앞 공간의 빈자리에 데이터를 채워넣게 되서 빈 공간을 낭비없이 사용하게 된다.

 

여기서 내가 추가로 구현한 것은 '동적 원형 큐'이다.

원형 큐를 구글링해보니까 대부분 fixed array를 사용하던데, fixed array를 사용하면 원형큐의 모든 공간이 꽉차면 데이터를 추가할 수 없게된다

동적 원형 큐는 벡터(vector)처럼 언제든 큐의 크기를 재할당을 해서 빈공간을 만드는 방법이다.

 

결국 3개의 큐를 구현하게 되었다

 


1. 기존방식의 동적배열을 이용한 큐

https://forestbird0.tistory.com/44

 

[C++] 나만의 Queue만들기 (1) - 동적 배열을 이용해서 구현

동적 배열로 구현하면 큐의 단점을 명확하게 볼 수 있다. 구현은 쉽지만, 직접 구현해보면서 어떤 문제가 있는지 보자 이렇게 데이터를 추가하면 차곡차곡 쌓이는데 데이터를 꺼내면 앞 공간이

forestbird0.tistory.com

 

2. 원형 큐

https://forestbird0.tistory.com/45

 

[C++] 나만의 Queue만들기 (2) - 원형 큐, 기초

이전 포스팅에서 구현했던 '동적 배열 큐'(선형큐)의 단점을 보완한 '원형 큐'. 원형큐는 선형큐의 단점인 '빈 공간 낭비'를 해결해서, 비교적 효율적으로 빈 공간을 관리한다. 원형큐를 간단하게

forestbird0.tistory.com

 

3. 동적 원형 큐

https://forestbird0.tistory.com/47

 

[C++] 나만의 Queue만들기 (4) - 동적 원형 큐

구글링으로 원형큐 구현을 보면 이상하게 원형큐의 사이즈가 정해져있고, 꽉차면 원형큐의 크기가 커지지않고 채울수 없는 구현만 봤다. 내가 못본걸수도 있지만,,, 이건 너무 이상해서 언제든

forestbird0.tistory.com

 

+ Recent posts