1. 우선순위 큐의 이해

 

▣ 우선순위 큐와 우선순위의 이해

1. 큐의 핵심 연산

· enqueue :  쿠에 데이터를 삽입하는 행위
· dequeue :  큐에 데이터를 꺼내는 행위

 

2. 우선운위 큐의 핵심 연산

· enqueue :  큐에 데이터를 삽입하는 행위
· dequeue : 큐에 데이터를 꺼내는 행위

※ 들어간 순서에 상관없이 우선순위가 높은 데이터가 먼저 나온다

 

 

▣ 우선순위 큐의 구현방법

· 배열을 기반으로 구현하는 방법
· 연결리스트를 기반으로 구현하는 방법
· 힙을 이용하는 방법




두 가지 방법 모두 최악의 경우 새 데이터의 위치를 찾기 위해서 
기존에 저장된 모든 데이터와 비교를 진행해야 한다. 

 

※ 우선순위큐는 우선순위를 근거로 적정 위치를 찾아서 데이터를 저장하는 방식이다

 

▣ 힙의 소개

최대힙

모든 노드에 저장된 값은 자식 노드에 저장된 값보다 크거나 같아야 한다.  
루트 노드에 저장된 값이 가장 커야 한다. 

 

 

최소힙

모든 노드에 저장된 값은 자식 노드에 저장된 값보다 작거나 같아야 한다.  
루트 노드에 저장된 값이 가장 작아야 한다. 

 


2. 힙의 구현과 우선순위 큐의 완성

 

 

▣ 힙에서의 데이터 저장과정

 

1. 자식 노드 데이터의 우선순위 ≤ 부모 노드 데이터의 우선순위 (최소힙?)

 


2. 새 데이터는 우선순위가 낮다는 가정하에 끝!
 에 저장 그리고 부모 노드와 비교를 진행

3. 부모 노드와 비교 및 자리 바꿈 

 

4. 자리를 찾는다

 

▣ 삽입과 삭제의 과정에서 보인 성능의 평가 (우선순위 큐의 시간 복잡도 )

 

∙ 배열 기반 데이터 삽입의 시간 복잡도   O(n) 
∙ 배열 기반 데이터 삭제의 시간 복잡도   O(1)
 

∙ 연결 리스트 기반 데이터 삽입의 시간 복잡도   O(n) 
∙ 연결 리스트 기반 데이터 삭제의 시간 복잡도   O(1)
 

∙ 힙 기반 데이터 삽입의 시간 복잡도   O(log 2 n) 
∙ 힙 기반 데이터 삭제의 시간 복잡도   O(log 2 n)
 

▣ 배열을 기반으로 힙을 구현하는데 필요한 지식들 


“연결 리스트를 기반으로 힙을 구현하면, 새로운 노드를 힙의 마지막 위치
에 추가하는 것이 쉽지 않다.”   

 

 

∙ 왼쪽 자식 노드의 인덱스 값     =>  부모 노드의 인덱스 값 * 2 
∙ 오른쪽 자식 노드의 인덱스 값  =>  부모 노드의 인덱스 값 * 2 + 1 
∙ 부모 노드의 인덱스 값            => 자식 노드의 인덱스 값 / 2 


 힙 구현: 숙지할 내용 


∙ 힙은 완전 이진 트리이다. 
∙ 힙의 구현은 배열을 기반으로 하며 인덱스가 0인 요소는 비워둔다. 
∙ 따라서 힙에 저장된 노드의 개수와 마지막 노드의 고유번호는 일치한다. 
∙ 노드의 고유번호가 노드가 저장되는 배열의 인덱스 값이 된다. 
∙ 우선순위를 나타내는 정수 값이 작을수록 높은 우선순위를 나타낸다고 가정한다 


※ 배열을 기반으로 하는 경우! 힙에 저장된 노드의 개수지막 노드의 고유번호가 일치하기 때문에 
    마지막 노드의 인덱스 값을 쉽게 얻을 수 있다! 


▣ 힙 구현 : 구조체

typedef struct _heapElem 
{
	Priority pr;   // 값이 작을수록 높은 우선순위     
	HData data; 
} HeapElem; 

