반응형
728x90
반응형

오늘은 덱? 디큐? 라고 불리우는 자료구조이다.

Dequeue 는 매우 혼종이다. 큐라고는 불리우는데 큐의 특징 이외에 스택의 특징을 가지고 있기 때문이다. 선입선출 / 후입선출 등의 개념을 모두 가지고 있다. 먼저 들어온 놈이 먼저 나갈 수 있고, 나중에 들어온 놈이 먼저 나갈 수도 있다는 것이다.

도데체 어떤 구조이길래 위와 같은 특징을 가지고 있을까? 다음과 같다.

위와 같은 구조이다. 뒤에서도 (tail의 사이드) 추가 및 삭제가 가능해야 하기 때문에 꼭 양방향 링크로 구성이 되어야 한다!!!!! 

그런데 뭐, 이제까지 다 다뤄봤던 구조들이기 때문에 예를 들어, 양방향 링크라든지 head와 tail이 함께 있는 구조라든지 어렵지 않은 부분들이므로 간단하게 설명한다.

초기화와 Dequeue의 비워짐 / 꽉 참의 확인

먼저, 초기화는 Head와 Tail 을 NULL로 초기화 하는 것으로 시작한다.

그리고 이러한 초기화에 따라서, Head와 Tail이 NULL 인 경우가 비워진 경우이고, 해당 Dequeue의 꽉 찬 상태는 확인할 필요가 없다. 왜냐하면 연결 기반의 큐이기 때문에 추가하는 수의 제한이 없다고 보면 되기 때문이다.

노드 추가

위 그림으로 설명이 충분하다고 생각한다. 노란색이 추가되는 노드이고, head와 tail 은 각각 옮겨가는 것을 표현한 것이다.

노드 삭제

노드 그림을 그리지 않고 코드로만 설명해도 되겠다고 판단하여 그림을 그리지 않는다. 코드의 주석만으로 충분한 설명이 되었다고 본다.

탐색

탐색은 다른 리스트들과 마찬가지로 링크들을 타면서 순회하는 것이기 때문에 딱히 구현하지 않았다.

메모리 관리

배열 기반의 원형 큐를 통해 메모리 관리를 한다.

 

소스 코드 및 실행 결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#include <stdio.h>
#include <stdlib.h>
 
#define QUEUE_LEN 11
#define TRUE 1
#define FALSE 0
 
struct node
{
    int data;
    struct node* prev;
    struct node* next;
};
 
struct list_dequeue
{
    struct node* head;
    struct node* tail;
    int count;
};
typedef struct list_dequeue dequeue;
 
struct array_queue // 배열 기반 원형 큐 구현
{
    int front;
    int rear;
    int count;
    struct node* queue_array[QUEUE_LEN];
};
typedef struct array_queue queue;
 
void QueueInit(queue* q);
int QisEmpty(queue* q);
struct node* Dequeue(queue* q);
void Enqueue(queue* q, struct node* data);
void ShowQueue(queue* q);
void RemoveAllNodeAuto(queue* q);
struct node* CreateNodeAuto(dequeue* dq, queue* q);
 
 
void DequeueInit(dequeue* dq)
{
    // head와 tail을 NULL로 초기화
    dq->head = NULL;
    dq->tail = NULL;
    // count 값을 0으로 초기화
    dq->count = 0;
    return;
}
 
