연결형 큐


큐를 링크드리스트 형태로 구현할 수 있습니다. 스택에서처럼 말이죠. 스택과는 다르게 큐는 front와 rear가 있기 때문에 큐는 두개의 노드를 갖고 있어야합니다.


아래 그림처럼 말이죠.




큐의 특징은 알아보았으니(또는 이미 알고있거나) 어떻게 구현을 할 지 생각해보도록 합시다.




구현

이제 Node라는 구조체 변수를 갖는 LinkedQueue를 구현해보겠습니다. 하나하나 차근차근히요.

Node구조체는 아래와 같습니다. data와 다음 노드를 가리키기 위한 next변수를 가지고 있지요.


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

이제 LinkedQueue를 간단하게 정의해보지요.

struct LinkedQueue {
	Node *front, *rear;
	int len;
	LinkedQueue() {
		front = rear = NULL;
                len=0;
	}
};


구조체의 초기값을 정합니다. front와 rear를 갖고 있고 이 둘은 처음에는 가리키는 노드가 없으니까 NULL이 됩니다. 그리고 길이 역시 0입니다.


우리는 이제 size, isEmpty, enqueue, dequeue 4개의 연산을 구현해보도록 할겁니다. 링크드리스트로 구현하는 형태이기 때문에 그 크기가 가변적입니다. 그렇기 때문에 isFull연산은 없습니다.


1. size

뭐 간단하죠? 그냥 len만 반환하면 되니까요.

int size() {
	return len;
}


2. isEmpty

간단합니다. len이 0이면 그냥 비어있는 겁니다. 바로 이해할겁니다. 허나, front로도 구할 수 있습니다. front는 이제 꺼내야될 노드를 가리키는 녀석입니다. front가 NULL이라면 꺼낼 노드가 없게 되니까 큐가 비어있는 상태로 보면 되겠죠. 


코드는 아래와 같습니다. 주석을 해제하고 같은 결과가 나오는지 확인해보세요.

bool isEmpty() {
	//return front == NULL;
	return len == 0;
}




3. enqueue

큐에 노드를 집어 넣습니다. 일단 큐가 비어있다면 front와 rear는 처음 들어오는 노드를 가리킵니다.


하지만 큐가 비어있지 않는다면 rear을 이용해야합니다. rear의 다음 노드(rear->next)가 새로 들어온 노드가 됩니다. 이후 rear을 새로 들어온 노드로 가리키면 되는 것이죠.


큐에 데이터가 추가되었으니 길이는 하나 증가합니다.


void enqueue(int data) {
	Node *node = (Node*)malloc(sizeof(Node));
	node->data = data;
	node->next = NULL;
	if (isEmpty()) {
		front = rear = node;
	}
	else {
		rear->next = node;
		rear = rear->next;
	}
	len++;
}


4. dequeue

큐에서 하나 꺼내는 연산인데요. 어디서 꺼내느냐. front가 가리키는 노드를 꺼내면 됩니다. 아참, 그전에 큐가 비어있는지 확인하는게 먼저겠죠. 큐에 내용이 있다면 front노드의 데이터를 하나 꺼내고, 임의로 node라는 포인터를 써 front가 가리키는 노드를 같이 가리키게 합니다. 그 이후 front->next로 front를 다음 노드로 가리킵니다. 그리고 메모리를 해제하면 됩니다. 


큐에서 데이터가 삭제됐으니 길이는 줄겠죠?


int dequeue() {

	if (isEmpty()) {
		cout << "Q is empty" << endl;
		return INF;
	}
	Node *delNode = front;
	int ret = delNode->data;
	front = delNode->next;
	free(delNode);
	len--;
	return ret;
}




전체코드


이제 전체코드를 보도록 해요. 메인에서는 큐에 1부터 5까지 삽입하고 다시 10을 삽입합니다. 결과는 어떻게 될까요?

#include <iostream>
#define INF 987654321
using namespace std;
struct Node {
	int data;
	Node *next;
};

struct LinkedQueue {
	Node *front, *rear;
	int len = 0;
	LinkedQueue() {
		front = rear = NULL;
	}
	int size() {
		return len;
	}