typedef struct _heap 
{ 
	int numOfData;     
	HeapElem heapArr[HEAP_LEN]; 
} Heap;


 힙 구현: 초기화와 Helper 

void HeapInit(Heap* ph)
{
	ph->numOfData = 0;
}

int HIsEmpty(Heap* ph)
{
	if (ph->numOfData == 0)
		return true;
	else
		return false;
}

int GetParentIndex(int idx)
{
	return idx / 2;
}

int GetLChildIndex(int idx)
{
	return idx * 2;
}

int GetRChildIndex(int idx)
{
	return GetLChildIndex(idx) +1;
}
//두 개의 자식 노드 중 높은 우선순위의 자식 노드 인덱스 값 반환
int GetHipriChildIDX(Heap * ph, int idx)
{
	//자식 노드가 존재하지 않는다면,  
	//numOfData는 마지막 노드의 고유번호이니,
	//자식 노드의 값이 이보다 크면 존재하지 않는 자식 노드이다. 
	if (GetLChildIndex(idx > ph->numOfData))
	{
		return 0;
	}
	
	//자식 노드가 왼쪽 자식 노드 하나만 존재한다면, 
	//자식 노드가 하나 존재하면 이는 왼쪽 자식 노드이다. 완전 이진 트리 이므로
	else if (GetLChildIndex(idx == ph->numOfData))
	{
		return GetLChildIndex(idx);
	}

	//자식 노드가 둘다 존재한다면, 
	else
	{
		//오른쪽 자식 노드의 우선순위가 높다면
		if (ph->heapArr[GetLChildIndex(idx)].pr > ph->heapArr[GetRChildIndex(idx)].pr)
		{
			return GetRChildIndex(idx);   //오른쪽 자식 노드의 인덱스 값 반환
		}
		else
		{
			return GetLChildIndex(idx);   //왼쪽 자식 노드의 인덱스 값 반환
		}
	}
}



▣ 힙 구현 : HInsert

 

1. 힙의 마지막 자리에 새로운 노드를 배정 (실제로 넣지는 않고 가정만 한다.)
2. 그의 부모노드와 우선순위를 비교하여 위치 변경
3. 삭제의 과정이 위에서 밑으로 였다면, 삽입의 과정은 밑에서 위로

void HInsert(Heap * ph, HData data, Priority pr)
{
	int idx = ph->numOfData + 1;		//새 노드가 저장될 인덱스 값을 idx 저장
	HeapElem nelem = { pr, data };		//새 노드의 생성 및 초기화

	while (idx != 1)					//새노드가 루트노드까지 while
	{
		//새노드의 우선순위가 높다면
		if (pr < (ph->heapArr[GetParentIndex(idx)].pr))       
		{
			//부모 노드를 한레벨 내림, (실제로 내림)
			ph->heapArr[idx] = ph->heapArr[GetParentIndex(idx)];

			//새 노드를 한 레벨 올림, (실제로는 인덱스 값만 갱신)
			idx = GetParentIndex(idx);
		}
		else
			break;
	}
	ph->heapArr[idx] = nelem; //새 노드를 배열에 저장
	ph->numOfData += 1;       //총 데이터수 +1
}


▣ 힙 구현 : HDelete

1. 가장 마지막 노드를 추출한다. (그리고 마지막노드를 루트노드로 이동시키고 재정렬)
2. 인덱스 1번에 위치한 노드가 삭제되기 때문에 그 밑의 자식노드들과 마지막 노드의 우선순위를 비교한다.
3. 자식노드의 우선순위를 비교할 때, GetHiPriChildIDX() 함수를 사용한다.
4. 현재의 노드가 마지막 노드보다 우선순위가 높다면 한칸 위로 이동한다. (1번이 빠졌기 때문에 공간을 메운다.)
5. 마지막 노드보다 우선순위가 낮거나, 자식노드가 더이상 없을 때, 마지막 노드의 자리가 결정된다.