int DQIsEmpty(dequeue* dq)
{
    if (dq->head == NULL && dq->tail == NULL// head와 tail이 동일하게 NULL을 가리킨다면 true 반환
        return TRUE;
    else // 아니면 false 반환
        return FALSE;
}
 
void DQAddFirst(dequeue* dq, queue* q, int data)
{
    // 새 노드 생성 ->  prev와 next는 모두 NULL
    struct node* newnode = CreateNodeAuto(dq, q);
    newnode->data = data;
 
    if (DQIsEmpty(dq)) // 비어있다면 head와 tail에게 새 노드 주소를 던짐
    {
        dq->head = dq->tail = newnode;
    }
    else // 안 비어있다면 next는 원래 head가 가리키고 있던 주소, head만 변경.
    {
        newnode->next = dq->head;
        dq->head->prev = newnode;
        dq->head = newnode;
    }
    // count 증가
    dq->count++;
 
    return;
}
void DQAddLast(dequeue* dq, queue* q, int data)
{
    // 새 노드 생성 ->  prev와 next는 모두 NULL
    struct node* newnode = CreateNodeAuto(dq, q);
    newnode->data = data;
 
    if (DQIsEmpty(dq)) // 비어있다면 head와 tail에게 새 노드 주소를 던짐
    {
        dq->head = dq->tail = newnode;
    }
    else // 안 비어있다면 prev는 원래 tail이 가리키고 있던 주소, tail만 변경.
    {
        newnode->prev = dq->tail;
        dq->tail->next = newnode;
        dq->tail = newnode;
    }
    // count 증가
    dq->count++;
    return;
}
 
int DQRemoveFirst(dequeue* dq)
{
    struct node* target = NULL;
    int data = 0;
    if (DQIsEmpty(dq)) // 비어있다면 x
    {
        printf("dequeue가 이미 비어져 있습니다.\n");
        return -1;
    }
    // 비어있지 않으면 현재 head를 임시 변수에 저장
    target = dq->head;
    // head를 현재 head의 next로 옮긴다
    dq->head = dq->head->next;
    // 임시 변수에서 값 추출
    data = target->data;
    // 카운트를 감소한다.
    dq->count--;
    // 변경된 헤드가 NULL인지 확인한다.
    if (dq->head == NULL)
    {
        dq->tail = NULL;
    }
    else
    {
        dq->head->prev = NULL;
    }
    // 메모리 해체는 원형 큐에서.
    return data;
}
int DQRemoveLast(dequeue* dq)
{
    struct node* target = NULL;
    int data = 0;
    if (DQIsEmpty(dq)) // 비어있다면 x
    {
        printf("dequeue가 이미 비어져 있습니다.\n");
        return -1;
    }
    // 비어있지 않으면 현재 tail을 임시 변수에 저장
    target = dq->tail;
    // tail을 현재 tail의 prev로 옮긴다
    dq->tail = dq->tail->prev;
    // 임시 변수에서 값 추출
    data = target->data;
    // 카운트를 감소한다.
    dq->count--;
    // 변경된 헤드가 NULL인지 확인한다.
    if (dq->tail == NULL)
    {
        dq->head = NULL;
    }
    else
    {
        dq->tail->next = NULL;
    }
    // 메모리 해체는 원형 큐에서.
    return data;
}
 
int DQGetFirst(dequeue* dq)
{
    // 비어있다면 x
    if (DQIsEmpty(dq))
    {
        printf("dequeue가 이미 비어져 있습니다.\n");
        return -1;
    }
    // 비어있지 않으면 현재 head의 값을 리턴
    return dq->head->data;
}
int DQGetLast(dequeue* dq)
{
    // 비어있다면 x
    if (DQIsEmpty(dq))
    {
        printf("dequeue가 이미 비어져 있습니다.\n");
        return -1;
    }
    // 비어있지 않으면 현재 tail의 값을 리턴
    return dq->tail->data;
}
 
void ShowDequeue(dequeue* dq)
{
    struct node* dq_index = NULL;
    for (dq_index = dq->head; dq_index != NULL; dq_index = dq_index->next)
        printf("[%p/%d] - ", dq_index, dq_index->data);
    fputc('\n', stdout);
    return;
}
 
 
 
int main(void)
{
    queue q;
    dequeue dq;
 
    QueueInit(&q);
    DequeueInit(&dq);
 
    DQAddFirst(&dq, &q, 2); ShowDequeue(&dq);  // ShowQueue(&q); 
    DQAddFirst(&dq, &q, 1); ShowDequeue(&dq);  // ShowQueue(&q);
    DQAddFirst(&dq, &q, 3); ShowDequeue(&dq);  // ShowQueue(&q);
    DQAddLast(&dq, &q, 4); ShowDequeue(&dq);  //ShowQueue(&q);
    DQAddLast(&dq, &q, 5); ShowDequeue(&dq);  //ShowQueue(&q);
    DQAddLast(&dq, &q, 6); ShowDequeue(&dq);  //ShowQueue(&q);
 
    printf("----- 1\n");
    while (!DQIsEmpty(&dq))
        printf("%d ", DQRemoveFirst(&dq));
    fputc('\n', stdout);
    RemoveAllNodeAuto(&q);
 
    DQAddFirst(&dq, &q, 3);
    DQAddFirst(&dq, &q, 2);
    DQAddFirst(&dq, &q, 1);
 
    DQAddLast(&dq, &q, 4);
    DQAddLast(&dq, &q, 5);
    DQAddLast(&dq, &q, 6);
 
    printf("----- 2\n");
    while (!DQIsEmpty(&dq))
        printf("%d ", DQRemoveLast(&dq));
    fputc('\n', stdout);
 
    RemoveAllNodeAuto(&q);
 
    // 6 5 3 1 2 4 7 8 
    DQAddFirst(&dq, &q, 1);
    DQAddLast(&dq, &q, 2);
    DQAddFirst(&dq, &q, 3);
    DQAddLast(&dq, &q, 4);
    DQAddFirst(&dq, &q, 5);
    DQAddFirst(&dq, &q, 6);
    DQAddLast(&dq, &q, 7);
    DQAddLast(&dq, &q, 8);
 
    printf("----- 3\n");
    while (!DQIsEmpty(&dq))
        printf("%d ", DQRemoveFirst(&dq));
    fputc('\n', stdout);
 
    RemoveAllNodeAuto(&q);
 
 
 
    return 0;
}
 
///// MEMORY QUEUE
void QueueInit(queue* q)
{
    int i = 0;
 
    q->front = q->rear = 0;
 
    for (i = 0; i < QUEUE_LEN; i++)
        q->queue_array[i] = NULL// 큐 출력을 위한 초기화
 
    q->count = 0;
    return;
}
int QisEmpty(queue* q)
{
    if (q->front == q->rear)
        return TRUE;
    else
        return FALSE;
}
struct node* Dequeue(queue* q)
{
    struct node* target;
    // 기본 동작 - 비었는가 확인 -> 삭제 -> 인덱스 변경
    if (q->front == q->rear)
    {
        printf("Queue가 이미 비었습니다.\n");
        return NULL;
    }
    target = q->queue_array[q->front];
    q->queue_array[q->front= NULL;
    q->front = (q->front + 1) % QUEUE_LEN;
    return target;
}
void Enqueue(queue* q, struct node* data)
{
    // 기본 동작 - 꽉 찼는가 확인 -> 삽입 -> 인덱스 변경
    if ((q->rear + 1) % QUEUE_LEN == q->front// 가득 찬 경우,
    {
        printf("Queue가 가득 찼습니다.\n");
        return;
    }
    q->queue_array[q->rear] = data;
    q->rear = (q->rear + 1) % QUEUE_LEN;
    q->count++;
    return;
}
 
void ShowQueue(queue* q)
{
    int i = 0;
    printf("{(r:%2d/f:%2d)} : ", q->rear, q->front);
    for (i = 0; i < QUEUE_LEN; i++)
        printf("[%2p]", q->queue_array[i]), putc('-', stdout);
    putc('\n', stdout);
    return;
}
 
// 내가 정의하는 함수
struct node* CreateNodeAuto(dequeue* dq, queue* q)
{
    struct node* tmp = (struct node*)malloc(sizeof(struct node));
    // 초기화
    tmp->data = 0;
    tmp->prev = NULL;
    tmp->next = NULL// tmp->next = s->head;
    // 큐 추가
    Enqueue(q, tmp);
 
    return tmp;
}
 
void RemoveAllNodeAuto(queue* q)
{
    struct node* rtarget = NULL;
    ShowQueue(q);
    while (!QisEmpty(q))
    {
        rtarget = Dequeue(q);
        printf("[%p/(%d)]가 제거되었습니다.\n", rtarget, q->count);
        ShowQueue(q);
        free(rtarget);
        q->count--;
    }
    return;
}
cs

 

728x90
반응형
728x90
반응형

리스트 기반 큐는 뭐 간단하다. 그냥 일반 큐인데 연결 리스트로 이루어진 큐이다.

기본 형태는 다음과 같다.

매우 간단하다. 그래서 행복하다 

기본적으로 배열 기반의 큐와 똑같이 동작한다. 그러나 배열 기반 큐와 다른 점은 크기가 정해져 있지 않고 동적이고 가변이기 때문에 뭐, 꽉 찬 것으로 보이지만 비어있네, 마네 뭐 이런 배열 기반 큐의 치명적인 단점을 생각할 필요가 없다.

Front 노드 포인터를 통해서 가장 먼저 들어온 노드를 가리키며, 삭제 연산을 진행할 때마다 Front 노드 포인터를 한 노드씩 옮긴다. 그리고 Rear 노드 포인터를 통해서 가장 마지막에 추가된 노드를 가리키며, 삽입 연산을 진행할 때마다, Rear 노드 포인터를 새로운 노드로 갱신한다. 

삽입 연산

 

삭제 연산

 

꽉참/비어짐 확인 연산

꽉 찼는지는 연결 리스트이므로 확인할 필요가 없으니, 비어져있는 상태만 확인하면 되는데 이 역시 매우 간단하다.

Front  노드  포인터가 NULL인지를 확인하면 된다.

메모리 관리

이번에는 연결 리스트 기반 스택으로 관리한다.

코드 및 실행 결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#include <stdio.h>
#include <stdlib.h>
 
#define STACK_LEN 10
 
#define TRUE 1
#define FALSE 0
 
// 공통 노드 정의
struct node
{
    int data;
    struct node* next;
};
 
// 스택으로 메모리 관리.
struct array_stack
{
    struct node* stack_array[STACK_LEN];
    int top_index;
};
typedef array_stack stack;
 
void StackInit(stack* s);
int SIsEmpty(stack* s);
int SIsFull(stack* s); // 증가시키고 대입이기 때문에 현 index에서 증가시키고 대입이 가능한지를 확인해야함.
void SPush(stack* s, struct node* data); // 증가시키고 대입.
struct node* SPop(stack* s);
struct node* SPeek(stack* s);
void ShowStack(stack* s);
 
struct node* CreateNodeAuto(stack* s);
void RemoveAllNodeAuto(stack* s);
 
// 리스트 기반 큐 자료구조(원형일 필요가 없음)
struct list_queue
{
    struct node* front;
    struct node* rear;
    int count;
};
typedef struct list_queue queue;
 
void QueueInit(queue* q);
int QisEmpty(queue* q);
struct node* Dequeue(queue* q);
void Enqueue(queue* q, stack* s, int data);
void ShowQueue(queue* q);
 
// Main
int main(void)
{
    stack s;
    queue q;
 
    QueueInit(&q);
    StackInit(&s);
 
    printf("큐/스택 생성 시작 --- \n");
 
    Enqueue(&q, &s, 1); ShowQueue(&q);
    Enqueue(&q, &s, 2); ShowQueue(&q);
    Enqueue(&q, &s, 3); ShowQueue(&q);
    Enqueue(&q, &s, 4); ShowQueue(&q);
    Enqueue(&q, &s, 5); ShowQueue(&q);
 
    printf("큐 제거 시작 --- \n");
 
    ShowQueue(&q);
    while (!QisEmpty(&q))
    {
        Dequeue(&q);
        ShowQueue(&q);
    }
    fputc('\n', stdout);
 
    printf("스택 제거 시작 --- \n");
    RemoveAllNodeAuto(&s);
    return 0;
}
 
 
// 메모리 관리 스택 관련 함수
 
void StackInit(stack* s)
{
    int i = 0;
    s->top_index = -1;
    for (i = 0; i < STACK_LEN; i++// 순회를 위해서 모두 초기화
        s->stack_array[i] = NULL;
    return;
}
int SIsEmpty(stack* s)
{
    if (s->top_index == -1)
        return TRUE;
    else
        return FALSE;
}
int SIsFull(stack* s) // 증가시키고 대입이기 때문에 현 index에서 증가시키고 대입이 가능한지를 확인해야함.
{
    if ((s->top_index) + 1 < STACK_LEN)
        return FALSE;
    else
        return TRUE;
}
void SPush(stack* s, struct node* data) // 증가시키고 대입임 (top_index가 -1부터 시작하기 때문에)
{
    if (SIsFull(s))
    {
        printf("스택이 가득찼습니다\n");
        return;
    }
    s->stack_array[++(s->top_index)] = data;
    return;
}
struct node* SPop(stack* s)
{
    struct node* rtarget = NULL;
    if (SIsEmpty(s))
    {
        printf("스택이 비어져있습니다\n");
        return NULL;
    }
    rtarget = s->stack_array[s->top_index];
    s->stack_array[s->top_index--= NULL;
    return rtarget;
}
struct node* SPeek(stack* s)
{
    if (SIsEmpty(s))
    {
        printf("스택이 비어져있습니다\n");
        return NULL;
    }
    return s->stack_array[s->top_index];
}
 
void ShowStack(stack* s)
{
    int i = 0;
    printf("{(top:%d)} : ", s->top_index);
    for (i = 0; i < STACK_LEN; i++)
        printf("[%p]", s->stack_array[i]), putc('-', stdout);
    putc('\n', stdout);
    return;
}
 
// 내가 정의하는 함수
 
struct node* CreateNodeAuto(stack* s)
{
    struct node* tmp = (struct node*)malloc(sizeof(struct node));
    // 초기화
    tmp->data = 0;
    tmp->next = NULL// tmp->next = s->head;
    // 메모리 스택에 추가
    SPush(s, tmp);
 
    return tmp;
}
 
void RemoveAllNodeAuto(stack* s)
{
    struct node* rtarget = NULL;
    ShowStack(s);
    while (!SIsEmpty(s))
    {
        rtarget = SPop(s);
        free(rtarget);
        ShowStack(s);
    }
    return;
}
 
// 큐 자료구조 관련 함수
 
void QueueInit(queue* q)
{
    q->front = q->rear = NULL;
    q->count = 0;
    return;
}
int QisEmpty(queue* q)
{
    if (q->front == NULL)
        return TRUE;
    else
        return FALSE;
}
struct node* Dequeue(queue* q)
{
    struct node* target;
    // 기본 동작 - 비었는가 확인 -> 삭제 -> 인덱스 변경
    if (QisEmpty(q))
    {
        printf("Queue가 이미 비었습니다.\n");
        return NULL;
    }
    // target 추출
    target = q->front;
 
    // front를 맨 앞 노드의 다음 노드로 변경
    q->front = q->front->next;
 
    // 카운트 감소
    q->count--;
    return target;
}
void Enqueue(queue* q, stack* s, int data)
{
    // 노드 생성
    struct node* new_node = CreateNodeAuto(s); // 추가되는 노드가 항상 마지막이 된다.
    new_node->data = data;
    new_node->next = NULL;
 
    ShowStack(s);
 
    if (QisEmpty(q)) // 노드가 처음 추가하는 노드라면 front와 rear 모두 갱신
    {
        q->front = q->rear = new_node;
    }
    else // 노드가 처음 추가하는 노드가 아니라면 rear만 갱신
    {
        q->rear->next = new_node;
        q->rear = new_node;
    }
 
    // 카운트 증가
    q->count++;
    return;
}
 
void ShowQueue(queue* q)
{
    struct node* index = NULL;
    printf("{(r:%p/f:%p)} : ", q->rear, q->front);
    for (index = q->front; index != NULL; index = index->next)
        printf("[%p(%d)]", index, index->data), putc('-', stdout);
    putc('\n', stdout);
    return;
}
 
 
 
 
 
 
 
cs

 

728x90
반응형
728x90
반응형

그날 그날 정리하고 올려야 한다.. 묵혀두면 이렇게 고생한다.

-

오늘은 큐에 대해서 기록할 것이다. 큐는 굉장히 중요한 자료구조이다.

큐란 무엇인가?

비유해보자면 선착순이다. 먼저 온 사람이 먼저 서비스를 받는 것!

이를 두고 선입선출의 개념이라고 한다. 그림과 같이 나타낸다.

위 그림에서 옆으로 눕혀진 원통은 배열이라고 생각하면 된다. 배열에서 큐를 구현할 때를 생각해보자.

단순 배열 기반 큐에서의 삽입

단순 배열 기반의 큐에서 삽입 연산을 하는 것은 어려울 것이 없다. 배열의 끝에 값을 추가하듯이 인덱스를 증가시켜가면서 값을 넣으면 된다. 다음과 같이 말이다.

큐를 만들기 나름이지만, 인덱스를 증가시킨 후 삽입하는 것으로 하겠다.

단순 배열 기반 큐에서의 값의 꺼냄(삭제)

그러면 큐의 자료 구조 원칙에 맞도록 먼저 들어온 값을 꺼내려거든 어떻게 해야하나? 이런 경우는 삽입을 위한 인덱스를 하나 두고 증가시키면서 삽입을 진행하고, 꺼내려는 인덱스를 하나 두고 꺼내는 연산을 할 때마다 증가 시키면 된다!

삭제 또한 인덱스를 증가시킨 후 값을 삭제하는 것으로 하겠다.

큐의 꽉참/비워짐 여부 확인

큐를 가지고 삽입 / 삭제 연산을 진행하기 위해서 선행되어야 하는 연산은 큐가 비워졌는지 꽉 찾는지를 확인하면 된다. 아래의 그림은 큐의 비워진 상태를 나타대기 위해서 삽입은 멈추고, 꺼내는 연산만 한 그림이다. 

큐에 값이 꽉 찼음을 확인하는 방법은 선형 큐에서는 다음과 같다.

이 경우이다. 삽입 인덱스가 끝까지 가서 삽입 후 증가를 해야하는데 증가를 하려는데 증가가 안된다. 왜냐하면 인덱스가 끝에 왔기 때문이다. 이 경우 우리는 꽉 찼음을 확인한다. 

배열 기반 큐의 문제

위의 기록한 큐에는 문제가 있다. 다음과 같다.

이는 구조의 문제이다. -> 배열 길이가 정해져있고 순환하지 않는 선형이기 때문에 

이로 인해서 등장하는 것이 원형 큐이다!

원형 큐를 구현하면 선형 큐의 모든 개념 + 선형 큐의 문제 해결이 들어가기 때문에 선형 큐를 따로 포스팅하고 원형 큐에서 구현까지 마무리 지을 것이다.

728x90
반응형
728x90
반응형

큐란?

컨테이너를 확장 및 축소하여 기능을 제한하거나 특정 기능과 특징을 사용하도록 만든 것을 컨테이너 어댑터라고 하는데, 큐 또한 바로 그것이다. 

내부적으로 dequeue, list 등의 컨테이너로 동작하며 기본적으로는 dequeue로 동작한다. vector는 안된다! 왜냐하면 vector는 앞에서 값을 빼는 pop 기능이 없기 때문이다.( vector::push_back( )과 vector::pop_back( )만 존재함 )

사용되고 있는 내부 컨테이너를 변경하려면 queue<data type, list<int>> listack; 이러한 식으로 진행하면 된다. ( 스택에서 사용될 data type, 변경할 컨테이너 구조: list or dequeue등)

코드 및 실행 결과

----

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <queue>
#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    queue<string> qu;
    string words[3= {
        "Apple",
        "Banana",
        "Corn"
    };
 
    for (int i = 0; i < 3; i++)
        qu.push(words[i]);
 
    while (!qu.empty())
    {
        cout << qu.front() << endl;
        qu.pop();
    }
    return 0;
}
cs

----

728x90
반응형

'프로그래밍응용 > Modern & STL' 카테고리의 다른 글

Iterator 사용 방법 예시  (0) 2021.01.05
Priority Queue  (0) 2021.01.04
Stack  (0) 2021.01.03
Tuple  (0) 2021.01.03
Pair  (0) 2021.01.03
728x90
반응형

공부를 하고 정리하는 것이 매우 중요하다. 근데 그 정리를 하기가 조금 귀찮은 것이 아니다. 앞으로는 그 때 그 때 정리하도록 하고, 모아두지 말아야겠다.. 업로드 할 것이 한 두 가지가 아니다ㅜㅜ

자료구조 공부의 순서에 상관없이 그냥 올리려고 한다.

오늘은 우선 순위 큐를 위한 Heap 자료 구조에 대해서 정리한다. 큐 형태로 포장하지 않았을 뿐이지, 큐를 염두하여 만들었기 때문에 큐라고 이름만 바꾸면 우선 순위 큐가 될 수 있다.

Heap 이란 무엇인가?

힙이란 다음 그림과 같은 형태이다.

Heap

Q : 엥? 이건 (완전) 이진 트리가 아닌가? 
A : 그렇다. 맞다
- 빼박 이진 트리 아닌가? 의미의 '완전'이 아니다, 리프 노드를 제외한 모든 노드들은 두 자식 노드를 꼭 채워진 형태의 트리, 리프 노드는 꼭 왼쪽부터 채워지는 트리를 의미하는 '완전 이진 트리'

이 친구들은 이진 트리의 형태를 하고 있으나, 일반 이진 트리와는 달리 좀 많은 규칙들로 탄탄히 채워진 트리이다. 이 규칙들을 통해 힙의 특징을 설명할 것이다.

Heap 의 특징

1. 완전 이진 트리의 특성을 갖는다.
리프 노드를 제외한 모든 노드들은 두 자식 노드를 모두 채운 상태여야하며, 리프 노드는 채울 때, 왼쪽부터 채워져야 한다.

2. 자식 노드와 부모 노드 간의 규칙이 존재한다.
힙은 밑에서 부터 쌓아 올리는 자료 구조이기 때문에 부모 노드와 자식 노드 간 특정한 규칙이 존재한다. 예를 들어, 자식 노드는 부모보다 커야만 한다, 자식 노드는 부모보다 작아야만 한다 등의 규칙이다. 

3. 일반적인 트리와 달리, 왼쪽 자식과 오른쪽 자식은 부모 노드 기준으로 값이 작은 노드와 큰 노드로 취급하지 않는다. 
그저 리프 노드의 자식을 채울 때, 왼쪽에서 오른쪽 순으로 먼저 채우는 것 뿐이다.

 

힙 구조에 값을 삽입


트리의 가장 끝에 삽입 후 부모와 비교하여 위치를 조정한다.

 

힙 구조에서 값을 삭제

삭제할 때에는 탐색 후 삭제가 아니라, Pop의 개념이다. 루트 노드의 값을 빼준다. 그리고 나서 꼭 트리를 재구성 해줘야하는데 이 때, 루트를 어떤 값으로 채울 것인가에 대한 고민을 해야한다. 그러기 위해서는 가장 마지막 노드를 루트에 넣고 트리를 재구성하여 내려간다.

삭제가 진행되기 전 힙 구조

이와 같은 구조에서 3을 제거하고, 10을 루트에 넣어본다.

10을 루트에 넣고 보니 위배되는 부분들이 존재한다. 이를 풀기 위해 계속 구조를 재조정한다.

힙 자료구조의 재구성이 완료된 모습


이 값을 삭제하는 부분으로 인해 우선 순위 큐의 구현이 가능하다!!!

우선 순위 큐란 무엇인가? 우선 순위가 높은 것을 먼저 빼는 것이다. 큐의 선입선출 이딴 거 다 필요없고, 언제 들어가든 간에 우선 순위가 높은 것이 먼저 나오는 것이다.
단, 우선 순위가 같은 경우에는 선입선출이 보장되어야 한다. 이는 리프노드의 왼쪽 삽입 우선 특징(1번 특징)으로 인해서 같은 우선 순위의 경우 선입 선출을 보장하도록 프로그램을 작성할 수 있다. 

즉, 힙의 루트 노드는 삽입, 삭제 과정에서의 힙 자료구조의 재조정을 통해 각 힙에서 지정한 규칙에 의거하여 우선 순위가 높은 값으로 채워넣어진다.
예를 들어

Max Heap의 경우에는 루트 노드로 올라갈수록 값이 커짐 -> 큰 값일 수록 우선 순위가 높다.
Min Heap의 경우에는 루트 노드로 올라갈수록 값이 작아짐 -> 작은 값일 수록 우선 순위가 높다.

사용자 정의 Heap의 경우에는 구조체 등을 이용하여 우선 순위 변수를 따로 두고 진행한다면 이 또한 우선 순위를 매길 수 있다.

이렇게 힙 구조에서 루트 노드에서 값을 빼냄으로써 우선 순위대로 값을 취할 수 있기 때문에 heap을 통한 우선 순위 큐를 구현할 수 있는 것이다.

힙의 구현

힙을 구현할 때에는 배열로 구현할 것이다. 왜냐?

그 이유를 알기 이전에 있어서 힙을 어떤 자료 구조로 구현할 수 있는지부터 확인해보자.

1. 완전 이진 트리이므로, 연결 리스트의 구조로 당연히 구현이 가능하다.
2. 그리고 인덱스 조절을 통한 배열로도 가능하다.

그렇다면 왜 배열로 구현할 것인가?
우리는 삽입 연산을 진행할 때, 트리의 가장 마지막 노드 자리에 값을 삽입한다. 만약 단순 연결 리스트로 구현했다면 이 마지막까지 순회하기에 비용이 많이 든다. 그리고 부모, 자식 간의 노드 비교가 많기 때문에 비교적 비용이 적게 드는 랜덤 엑세스가 가능한 배열로 구현하기로 했다. (라고 윤성우 교수님의 자료구조 서적에 나와있다 ㅋ)

그래서 인덱스 제어 방법이 필요하다.

1. 배열의 인덱스는 1부터 시작한다. ( 인덱싱을 용이하게 하기 위함이다. 구현하다보면 안다. 왜 인덱싱을 1부터 하는지 ㅋㅋㅋ)

2. 현재의 인덱스를 넣었을 때, 부모 혹은 자식의 노드를 구하는 방법을 알아야 한다.

위 그림과 같이 계산하면 임의의 인덱스를 기준으로 부모 노드의 인덱스를 구할 수도, 자식 노드를 구할 수 도 있다. 즉, 완전 이진 트리 구조라고 할지라도 인덱스를 통한 랜덤 엑세스가 가능한 것이다.

아래는 그 코드와 주석이다.

---

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#include <stdio.h>
 
#define TRUE 1
#define FALSE 0
#define HEAP_LEN 100
 
struct heap_element // 정수 값을 저장하는 힙의 노드 구조체로 우선 순위를 지정할 수 있다.
{
    int priority; // 값이 작을수록 우선 순위가 크다.(1등 ~ 10등 ... 개념의 수를 저장함)
    int data;
};
typedef struct heap_element heap_element;
 
struct heap
{
    int count;
    heap_element heap_arr[HEAP_LEN]; // 힙은 배열 구조를 기반으로 구현된다.
};
typedef struct heap heap;
 
void HeapInit(heap* h);
int HIsEmpty(heap* h);
void HInsert(heap* h, int data, int priority);
int HDelete(heap* h);
void ShowHeap(heap* h);
 
int GetParentIndex(int child_index);
int GetLeftChildIndex(int parent_index);
int GetRightChildIndex(int parent_index);
 
int GetChildIndex(heap* h, int parent_index);
 
 
int main(void)
{
    heap h;
    HeapInit(&h);
 
    HInsert(&h, 31);
    HInsert(&h, 52);
    HInsert(&h, 105);
    HInsert(&h, 84);
    HInsert(&h, 126);
    HInsert(&h, 73);
 
    printf("%d \n", HDelete(&h));
    ShowHeap(&h);
 
    printf("--- Del --- \n");
    while (!HIsEmpty(&h))
        printf("%d, ", HDelete(&h));
    fputc('\n', stdout);
    ShowHeap(&h);
    return 0;
}
 
 
void HeapInit(heap* h)
{
    int i = 0;
    h->count = 0;
    for (i = 0; i < HEAP_LEN; i++)
    {
        h->heap_arr[i].data = 0;
        h->heap_arr[i].priority = -1;
    }
    return;
}
int HIsEmpty(heap* h)
{
    return (h->count == 0) ? TRUE : FALSE;
}
 
void HInsert(heap* h, int data, int priority)
{
    int insert_index = h->count + 1
    int parent_index = 0;
    heap_element new_element = { priority, data };
     
    while (insert_index != 1// 처음 삽입되는 위치가 루트 노드가 아니면 혹은 갱신된 추가 인덱스가 루트 노드가 아니면
    {
        parent_index = GetParentIndex(insert_index);
        if (new_element.priority >= h->heap_arr[parent_index].priority) // 우선 순위가 부모가 높다면 구조 재조정이 필요 없다.
            break;
        else // 추가할 노드가 우선 순위가 높은 경우, 계속 구조 재조정이 필요함.
        {
            h->heap_arr[insert_index] = h->heap_arr[parent_index];
            insert_index = parent_index;
        }
    }
    h->heap_arr[insert_index] = new_element;
    h->count++;
    return;
}
 
int HDelete(heap* h)
{
    int r_data = h->heap_arr[1].data; // 삭제할 데이터(Pop 할 데이터와 같은 의미)
    heap_element last_element = h->heap_arr[h->count]; // 비교 대상인 마지막 노드 지정( 마지막 노드를 루트 자리로 올려 비교하기 때문 )
    // 최종 결정된 인덱스
    int parent_index = 1// 루트 노드 부터 시작한다.
    int child_index = 0;
 
    while (child_index = GetChildIndex(h, parent_index))
    {
        if (last_element.priority <= h->heap_arr[child_index].priority) // 자식 노드보다 우선 순위가 높다면 현재 구한 인덱스로의 변경만 하면 된다.
            break;
        else // 계속해서 구조의 재조정이 필요한 경우
        {
            h->heap_arr[parent_index] = h->heap_arr[child_index];
            parent_index = child_index;
        }
    }
    h->heap_arr[parent_index] = last_element;
    h->count--;
    return r_data;
}
// Heap의 내용을 트리의 계층(레벨) 별로 보여준다.
void ShowHeap(heap* h)
{
    int begin = 1end = 1, index = 1;
    int i = 0;
 
    if (h->count == 0)
    {
        printf("heap이 비었습니다!\n");
        return;
    }
 
    printf("%d(%d), \n", h->heap_arr[1].data, h->heap_arr[1].priority);
 
    while (index <= h->count)
    {
        begin = GetLeftChildIndex(begin), end = GetRightChildIndex(end); // 각 레벨 층의 시작과 끝 인덱스 설정 후 출력한다
        if (end > h->count)    end = h->count; // end가 마지막 노드보다 넓은 범위에 있다면 end를 마지막 노드의 인덱스로 설정
 
        index = end + 1// 위에서 지정한 end 다음 값으로 변경한다.
 
        for (i = begin; i <= end; i++)
            printf("%d(%d), ", h->heap_arr[i].data, h->heap_arr[i].priority);
        fputc('\n', stdout);
    }
    return;
}
 
// 부모 노드의 인덱스를 구한다.
int GetParentIndex(int child_index)
{
    return child_index / 2;
}
// 왼쪽 자식 노드의 인덱스를 구한다.
int GetLeftChildIndex(int parent_index)
{
    return parent_index * 2;
}
// 오른쪽 자식 노드의 인덱스를 구한다.
int GetRightChildIndex(int parent_index)
{
    return (parent_index * 2+ 1;
}
// 두 자식 노드 중 우선 순위에 따라 반환한다.
int GetChildIndex(heap* h, int parent_index)
{
    if (GetLeftChildIndex(parent_index) > h->count) // 자식 노드가 없으면 0 반환
        return 0;
    else if (GetLeftChildIndex(parent_index) == h->count) // 자식 노드가 하나인 경우에는 해당 인덱스 반환
        return GetLeftChildIndex(parent_index);
    else // 두 자식 노드 중 우선 순위가 높은 것의 인덱스 반환
    {
        int left_child_index = GetLeftChildIndex(parent_index), right_child_index = GetRightChildIndex(parent_index);
        if (h->heap_arr[left_child_index].priority > h->heap_arr[right_child_index].priority) // 오른쪽 자식 노드가 더 우선 순위가 높다면
            return right_child_index; // 오른쪽 자식 노드의 인덱스 반환
        else // 왼쪽 자식 노드가 더 우선 순위가 높거나 양쪽 노드의 우선 순위가 같다면
            return left_child_index; // 왼쪽 자식 노드의 인덱스 반환
    }
}
 
 
cs

---

728x90
반응형

+ Recent posts