	bool isEmpty() {
		return len ==0;
	}

	void enqueue(int data) {
		Node *node = (Node*)malloc(sizeof(Node));
		node->data = data;
		node->next = NULL;
		if (isEmpty()) {
			front = rear = node;
		}
		else {
			rear->next = node;
			rear = rear->next;
		}
		len++;
	}

	int dequeue() {
		
		if (isEmpty()) {
			cout << "Q is empty" << endl;
			return INF;
		}

		Node *delNode = front;
		int ret = delNode->data;
		front = delNode->next;
		free(delNode);
		len--;
		return ret;
	}
};

int main() {
	
	LinkedQueue q;
	for (int i = 1; i <= 5; i++)
		q.enqueue(i);

	q.enqueue(10);

	while (!q.isEmpty()) 
		cout << q.dequeue() << endl;
	
}

원하는 대로 결과가 나왔나요?




여기까지 큐를 링크드리스트로 구현하는 방법을 알아보았습니다. 링크드리스트로 구현한 큐이든, 선형큐이든 항상 무슨 프로그램을 구현할 것인지 고려하여 쓰기 바랍니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

큐(Queue)


스택과 양대산맥인 큐는 스택과 반대의 성격을 지닌 녀석입니다. 스택이 후입선출(Last In First Out)방식이었다면 큐는 선입선출(First In First Out)방식이 되지요. 스택과 큐는 컴퓨터 과학에서 아주 중요한 도구들입니다. 스택의 개념과 사용처는 지난 포스팅에서 했으니, 큐에 대해서만 설명해보도록 하겠습니다.


큐는 개념은 정말 쉽습니다. 첫번째로 들어오는 녀석을 가장 먼저 내보내는 것이 큐입니다. 현실 세계에서도 역시 큐에 개념이 사용됩니다. 버스 기다릴때나 은행 업무를 기다릴때, 가장 먼저 도착한 사람이 버스를 타거나 은행업무를 보게 되지요. 새치기하지마라 진짜





'


위의 그림 처럼 말이죠. 그냥 쭉 줄서있죠? 이런 큐를 선형큐라고 합니다.


그림에서는 보이진 않지만 가장 앞에 있는 원소를 Front, 그리고 가장 뒤에 있는 원소를 Rear라고 합니다.


큐에는 기본적으로 두 가지의 연산이 있습니다. 바로 enqueue와 dequeue입니다.


enqueue 큐에 데이터를 삽입합니다.

dequeue 큐에서 데이터를 꺼내옵니다.


선형큐는 구현하는 게 매우 간단하지만( 단지 배열로 일직선으로 구현) 치명적인 약점이 있는데요.


1) 큐에 데이터가 꽉 찼다면 데이터를 더 추가할 수 없습니다(물론 큐 사이즈를 늘리고 원소를 다시 복사하면 됩니다. 근데 시간이 안습이죠.). 


2)공간의 효율성을 고려해야합니다. 배열로 단순히 구현하면 front가 뒤로 계속 밀려 앞의 공간이 남게 되죠. 그러니까 하나의 원소가 빠져나가면 그 다음부터 모든 녀석들을 앞으로 당겨와야하니까 속도 측면에서 상당히 느립니다. 작은 데이터들이라면 물론 상관없지만, 요즘처럼 사이즈가 크고 많은 데이터를 처리하기에는 무리죠.


사용하기에는 단점이 좀 아프니까 선형큐는 개념 정도만 알고 가겠습니다.




원형큐(Circular Queue)

선형큐의 단점을 보완할 수 있는 큐로는 바로 원형큐가 있습니다. 큐를 직선 형태로 놓는 것 보다 원형으로 생각해서 큐를 구현하는 것이죠. 큐를 원형으로 생각해야되기 때문에 모듈러 연산(나머지 연산)을 해야합니다.


그림으로 설명해보도록 하지요.





우선 큐의 시작점은 어느 점이라도 됩니다. 그냥 단순히 보기 위해서 가장 아래에 있는 점을 시작 점이라고 정의해보겠습니다.