HData HDelete(Heap * ph)  
{
	HData retData = (ph->heapArr[1]).data;           //반환을 위해서 삭제할 데이터 저장
	HeapElem lastElem = ph->heapArr[ph->numOfData];   //힙의 <마지막 노드> 저장

	// 아래의 변수 parentIdx에는 마지막 노드가 저장될 위치정보가 담긴다.
	int parentIdx = 1;  //루트 노드가 위치해야할 인덱스 값 저장
	int childIdx;

	//루트 노드의 우선수위가 높은 자식 노드를 시작으로 반복문 시작
	while (childIdx = GetHipriChildIDX(ph, parentIdx))
	{
		if (lastElem.pr <= ph->heapArr[childIdx].pr)        //마지막 노드와 우선순위 비교
			break;                                          //마지막 노드의 우선순위가 높으면 반복문 탈출

		//마지막 노드보다 우선순위 높으니, 비교대상 노드의 위치를 한 레벨 올림
		ph->heapArr[parentIdx] = ph->heapArr[childIdx];
		parentIdx = childIdx;                               //마지막 노드가 저장될 위치정보를 한 레벨 내림
	}
	//반복문 탈출하면 parentIdx에는 마지막 노드의 위치정보가 저장됨
	ph->heapArr[parentIdx] = lastElem;           //마지막 노드 최종 저장
	ph->numOfData -= 1;
	return retData;
}

 



▣ 힙 구현 :  main

int main()
{
	Heap heap;
	HeapInit(&heap);

	HInsert(&heap, 'A', 1);
	HInsert(&heap, 'B', 2);
	HInsert(&heap, 'C', 3);
	cout << HDelete(&heap) << "\n";

	HInsert(&heap, 'A', 1);
	HInsert(&heap, 'B', 2);
	HInsert(&heap, 'C', 3);
	cout << HDelete(&heap) << "\n";

	while(!HIsEmpty(&heap))
		cout << HDelete(&heap) << "\n";

	return 0;
}






▣ 힙 구현 : 힙의 변경

데이터를 저장할 때 우선순위 정보를 별도로 전달하는 것은 적합하지 않은 경우가 많다. 
일반적으로  데이터의 우선순위는 데이터를 근거로 판단이 이뤄지기 때문이다.  

typedef int PriorityComp(HData d1, HData d2);

typedef struct _heap 
{ 
	PriorityComp * Comp;
	int numOfData;     
	HData heapArr[HEAP_LEN]; 
} Heap;
void HeapInit(Heap* ph, PriorityComp pc)
{
	ph->numOfData = 0;
	ph->Comp = pc;
}

 

▣ 힙 구현: PriorityComp

int DataPriorrity(char ch1, char ch2)
{
	return ch2 - ch1;
}

∙ 첫 번째 인자의 우선숚위가 높다면, 0보다 큰 값 반환  
 
∙ 두 번째 인자의 우선숚위가 높다면, 0보다 작은 값 반
 
∙ 첫 번째, 두 번째 인자의 우선숚위가 동일하다면, 0이 반

 

void HInsert(Heap * ph, HData data, Priority pr); //이 함수를

void HInsert(Heap * ph, HData data) //로 변경

 

※ PriorityComp형 함수가 등록되면 HInsert 함수는 등록된 함수를 활용하여 우선순위를 비교 판단한다


▣ 힙 구현 :  Helper 함수의 변경 

 

int GetHipriChildIDX(Heap * ph, int idx)
{
	if (GetLChildIndex(idx > ph->numOfData))
	{
		return 0;
	}
	else if (GetLChildIndex(idx == ph->numOfData))
	{
		return GetLChildIndex(idx);
	}
	else
	{
		if (ph->Comp( ph->heapArr[GetLChildIndex(idx)], 
						ph->heapArr[GetRChildIndex(idx)]) <0)
		{
			return GetRChildIndex(idx);   
		}
		else
		{
			return GetLChildIndex(idx);  
		}
	}
}

 

comp에 등록된 함수의 호출결과를  통해서 우선순위를 판단한다

 

▣ 힙 구현 :  HInsert의 변경 

 

void HInsert(Heap * ph, HData data)
{
	int idx = ph->numOfData + 1;		//새 노드가 저장될 인덱스 값을 idx 저장

	while (idx != 1)				
	{
		//새노드의 우선순위가 높다면
		if (ph->Comp (data , ph->heapArr[GetParentIndex(idx)]) >0)
		{
			ph->heapArr[idx] = ph->heapArr[GetParentIndex(idx)];

			idx = GetParentIndex(idx);
		}
		else
			break;
	}
	ph->heapArr[idx] = data; //새 노드를 배열에 저장
	ph->numOfData += 1;       //총 데이터수 +1
}


comp에 등록된 함수의 호출결과를   통해서 우선순위를 판단한다

 

▣ 힙 구현 :  HDelete의 변경 

HData HDelete(Heap * ph)  
{
	HData retData = ph->heapArr[1];           //반환을 위해서 삭제할 데이터 저장
	HData lastElem = ph->heapArr[ph->numOfData];   //힙의 <마지막 노드> 저장

	int parentIdx = 1;  //루트 노드가 위치해야할 인덱스 값 저장
	int childIdx;

	while (childIdx = GetHipriChildIDX(ph, parentIdx))
	{
		//if (lastElem.pr <= ph->heapArr[childIdx].pr)        
		//	break;                                          

		if (ph->Comp(lastElem , ph->heapArr[childIdx]) >=0)
			break;


		ph->heapArr[parentIdx] = ph->heapArr[childIdx];
		parentIdx = childIdx;                               //마지막 노드가 저장될 위치정보를 한 레벨 내림
	}
	//반복문 탈출하면 parentIdx에는 마지막 노드의 위치정보가 저장됨
	ph->heapArr[parentIdx] = lastElem;           //마지막 노드 최종 저장
	ph->numOfData -= 1;                          //총 데이터 수 -1
	return retData;
}


comp에 등록된 함수의 호출결과를  통해서 우선순위를 판단한다

 

▣ 힙 구현 :  main의 변경 

int main()
{
	Heap heap;
	HeapInit(&heap , DataPriorrity);

	HInsert(&heap, 'A');
	HInsert(&heap, 'B');
	HInsert(&heap, 'C');
	cout << HDelete(&heap) << "\n";

	HInsert(&heap, 'A');
	HInsert(&heap, 'B');
	HInsert(&heap, 'C');
	cout << HDelete(&heap) << "\n";

	while (!HIsEmpty(&heap))
	{
		cout << HDelete(&heap) << "\n";
	}

	return 0;
}

'프로그래밍 > 자료구조' 카테고리의 다른 글

8. 트리(Tree)  (0) 2019.06.11
7. 큐 (Queue)  (0) 2019.06.10
6. 스택(Stack)  (0) 2019.06.07
5. 원형 연결 리스트 3 (원형 + 양방향)  (0) 2019.06.06
4. 연결 리스트 2 (연결리스트 + 더미노드 + 정렬삽입)  (0) 2019.06.05

1. 트리의 개요

 

트리는 계층적 관계(Hierarchical Relationship)를 표현하는 자료구조이다

 

 

 

 

 ※ 트리는 단순한 데이터의 저장을 넘어서 데이터의 표현을 위한 도구이다

 

▣ 트리 관련 용어

 

트리

 

∙ 노드
node 트리의 구성요소에 해당하는 A, B, C, D, E, F와 같은 요소

∙ 간선 (edge)
노드와 노드를 연결하는 연결선

∙ 루트 노드 (root node )
트리 구조에서 최상위에 존재하는 A와 같은 노드

∙ 단말 노드 (terminal node)
 아래로 또 다른 노드가 연결되어 있지 않은 E, F, C, D와 같은 노드

∙ 내부 노드 (internal node)
 단말 노드를 제외한 모든 노드로 A, B와 같은 노드

 

 

▣ 트리의 노드간 관계

 

 

∙  노드 A는 노드 B, C, D의 부모 노드(parent node)이다.

∙ 노드 B, C, D는 노드 A의 자식 노드(child node)이다.

∙ 노드 B, C, D는 부모 노드가 같으므로, 서로가 서로에게 형제 노드(sibling node)이다.




▣ 서브 트리의 이해

 

루트의 서브트리

 