그리고 색이 칠해져있는 부분이 노드가 삽입된 상태입니다.


또한, front가 노드를 가리키고 있지 않고 그 전의 점을 가리키고 있는 이유는 우선 증가 연산 후에 데이터를 꺼내오기 때문입니다.

 


우리는 아래 구조체를 코딩을 통해서 큐를 구현할 겁니다. SIZE는 원형큐의 SIZE를 의미합니다. 그리고 front와 rear가 배열의 마지막 원소로 지정한 것은 배열 인덱스 0부터 시작하려고 하기 때문입니다(선증가 후 원소를 삽입한다는 사실만 지금은 기억하기 바랍니다.). 뭐 front와 rear를 0으로 하건 1로하건 상관없습니다.


struct cirQueue {
	
	int arr[SIZE];
	int front, rear;
	
	cirQueue() {
		front = SIZE-1;
		rear = SIZE-1;
	}
};


cirQueue()는 front와 rear의 초기값을 지정합니다. 이렇게 하고 싶지 않다면  변수를 선언할때 일반적인 구조체를 다루듯이 

cirQueue q;

q.front=q.rear=SIZE-1;

이런식으로 코딩하면 됩니다.


우리는 이제 4가지의 연산을 정의할 건데요. 잘 따라오세요.




1. isEmpty

큐가 비어있냐 아니냐를 질의하는 연산입니다. 처음 큐의 rear와 front는 같은 자리에 위치하게 됩니다. 그러니 rear와 front는 당연히 같은 값이 되지요. 그리고 큐가 삽입되고 꺼내어지고 할 때 역시 큐가 비어있게 된다면 rear와 front는 역시 같은 값이 됩니다. front를 하나 증가하고 난 이후에 데이터를 꺼내오니까요.




코드도 역시 간단하게 구현이 되는 군요.

bool isEmpty() {
		return rear == front;
}


2. isFull

큐에 데이터가 꽉차있는 상태인지 알아보는 연산입니다. rear를 하나 증가시키고 데이터를 넣으려보니까 그 자리에는 front라는 것이 자리잡고 있는 겁니다. 


여기서 중요한 사실은 큐에서 1개는 쓸 수 없다는 사실입니다. rear+1자리에 front가 있는 지 알아보기 위해서지요.





bool isFull() { return ((rear + 1) % SIZE) == front; }


여기서 큐의 SIZE로 나누는 이유는 계속 rear+1이 큐의 SIZE보다 클 수 있다는 겁니다. rear가 SIZE-1의 원소(배열의 마지막 원소)를 가리키고 있고 다음 rear+1은 SIZE가 아니라 0이 되어야합니다. 그래야 원형큐가 되니까요.


아참! 간간히 눈에 보이지 않는 오류가 있는데 수식을 잘 보셔야됩니다. 덧셈보다 모듈러 연산의 우선순위가 더 높기 때문에 (rear +1 % SIZE) == font 를 하게 되면 1%SIZE 부터 계산하게 됩니다. 우리가 원하는 것은 rear를 먼저 1증가시킨 후 SIZE로 나눈 나머지를 구하는 것이니 괄호를 처리해야 합니다. 

( (rear + 1) % SIZE ) == front 로요.




3. enqueue

큐에 원소를 삽입하는 연산입니다. 우선 큐가 꽉차있는 상태인지 아닌지 부터 알아봅니다. 삽입하기 충분한 공간이 있다면 현재 rear에서 1을 증가시킵니다 다음 원소에 추가시키기 위해서요. 그자리에 원소를 삽입하면 됩니다. 


여기서도 역시 나머지 연산이 들어갑니다. 원형큐이기 때문에 SIZE보다 크면 다시 처음부터 원소가 삽입되어야 하기 때문입니다.


void enqueue(int data) {
		if (isFull()) {
			cout << "Q is full" << endl;
			return;
		}
		rear = (rear + 1) % SIZE;
		arr[rear] = data;
}

4. dequeue