하나의 트리를 구성하는 왼쪽과 오른쪽의 작은 트리를 가리켜 서브 트리라 한다

 

왼쪽 서브의 서브트리

 

서브 트리 역시 또 다른 서브 트리를 갖는다

 

서브 트리 역시 서브 트리로 이뤄져 있으며, 
그 서브 트리 역시 또 다른 서브 트리로 이뤄져 있다. 

이렇듯 트리는 그 구조가 재귀적이다!

 


▣ 이진 트리의 이해

 

▶ 이진 트리의 조건
∙ 루트 노드를 중심으로 두 개의 서브 트리로 나뉘어진다.
∙ 나뉘어진 두 서브 트리도 모두 이진 트리이어야 한다.

이상적인 이진트리



트리 그리고 이진 트리는 그 구조가 재귀적이다.
따라서 트리와 관련된 연산은 재귀적으로 사고하고 재귀적으로 구현할 필요가 있다

 

▣ 이진 트리와 공집합 노드

 

공집합1

 

공집합2

 

 

공집합(empty set)도 이진 트리에서는 노드로 간주한다!

하나의 노드에 두 개의 노드가 달리는 형태의 트리는 모두 이진 트리이다

 

▣ 레벨과 높이, 그리고 포화, 완전 이진 트리

 

트리의 높이 레벨

트리의 높이와 레벨의 최대 값은 같다

 

 

포화 이진 트리  

모든 레벨에 노드가 꽉 찬 
포화 이진 트리

 

 

 

빈 틈 없이 차곡차곡 채워진
완전 이진 트리

 

 

완전 이진 트리는 위에서 아래로 왼쪽에서 오른쪽으로 채워진 트리를 의미한다. 

따라서 포화 이진 트리는 동시에 완전 이진 트리이지만 그 역은 성립하지 않는다.


2. 이진 트리의 구현

 

 

▣  이진 트리 구현의 두 가지 방법

 

배열 기반

 

∙ 노드에 번호를 부여하고 그 번호에 해당하는 값을 배열의 인덱스 값으로 활용한다.  
∙ 편의상 배열의 첫 번째 요소는 사용하지 않는다.

 

연결리스트 기반

 

∙ 연결 리스트 기반에서는 트리의 구조와 리 스트의 연결 구조가 일치한다. 
∙ 따라서 구현과 관련된 직관적인 이해가 더 좋은 편이다. 

 

▣ ADT

 

 

이진 트리의 모든 노드는 직/간접적으로 연결되어 있다.  
따라서 루트 노드의 주소 값만 기억하면, 이진 트리 전체 를 가리키는 것과 다름이 없다.

 

논리적으로도 하나의 노드는 그 자체로 이진 트리이다. 
따라서 노드를 표현한 구조체는 실 제로 이진 트리를 표현한 구조체가 된다.

 

//노드 생성
BTreeNode* MakeBTreeNode()
{
	BTreeNode* ne = new BTreeNode();
	ne->left = NULL;
	ne->right = NULL;
	return ne;
}

BTData GetData(BTreeNode* bt)
{
	bt->data;
}

//노드에 데이터를 저장
void SetData(BTreeNode* bt, BTData data)
{
	bt->data = data;
}

//왼쪽 서브트리의 주소값 반환
BTreeNode* GetLeftSubTree(BTreeNode* bt)
{
	return bt->left;
}

//오른쪽 서브트리의 주소값 반환
BTreeNode* GetRightSubTree(BTreeNode* bt)
{
	return bt->right;
}

//main 왼쫀 서브트리로 sub 연결
void MakeLeftSubTree(BTreeNode* main, BTreeNode* sub)
{
	if (main->left != NULL)
		delete main->left;

	main->left = sub;
}

//main 오른쪽 서브트리로 sub 연결
void MakeRightSubTree(BTreeNode* main, BTreeNode* sub)
{
	if (main->right != NULL)
		delete main->right;

	main->right = sub;
}

 

int main()
{
	BTreeNode * ndA = MakeBTreeNode();       
	BTreeNode * ndB = MakeBTreeNode();     
	BTreeNode * ndC = MakeBTreeNode();    


	MakeLeftSubTree(ndA, ndB); 

	MakeRightSubTree(ndA, ndC); 

	return 0;
}

 

1차원 이진트리 채워진 결과

 

int main()
{
	BTreeNode * bt1 = MakeBTreeNode();       
	BTreeNode * bt2 = MakeBTreeNode();       
	BTreeNode * bt3 = MakeBTreeNode();       
	BTreeNode * bt4 = MakeBTreeNode();


	SetData(bt1, 1);
	SetData(bt2, 2);
	SetData(bt3, 3);
	SetData(bt4, 4);


	MakeLeftSubTree(bt1, bt2); 
	MakeRightSubTree(bt1, bt3); 
	MakeLeftSubTree(bt2, bt4);

	return 0;
}

 

main 함수에서 생성하는 트리

 


3. 이진 트리의 순회(Traversal)

 

 

기준은 루트 노드를 언제 방문하느냐에 있다

루트 노드를 방문하는 시점에 따라서 중위, 후위, 전위 순회로 나뉘어 진다.

 

 

중위 순회

 

후위 순회

 

전위 순회

 

 

▣ 순회의 재귀적 표현

 

이진 트리의 중위 순회

 

탈출 조건이 명시되지 않은 불완전한 구현

 

▣ 순회의 재귀적 표현 완성

void InorderTraverse(BTreeNode* bt)
{
	if (bt == NULL)		//NULL 이면 재귀 탈출
    return;

	InorderTraverse(bt->left);
	cout <<  bt->data << "\n";
	InorderTraverse(bt->right);
}

 

 

 

노드가 단말 노드인 경우, 단말 노드의 자식 노드는 NULL이다!

 

int main()
{
	BTreeNode * bt1 = MakeBTreeNode();       
	BTreeNode * bt2 = MakeBTreeNode();       
	BTreeNode * bt3 = MakeBTreeNode();       
	BTreeNode * bt4 = MakeBTreeNode();


	SetData(bt1, 1);
	SetData(bt2, 2);
	SetData(bt3, 3);
	SetData(bt4, 4);


	MakeLeftSubTree(bt1, bt2); 
	MakeRightSubTree(bt1, bt3); 
	MakeLeftSubTree(bt2, bt4);


	InorderTraverse(bt1); //재귀 적트로 트리를 순회하면서, 값을 출력

	return 0;
}

 

 

▣ 준위 순회와 후위 순회

중위순회

void InorderTraverse(BTreeNode* bt)
{
	if (bt == NULL)		return;

	InorderTraverse(bt->left);
	cout <<  bt->data << "\n"; // 중위순회
	InorderTraverse(bt->right);
}

 

 

전위 순회

void InorderTraverse(BTreeNode* bt)
{
	if (bt == NULL)		return;

	cout << bt->data << "\n"; //전위순회
	InorderTraverse(bt->left);
	InorderTraverse(bt->right);
}


 

 

후위 순회

void InorderTraverse(BTreeNode* bt)
{
	if (bt == NULL)		return;

	InorderTraverse(bt->left);
	InorderTraverse(bt->right);
	cout << bt->data << "\n";   //후위 순회
}

 

 

▣ 노드의 방문 이유! 자유롭게 구성하기

 

함수포인터를 이용하여, 추가 함수 기능 구현

 

//함수 포인터 형 VisitFuncPtr의 정의
typedef void VisitFuncPtr(BTData data);

//Funcptr이 가리키는 함수를 통해서 방문을 진행
void InorderTraverse(BTreeNode* bt, VisitFuncPtr Funcptr)
{
	if (bt == NULL)		return;

	InorderTraverse(bt->left, Funcptr);
	Funcptr(bt->data);
	InorderTraverse(bt->right, Funcptr);
}

//VisitFuncPtr형을 기준으로 정의된 함수
void ShowData(int data)
{
	cout << data << " \n";
}