큐에 원소를 꺼내어 오는 연산입니다. 맨 처음 들어간 원소는 front+1이 됩니다. 그러니까 우선 큐가 비어있는 지 확인하는 작업이 필요합니다. 비어있는 큐에 원소를 꺼내올 수 없으니까요.


이후 front를 한칸 증가시켜 그 원소를 빼내옵니다. 그러고 나면 front는 빈 원소를 가리키게 되는 것이죠.


역시 모듈러 연산이 쓰이는 군요. 이유는 위와 같습니다.



int dequeue() {
		if (isEmpty()) {
			cout << "Q is empty" <<endl;
			return INF;
		}
		return arr[front = (front + 1) % SIZE];
}



전체 코드

이렇게 큐의 연산을 모두 끝냈습니다. 전체 코드는 아래에 있습니다.

#include <iostream>
#define INF 987654321
#define SIZE 6
using namespace std;

struct cirQueue {
	
	int arr[SIZE];
	int front, rear;
	
	cirQueue() {
		front = SIZE-1;
		rear = SIZE-1;
	}
	
	bool isEmpty() {
		return rear == front;
	}
	
	bool isFull() {
		return ((rear + 1) % SIZE) == front;
	}

	void enqueue(int data) {
		if (isFull()) {
			cout << "Q is full" << endl;
			return;
		}
		rear = (rear + 1) % SIZE;
		arr[rear] = data;
	}

	int dequeue() {
		if (isEmpty()) {
			cout << "Q is empty" << endl;
			return INF;
		}
		return arr[front = (front + 1) % SIZE];
	}
};

int main() {
	
	cirQueue q;
	for (int i = 1; i <= 6; i++)
		q.enqueue(i);
	q.enqueue(10);
	while (!q.isEmpty())
		cout << q.dequeue() << endl;
	cout << q.isEmpty() << endl;
	cout << q.isFull() << endl;
}


결과는?


메인함수에서 코드를 보면 왜 이렇게 결과가 나오는지 이해할 수 있을 겁니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

스택( 연결 리스트 형)


스택은 링크드리스트로도 구현할 수 있습니다.  링크드리스트의 장점을 그대로 물려받게 되면서요.


어찌 구현하면 될까요? 링크드리스트의 형태를 우선 돌이켜보면 어떻게 구현하는 지 감이 올거에요.




우리는 옆으로 나열된 노드를 아래로 나열된 리스트 형태로 한번 떠올려볼까요? 그리고 맨 처음의 Root노드를 Top으로 정의하게 된다면 연결형태의 Stack을 구현할 감이 오게 됩니다.




구현

node와 stack 구조체를 정의할 겁니다. node는 링크드리스트의 노드와 같습니다. 그리고 stack이라는 구조체는 현재 node형의 top이라는 변수를 갖고 있습니다. top은 역시 stack의 top을 말하지요. 


노드는 이렇게 생겨먹었습니다. 익숙하군요.

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



그리고 스택은 지금 이렇게 생겨먹었죠. 근데 top과 len(길이)밖에 없으니 무언가 공허합니다.

struct stack {
	node *top;
        int len;
        stack(){
                top=NULL;
                len=0;
        }
};


초기값 top은 NULL을 가리키고 스택 크기는 0입니다. 우리는 이 stack이라는 구조체에 연산을 삽입할 겁니다.


스택의 연산에는 isEmpty, push, peek, pop 정도가 있었지요? 추가로 스택의 size를 구하는 연산도 넣겠습니다. 이제 링크드리스트 형태로 구현해볼 것입니다.




1.size

스택의 크기가 얼만큼 되는 가에 대해 질의하는 연산입니다. 

int size() {
		return len;
}


그냥 스택의 길이만 반환하면 됩니다.


2. isEmpty

스택에 노드가 없느냐 물어보는 질의형태의 연산입니다. 만약 스택이 비어있다면 스택이 모두 비워져있는 상태이므로 top은 NULL을 가리키게 됩니다. 또는 스택의 길이를 이용하는 겁니다. 길이가 0이면 비워진 스택이 됩니다. 코드는 간단합니다.