int main()
{
	BTreeNode * bt1 = MakeBTreeNode();       
	BTreeNode * bt2 = MakeBTreeNode();       
	BTreeNode * bt3 = MakeBTreeNode();       
	BTreeNode * bt4 = MakeBTreeNode();

	SetData(bt1, 1);
	SetData(bt2, 2);
	SetData(bt3, 3);
	SetData(bt4, 4);

	MakeLeftSubTree(bt1, bt2); 
	MakeRightSubTree(bt1, bt3); 
	MakeLeftSubTree(bt2, bt4);

	//InorderTraverse(bt1);
	InorderTraverse(bt1,ShowData);

	return 0;
}

1. 큐의 이해와 ADT 정의

 

큐는 ‘FIFO(First-in, First-out) 구조’의 자료구조이다. 
때문에 먼저 들어간 것이 먼저 나오는, 일종의 줄서기에 비유할 수 있는 자료구조이다

 

▣ dequeue '큐'의 기본 연산

∙ enqueue : 큐에 데이터를 넣는 연산 
∙ dequeue : 큐에서 데이터를 꺼내는 연산

※ 큐는 운영체제 관점에서 보면 프로세스나 쓰레드의 관리에 활용이 되는 자료구조이다. 
이렇듯 운영 체제의 구현에도 자료구조가 사용이 된다. 
따라서 운영체제의 이해를 위해서는 자료구조에 대한 이해가 선행되어야 한다.

 

 

큐의 ADT 정의

 

ADT를 대상으로 배열 기반의 큐 또는 연결 리스트 기반의 큐를 구현할 수 있다


2. 큐의 배열 기반 구현

 

F :큐의 머리를 의미

R : 큐의 꼬리를 의미

 

enqueue 과정

 

큐의 꼬리를 의미하는 R을 한칸 이동시키고 새 데이터를 저장한다

 

 



dequeue 과정

 

큐의 머리를 의미하는 F가 가리키는 데이터를 반환하고 F를 한 칸 이동시킨다

 

  가장 기본적인 배열 기반 큐의 문제점

 

dequque한 배열

 

enqueue 한뒤, 끝까지 이동한 배열, 앞 원소는 비어있다

 

분명 배열은 비어있다. 
하지만 R을 오른쪽으로 한칸 이동시킬 수 없어서 더 이상 데이터를 추가할 수 없다!  

데이터를 더 추가하기 위해서는 R을 인덱스가 0인 위치로 이동시켜야 한다.
그리고 이러한 방식으로 문제를 해결한 것이 바로 ‘원형 큐’이다!


▣ 원형 큐

 

 

배열 큐 -> 원형 큐

배열의 머리와 끝을 연결한 구조를  원의 형태로 바라본다.

 

 

단순 배열단순 배열 큐와 마찬가지로 R이 이동한 다음에 데이터 저장한다

 

단순 배열 큐와 마찬가지로 F가 가리키는 데이터 반환 후 F 이동!

 

▣ 원형 큐의 단순한 연산의 문제점

 

큐가 Full 인 경우

 

 

큐가 없는 경우

 

F와 R의 위치만으로는 큐가 Full 이거나 빈경우를 구분 할 수가 없다!



▣ 원형 큐의 단순한 연산의 문제점 해결

 

 

해결 방법은 저장 공간 하나를 잃고 문제점을 해결한다

 

데이터가 하나 존재하는 경우 F와 R이 같은 위치를 가리켰는데, 
초기화 직후에 이 상태가 되게 한다

 

 


하나 비운 상태에선 FULL로 인정한다! 
그러면 Empty와 Full의 구분이 가능하다!

 


▣ 원형 큐 구현

 

 

//구조체 정의
#define QUE_LEN 100
typedef int  Data;

typedef struct _cQueue
{
	int _F;
	int _R;
	Data qArr[QUE_LEN]
}CQueue;

 

//이동할 인덱스를 알려줄 헬퍼 함수
int NextIndex(int pos)
{
	if (pos QUE_LEN - 1) 
		return 0;
	else 
		return pos + 1;
}

 

빈상 태와 풀 상태의 위치

 

void QueueInit(Queue * pq)
{
	pq->_F = 0;
	pq->_R = 0;
}


int QIsEmpty(Queue * pq)
{
	if (pq->_F == pq->_R)
		return true;
	else
		return false;
}

//rear을 이동시키고(NextPosIdx 함수의 호출을 통해), 그 위치에 데이터 저장
void Enqueue(Queue * pq, Data data)
{
	if (NextIndex(pq->_R) == pq->_F) // R 이 F와 같으면 NULL
	{
		cout << "Error" << endl;
	}
	//아니라면 R+1로 더해서 이동하고, 배열에 값을 넣는다.
	pq->_R = NextIndex(pq->_R); 
	pq->qArr[pq->_R] = data;
}

//front를 이동시키고(NextPosIdx 함수의 호출을 통해), 그 위치의 데이터 반환
Data Dequeue(Queue * pq)
{
	if (QIsEmpty(pq))
	{
		cout << "Error" << endl;
	}

	pq->_F = NextIndex(pq->_F);

	return pq->qArr[pq->_F];
}

Data QPeek(Queue * pq)
{
	if (QIsEmpty(pq))
	{
		cout << "Error" << endl;
	}
	pq->_F = NextIndex(pq->_F);

	return pq->qArr[NextIndex(pq->_F)];
}


int main()
{
	Queue q;
	QueueInit(&q);

	Enqueue(&q, 1);
	Enqueue(&q, 2);
	Enqueue(&q, 3);
	Enqueue(&q, 4);


	while (!QIsEmpty(&q))
		cout << Dequeue(&q) << endl;

	return 0;
}

 

 


3. 큐의 연결 리스트 기반 구현

 

 

스택과 의 유일한 차이점이 앞에서 꺼내느냐 뒤에서 꺼내느냐에 있으니, 
이전에 구현해 놓은 스택을 대상으로 꺼내는 방법만 조금 변경하 면 큐가 될 것 같다

 

▣ 연결 리스트 기반 큐의 구현: Init

//구조체 정의
typedef struct _node
{
	Data data;
	struct _node * next;
}Node;

typedef struct _cQueue
{
	Node* _F;
	Node* _R;

} LQueue;

typedef LQueue Queue;

 

int QIsEmpty(Queue * pq)
{
	if (pq->_F == NULL)
		return true;
	else
		return false;
}

void QueueInit(Queue * pq)
{
	pq->_F = 0;
	pq->_R = 0;
}

 

 

  연결 리스트 기반 큐의 구현: enqueue

enqueue 과정

 

void Enqueue(Queue * pq, Data data)
{
	Node* newNode = new Node;
	
	newNode->next = NULL;
	newNode->data = data;

	if (QIsEmpty(pq))
	{
		pq->_F = newNode;
		pq->_R = newNode;
	}
	else 
	{
		pq->_R->next = newNode;
		pq->_R = newNode;

	}
}

 

 

  연결 리스트 기반 큐의 구현: dequeue

 

F가 다음 노드를 가리키게 하고, F가 이전에 가리키던 노드를 소멸시킨 결과

 

 

또 F가 다음 노드를 가리키게 하고, F가 이전에 가리키던 노드를 소멸시킨 결과

 

R은 그냥 내버려 둔다
그래야 dequeue의 흐름을 하나로 유지할 수 있다
그리고 R은 그냥 내버려 두어도 된다

 

 

dequeue 사용시 F를 이동한다

 

Data Dequeue(Queue * pq)
{
	Node* delNode;  //삭제할 노드를 저장할 노드
	Data rData;     //반환할 데이터

	
	if (QIsEmpty(pq))
	{
		cout << "Dequeue Error"<<endl;
	}
	//삭제하기 전에 데이터를 저장한다
	delNode = pq->_F;
	rData = pq->_F->data;

	
	pq->_F = pq->_F->next;
	delete delNode;

	return rData;
}

 

Data QPeek(Queue * pq)
{
	if (QIsEmpty(pq))
	{
		cout << "Error" << endl;
	}

	return pq->_F->data;
}


int main()
{
	Queue q;
	QueueInit(&q);

	Enqueue(&q, 1);
	Enqueue(&q, 2);
	Enqueue(&q, 3);
	Enqueue(&q, 4);


	while (!QIsEmpty(&q))
		cout << Dequeue(&q) << endl;

	return 0;
}

 

 

 

+ Recent posts