int isEmpty() {
		//return top == NULL;
                return len == 0;
}

이 코드를 따로 구현한 이유는 while 조건문을 간단하게 구현하기 위함입니다.


3. push

스택에 노드를 추가하는 연산입니다. 어떤 노드를 추가하고 그 노드의 next는 이미 기존의 top을 가리킵니다. 그리고 top을 새로운 노드로 가리키게 만들어주면 됩니다.


그림으로 더 쉽게 보도록 하시죠. 






새로운 노드가 스택에 추가되려고 기다리고 있습니다. 새로들어오는 노드의 next를 기존의 top이 가리키고 있는 노드를 가리키도록 바꾸어줍니다. 그리고 top을 다시 새로운 노드로 바꾸어주는 것이죠. 그러면 그림은 아래처럼 바뀌게 됩니다.



스택에 하나 넣었으니 스택의 크기를 하나 증가시킵니다(len++).


이것을 코드로 구현한 것이 아래의 코드입니다.

void push(int data) {
		
		node *newNode = (node*)malloc(sizeof(node));
		newNode->data = data;
		newNode->next = top;
		top = newNode;
                len++;
}


4. pop

처음 top에 있었던 데이터를 복사하고 난 후 기존 top의 next를 다시 top이 가리키도록 바꾸면 됩니다.

그리고 나서는 기존의 top 노드를 삭제해야하는 데 그것은 free함수만 사용하면 됩니다.




스택에서 하나 꺼내니까 사이즈가 줄겠죠?(len--);

pop의 연산을 아래의 코드로 구현했습니다. 

int pop() {
		
		int ret;
		if (isEmpty()) return INF;
		node *here = top;
		ret = here->data;
		top = here->next;
		free(here);
                len--;
		return ret;
	}


우선 스택이 비어있는지 확인하는 작업이 필요합니다. 스택이 비어있는데, 거기서 node를 꺼낼 수 없거든요. 새롭게 확인하는 코드를 쓰기 보다 그전에 isEmpty연산 이용하도록 합시다. 스택이 비어있다면 INF라는 무한정 큰값을 반환합니다. 




그리고 어떤 케이스에 따라서는 node 자체를 반환해야할 때가 있습니다. 그럴때는 위의 코드처럼 free함수를 하면 안됩니다. 메모리가 해제된 상태로 node 포인터를 반환하게 되어버리기 때문입니다.


5. peek

peek 연산은 pop에서 별반 다를것이 없습니다. 그냥 노드를 삭제하지 않고 데이터만 뽑아내면 되기 때문이죠.


전체코드

전체코드(C++)는 아래와 같습니다. 

#include <iostream>
#define INF 987654321

using namespace std;

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

struct stack {
	node *top;
        int len;
        stack(){
                top = NULL;
                len = 0;
        }
        int size() {
		return len;
	}
	int isEmpty() {
		//return top == NULL;
                return len==0;
	}

	void push(int data) {
		
		node *newNode = (node*)malloc(sizeof(node));
		newNode->data = data;
		newNode->next = top;
		top = newNode;
                len++;
	}

	int pop() {
		
		int ret;
		if (isEmpty()) return INF;
		node *here = top;
		ret = here->data;
		top = here->next;
		free(here);
                len--;
		return ret;
	}

	int peek() {
		if (isEmpty()) return INF;
		node *here = top;
		return here->data;
	}
};

stack st = { NULL };


int main() {
	
	cout << st.peek() << endl;
	for (int i = 1; i <= 3; i++) 
		st.push(i);
	

	cout << st.pop() << endl;
	cout << st.pop() << endl;
	for (int i = 4; i <= 6; i++)
		st.push(i);

	while (!st.isEmpty())
		cout << st.pop() << endl;

}
main함수에서의 결과를 예측해보세요.  배열로 구현한 스택과 동일한 연산을 합니다.

지금까지 링크드리스트 형태의 스택이 어떻게 동작하고 구현되는 지 알아보았습니다. 딱히 어려운 것은 없었지요?


반응형
블로그 이미지

REAKWON

와나진짜

,