반응형
728x90
반응형

스택이란 정말 간단하다.

분명히 내가 이거 그림 그리면서 포스팅을 한거 같은데 아무리 블로그를 뒤져봐도 없다... 그래서 다시 하는 느낌으로 스택 첫 포스팅을 시작한다.

먼저, 

스택이란 무엇인가?

우리는 스택을 이미 알고 있다. 스택은 프링글스이다. 아래의 그림을 보자.

우리는 세 개의 감자칩 중에서 어떤 감자칩이 제일 먼저 들어갔는지 알 수 있다. 바로 A이다. 어떻게 알았지? 출입구가 하나이고, 원통 내부의 크기는 서로가 뭐 어떻게 비집고 해서 바꿀 수 있는 사이즈가 아니다. 

A가 먼저 들어갔고 -> 그 다음 B가 그 위에 쌓이고 -> 그 후 C가 들어가면서 그 위에 쌓이는 것이다.

이렇듯 스택은 쌓이는 특징을 가지고 있다. 그렇다면 안의 감자칩을 꺼내먹을 때에는 어떻게 할까? 

가장 나중에 들어간 C가 제일 먼저 다시 나오고 -> 그 다음 B가 나오고 -> 가장 먼저 들어간 A가 제일 늦게 나온다. 

이렇게 나중에 들어간 값이 제일 먼저 나오는 구조를 후입선출 구조라고 한다.

단순 배열 기반 스택에서의 삽입

어려울 것이 전혀 없다. 그냥 값이 들어오는데로 쌓아올린다고 생각하고 인덱스를 증가시키면서 값을 추가하면 된다.

82를 삽입 전(왼) / 82를 삽입 후(오)

하나도 어렵지 않다. 이렇게 보면 그냥 배열 끝에 값을 추가하는 것 같다. 우리는 스택에 값을 추가하는 행위push 라고 할 것이다. 삭제 연산을 보자. 

단순 배열 기반 스택에서의 삭제

3 값이 튀어나가면서 삭제 처리가 되어 버리고, 삽입 인덱스가 하나 감소한다.

마지막 하나 남은 1이 튀어 나가며 값이 삭제 처리 되고, 삽입 인덱스가 갈 곳을 잃는데 이 때, 삽입 인덱스를 -1로 하자. 그러면 나중에 빈 스택 상태에서 값을 추가할 때, 삽입 인덱스를 증가시키면 [-1] 에서 [0]으로 바뀌어서 자연스럽게 값을 추가하면 될 뿐만 아니라, 스택이 비어있는지 비어있지 않는지 삽입 인덱스가 -1인지 아닌지를 보면 된다.

그리고 우리는 삭제하는 연산을 pop 한다고 할 것이다.

단순 배열 기반 스택에서의 비워짐과 꽉참을 확인

또한 간단하다.
인덱스가 비워져 있는지를 확인하기 위해선 아까 삭제 연산에의 약속대로 삽입 인덱스가 -1인지를 확인하면 된다. 
인덱스가 가득차 있는지를 확인하기 위해선 현재 삽입 인덱스가 배열의 길이랑 같은지를 확인하면 된다.

코드 및 실행 결과

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
 
#include <stdio.h>
#define STACK_LEN 10
 
#define TRUE 1
#define FALSE 0
 
struct array_stack
{
    int stack_arr[STACK_LEN];
    int top_index;
};
 
typedef array_stack stack;
 
void StackInit(stack* s);
int SIsEmpty(stack* s);
void SPush(stack* s, int data);
int SPop(stack* s);
int SPeek(stack* s);
 
int main(void)
{
    stack s;
    StackInit(&s);
 
    SPush(&s, 1);
    SPush(&s, 2);
    SPush(&s, 88);
    SPush(&s, 125);
    SPush(&s, 9912);
 
    while (!SIsEmpty(&s))
        printf("[%d] ", SPop(&s));
    fputs("\n", stdout);
 
    if (SIsEmpty(&s))
        printf("스택이 텅 비었습니다.\n");
    return 0;
}
 
void StackInit(stack* s)
{
    s->top_index = -1;
    return;
}
int SIsEmpty(stack* s)
{
    if (s->top_index == -1// 같아도 된다고?
        return TRUE;
    else
        return FALSE;
}
void SPush(stack* s, int data)
{
    if (s->top_index == STACK_LEN)
    {
        printf("스택이 꽉 찼습니다.\n");
        return;
    }
 
    s->stack_arr[++(s->top_index)] = data;
    printf("스택에 %d 값이 추가 되었습니다.\n", s->stack_arr[s->top_index]);
    return;
}
int SPop(stack* s)
{
    if (SIsEmpty(s))
    {
        printf("스택이 이미 비어져 있습니다.\n");
        return -1// 적절치 않다. -1 또한 값으로 넣을 수 있기 때문에. -> 차라리 프로그램을 종료시키는게 적절.
    }
    return s->stack_arr[s->top_index--];
}
int SPeek(stack* s)
{
    if (SIsEmpty(s))
    {
        printf("스택이 텅 비었습니다.\n");
        return -1;
    }
    return s->stack_arr[s->top_index];
}
cs

728x90
반응형
728x90
반응형

드디어 원형 큐이다. 자기소개 페이지를 좀 작성하느라, 기록을 하지 못했다.. ㅎㅎ ㅠ.ㅠ

일단, 원형 큐이다. 지난 포스팅 기록에서 처럼 구조로 인해 어쩔 수 없는 단점과 문제를 가지고 있는 일반 배열 기반 큐에서 개선된 것이 원형 큐이다. 그 단점이자 문제는 다음과 같다.

아니 그러면, 삽입 인덱스와 꺼냄 인덱스를 모두 초기화 시켜서 다시 앞으로 옮겨오면 되는 것 아닌가? 라는 생각이 들었었는데 이를 반박할 수 있는 사례가 또 나타난다....

바로 이 경우이다. 순서대로 읽으면 된다. 35와 43 값을 추가 후, 배열의 끝이기 때문에 가정했던대로 삽입 인덱스를 초기화 시켜서 다시 삽입하여 공간을 활용하려는 노력이 보이는 그림이다. 그럼 만약에 위 상황에서 데이터를 순회하려면 어떻게 해야하는가?

7 -> 11 -> 15 -> 35 -> 43 으로 순회를 해야할까? 아니다. 이렇게 되면 큐의 기본 원리에 제대로 위배된다. 35와 43이 먼저 나와야한다. 

위 상황에서 큐의 기본 원리를 지키며 데이터를 순회하기 위해서는
1) 꺼냄 인덱스부터 먼저 확인 후 값을 꺼낸 뒤,
2) 다시 꺼냄 인덱스를 초기화하여 인덱스 0부터 들어간 값을 꺼내며 인덱스를 증가시키고,
3) 삽입 인덱스와 접하는지를 확인한다.
-> 2, 3 을 반복한다.

나 같으면 위와 같은 짓, 안 하겠다. 원형 큐에 대해 당장 알아본다.

먼저, 왼쪽과 같은 일반 배열을 오른쪽처럼 가상으로 구부렸다고 생각하고 모든 기록을 전개할 것이다. 기본 세팅에 대해서 좀 더 설명하자면, 다음 그림과 같다.

일반 큐처럼 삽입 인덱스와 꺼냄 인덱스가 존재하며, 이름이 각각 그림과 같이 달라질 뿐이다. 그리고 각 인덱스는 대입 연산이나 삭제 연산이 진행된 후에 증가하는 형태를 기본으로 한다.

그럼 삽입과 삭제는 그림 한 장으로 설명이 가능할 것 같다.

삽입(왼) / 삭제(오)

그렇다면 삽입을 하려면 큐가 꽉 찾는지 확인해야하며, 삭제하려면 큐가 비어있는지 확인해야한다. 원형 큐의 문제가 바로 이것이기 때문에 중요하다!!!!

먼저, 다음 그림을 보면 문제가 뭔지 알 수 있다

큐가 모두 비었을 때와 가득 찼을 때의 인덱스들의 상태가 완전히 똑같다. 인덱스의 위치에 상관없이 front가 rear을 따라 잡았다는 소리는 삭제를 모두 완료했다고 볼 수 있다. 그러나, rear이 front를 따라 잡았다는 소리는 삽입을 모두 완료했다고 볼 수도 있기 때문이다. 아주 기가 막힙니다.

그래서 원형 큐를 삽입할 때, 큐의 길이 - 1 만큼만 삽입하도록 할 예정이다. 꽉 찬 상태와 텅 빈 상태는 그림과 같다.

이 약속들만 지킨다면 원형 큐를 구현하면서 기가 막힐 일은 없을 것이다.

코드 및 실행 결과

----

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
#include <stdio.h>
 
#define QUEUE_LEN 11
 
#define TRUE 1
#define FALSE 0
 
// 배열 기반 원형 큐 구현
struct array_queue
{
    int front;
    int rear;
    int queue_array[QUEUE_LEN];
};
typedef struct array_queue queue;
 
void QueueInit(queue* q);
int Dequeue(queue* q);
int Enqueue(queue* q, int data);
void ShowQueue(queue* q);
 
int main(void)
{
    queue q;
 
    QueueInit(&q);
 
    for (int i = 0; i < 10; i++)
    {
        Enqueue(&q, i);
        ShowQueue(&q);
    }
    Enqueue(&q, 1000);
    printf("------ end enqueue ------\n");
    ShowQueue(&q);
 
    printf("####################################################################################\n");
 
    for (int i = 0; i < q.rear; i++)
    {
        Dequeue(&q);
        ShowQueue(&q);
    }
    Dequeue(&q);
    printf("------ end dequeue ------\n");
    ShowQueue(&q);
 
 
    printf("####################################################################################\n");
 
    Enqueue(&q, 1000); ShowQueue(&q);
    Enqueue(&q, 1002); ShowQueue(&q);
    Enqueue(&q, 1004); ShowQueue(&q);
    Enqueue(&q, 1006); ShowQueue(&q);
    Dequeue(&q); ShowQueue(&q);
    Dequeue(&q); ShowQueue(&q);
 
 
    return 0;
}
 
void QueueInit(queue* q)
{
    int i = 0;
 
    q->front = q->rear = 0;
 
    for (i = 0; i < QUEUE_LEN; i++)
        q->queue_array[i] = -1// 큐 출력을 위한 초기화
    return;
}
int Dequeue(queue* q)
{
    int rval = 0;
    // 기본 동작 - 비었는가 확인 -> 삭제 -> 인덱스 변경
    if (q->front == q->rear)
    {
        printf("Queue가 이미 비었습니다.\n");
        return 0;
    }
    rval = q->queue_array[q->front];
 
    q->queue_array[q->front= -1;
    q->front = (q->front + 1) % QUEUE_LEN;
 
    return rval;
}
int Enqueue(queue* q, int data)
{
    int rval = 0;
    // 기본 동작 - 꽉 찼는가 확인 -> 삽입 -> 인덱스 변경
    // 2.
    if ((q->rear + 1) % QUEUE_LEN == q->front// 원형 큐이기 때문에 나머지 연산을 통해서 원형 순회를 이룬다.
    {
        printf("Queue가 가득 찼습니다.\n");
        return 0;
    }
    // 1.
    q->queue_array[q->rear] = data;
    rval = q->queue_array[q->rear];
    q->rear = (q->rear + 1) % QUEUE_LEN;
 
    return rval;
}
 
void ShowQueue(queue* q)
{
    int i = 0;
    printf("{(r:%2d/f:%2d)} : ", q->rear, q->front);
    for (i = 0; i < QUEUE_LEN; i++)
        printf("[%2d]", q->queue_array[i]), putc('-', stdout);
    putc('\n', stdout);
    return;
}
 
cs

----

728x90
반응형
728x90
반응형

오늘도 역시 공부해놨던 부분을 이제 포스팅하는 시간이다. 역시 미리미리 해야한다.

스택이란?

일단 그림이 먼저다.

A, B, C 순서로 사각형을 넣을 것이다.

이렇게 된다. 그렇다면 다시 뺀다면?

이 때, 담는 용기, 통! 이것이 스택 자료구조이다. 
먼저 들어간 사각형 A가 제일 늦게 나오는 구조인 것이다. 이러한 개념을 후입선출이라고 한다. 나중에 들어간 것이 먼저 나오는 구조!

이를 구현하면 아래와 같다.

코드 및 실행 결과

---

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
 
#include <stdio.h>
#define STACK_LEN 10
 
#define TRUE 1
#define FALSE 0
 
struct array_stack
{
    int stack_arr[STACK_LEN];
    int top_index;
};
 
typedef array_stack stack;
 
void StackInit(stack* s);
int SIsEmpty(stack* s);
void SPush(stack* s, int data);
int SPop(stack* s);
int SPeek(stack* s);
 
int main(void)
{
    stack s;
    StackInit(&s);
 
    SPush(&s, 1);
    SPush(&s, 2);
    SPush(&s, 88);
    SPush(&s, 125);
    SPush(&s, 9912);
 
    while (!SIsEmpty(&s))
        printf("[%d] ", SPop(&s));
    fputs("\n", stdout);
 
    if (SIsEmpty(&s))
        printf("스택이 텅 비었습니다.\n");
    return 0;
}
 
void StackInit(stack* s)
{
    s->top_index = -1;
    return;
}
int SIsEmpty(stack* s)
{
    if (s->top_index == -1// 같아도 된다고?
        return TRUE;
    else
        return FALSE;
}
void SPush(stack* s, int data)
{
    if (s->top_index == STACK_LEN)
    {
        printf("스택이 꽉 찼습니다.\n");
        return;
    }
 
    s->stack_arr[++(s->top_index)] = data;
    printf("스택에 %d 값이 추가 되었습니다.\n", s->stack_arr[s->top_index]);
    return;
}
int SPop(stack* s)
{
    if (SIsEmpty(s))
    {
        printf("스택이 이미 비어져 있습니다.\n");
        return -1// 적절치 않다. -1 또한 값으로 넣을 수 있기 때문에. -> 차라리 프로그램을 종료시키는게 적절.
    }
    return s->stack_arr[s->top_index--];
}
int SPeek(stack* s)
{
    if (SIsEmpty(s))
    {
        printf("스택이 텅 비었습니다.\n");
        return -1;
    }
    return s->stack_arr[s->top_index];
}
cs

---

중간 중간에 보완해야할 부분이 있지만 개념 부분에서는 충분하기 때문에 보완하지는 않았다.

728x90
반응형

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

C Data Structure - 큐  (0) 2021.01.08
C Data Structure - 퀵 정렬  (0) 2021.01.06
C Data Structure - 병합 정렬  (0) 2021.01.05
C Data Structure - 힙 정렬  (0) 2021.01.05
C Data Structure - 연결 리스트  (0) 2021.01.04
728x90
반응형

이번에는 병합 정렬이다. 이게 생각보다 간단하긴 한데 복잡하다(?) 일단 시작한다.

병합 정렬이란?

병합 정렬의 기본은 다음과 같다.

1. 배열을 원소 단위로 쭉~~ 나눴다가
2. 다시 단계적으로 정렬과 동시에 결합을 반복한다.

그림으로 나타내자면 다음과 같다.

원소 단위 까지 쭉 쪼갠다.
정렬과 결합을 단계적으로 반복한다

그럼 문제는 무엇이냐? 어떻게 쪼갤 것인가? 이다. arr[0] , arr[1] 이게 쪼갠 것 아닌가? 아니다. 그냥 원소에 접근한거지 쪼갠게 아니다. 

그럼 쪼갠다 라는 의미를 아주 곰곰히 생각해봐야한다. 

쪼갠다는 것은 큰 덩어리부터 시작해서 작은 단위로 나눈다는 것이고, 내가 원소 레벨까지 쪼갰다 라는 의미는 내가 지금 쪼개고 있는 진행 흐름이 원소 단위까지 접근했다! 라는 것이다.

좀 어렵지만 잘 생각해라. arr[0] 에 접근하는 것은 우리 집 강아지와 할머니도 할 수 있는 행위이다.(집에 강아지와 할머니는 없다) 이것은 쪼갠 것이 아니고 접근한 거다. 

쪼갠다는 것은 그림의 설명과 같다.

제어할 수 있는 인덱스 범위를 한정지으면 되겠다 이것이다. 그런데 내가 가만히 보니 이 쪼개는 단계가 범위만 줄어드는 것 외에는 모두 같은 짓을 하고 있는 것이다. 그렇다면 원본 배열을 던지고, 그와 함께 범위 값만 다르게 던져주면 알아서 쪼개주는 함수가 없으려나?

있다. 바로 재귀 함수이다. 바로 코드를 본다

위 함수처럼 원본과 범위를 지정하여 함수에 던져주면 알아서 반으로 쪼갤 것이다. 

위 그림처럼 될 것이다.

그렇다면 쪼갰으니 정렬과 결합을 해야한다. 다시 MergeSort 코드를 보겠다.

MergeSort는 범위를 나누어 두 토막 내고(2개의 MergeSort) 그리고 정렬과 결합을(Merge) 수행한다 라는 사실을 기억해야하며, 신뢰해야한다. 그니까 밑에 잔뜩 실행된 MergeSort 함수는 신경쓰지말고, 현재 주어진 범위들에 대해서 정렬하고 결합한다는 것이다.

위 그림처럼 밑 바닥까지 MergeSort 하면서 내려가면 더 이상 쪼개지 못하므로 각 MergeSort에게 지정된 범위에 대해서 Merge 를 통해 정렬과 결합을 수행해주고 리턴한다 그러면 한 단계 위층에서의 MergeSort들은 내부적으로 정렬된 두 덩어리를 받을 것이고 또한 그들도 Merge 를 통해 정렬과 결합을 수행해주고 리턴해준다. 언제까지? 다 합쳐질 때까지!

물론!!!! 여기서 정렬은 조상님이 와서 대신 해주는게 아니다! Merge라는 함수 내에서 이를 구현해야한다. 이는 어렵지 않으므로 코드로 확인해보면 될 것이다.

이 코드가 핵심인데, 앞 범위 토막과 뒷 범위 토막의 요소들을 모두 둘둘씩 비교하여 작은 값을 새로운 공간에 차곡차곡 넣는 방식이다. 이게 가능한 이유는 각 토막들은 아래 계층에서 이미 정렬되어 올라왔기 때문이다! (쓰면서 명확히 깨달음 ㅋ)

위 비교 순서로 값이 정렬되고 합해지는데, 이 원리가 위 코드다 ;;;

내가 무슨 말을 하고 있는지 모르겠다면 직접해봐라 미래의 나야

코드 및 실행 결과

----

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
#include <stdio.h>
#include <stdlib.h>
 
void ShowArray(int arr[], int n)
{
    int i = 0;
    for (i = 0; i < n; i++)
        printf("%d ", arr[i]);
    fputc('\n', stdout);
    return;
}
void Merge(int arr[], int beginint mid, int end)
{
    int bidx = begin, eidx = mid + 1, cidx = 0, lidx = -1// begin side index, end side index, current index, last index
    int* temp = (int*)malloc((end - begin + 1* sizeof(int)); // 필요한 만큼만 할당한다.
 
    while (bidx <= mid && eidx <= end)
    {
        if (arr[bidx] <= arr[eidx])
            temp[cidx++= arr[bidx++];
        else
            temp[cidx++= arr[eidx++];
    }
    if (bidx > mid) // end side는 아직 다 안 채웠음.
        lidx = eidx;
    else if (eidx > end// begin side는 아직 다 안 채웠음.
        lidx = bidx;
 
    // 아직 정렬되지 않은 side를 채워넣는다.
    while (cidx < (end - begin + 1))
        temp[cidx++= arr[lidx++];
 
    // 원래 배열의 해당 인덱스 구간에 정렬된 결과를 반영한다.
    for (lidx = begin, cidx = 0; lidx <= end; lidx++, cidx++)
        arr[lidx] = temp[cidx];
 
    ShowArray(arr, 21);
    free(temp);
    return;
}
 
void MergeSort(int arr[], int beginint end)
{
    int mid = (begin + end/ 2;
 
    if (begin < end// 아직 나눌 수 있음.
    {
        MergeSort(arr, begin, mid);
        MergeSort(arr, mid + 1end);
 
        Merge(arr, begin, mid, end);
    }
    // 나눌 수 없는 경우는 원소 하나 단위이니 할 수 있는게 없다 -> 그냥 리턴
    return;
}
 
 
int main(void)
{
    int arr[21= { 20191817161514131211109876543210 };
    int i = 0;
 
    ShowArray(arr, 21);
    MergeSort(arr, 020);
    printf("최종 정렬 결과 : \n");
    ShowArray(arr, 21);
    return 0;
}
cs

----

머리가 참 아득해진다..

머리가 아득해지는 김에 빅오를 보자

병합 정렬 성능

최악/ 평균 / 최선에 상관없이 비교와 데이터 이동 연산을 모두 세어보면 O(n log 2 n)이다. 2는 아래 첨자 2이다.

728x90
반응형
728x90
반응형

오늘은 힙 정렬이다.

힙 정렬이란?

말 그대로 Heap 자료구조를 이용한 정렬 방식이다. Min Heap에서 루트 노드부터 빼오면 오름차순 정렬이 가능한 점을 이용해한 것이다.

힙 정렬은 Heap 자료 구조 조금 개조하면 만들 수 있다.
아래 그림과 같은 Min-Heap을 이용하면 정렬 성능이 죽여준다 ㅋ 

우선 순위 큐에서도 우선 순위가 높은 것을 작은 값이라고 약속하고 작성한다면 같은 결과이다. 뭐 그 기반이 heap 자료구조이니 heap을 통해서 구현했고, 이미 구현해놓은 것에 우선 순위 부분만 조금 수정하였다. 나중에 기억 안날 때에는 아래 링크 먼저 보고, 다음 코드를 확인하면 될 듯 하다. 

수정한 부분을 보자면, 원래 기존 heap 코드는 우선 순위를 결정하는 부분을 삽입마다 지정하기로 했는데 그것을 함수 포인터를 이용하여 없애버리고 자동으로 우선 순위를 나뉘도록 했다.

https://typingdog.tistory.com/110?category=924874

 

C Data Structure - Heap ( Priority Queue, 우선 순위 큐 ) 개념 및 코드 구현

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

typingdog.tistory.com

다음은 코드이다.

코드 및 실행 결과

----

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
 
#include <stdio.h>
 
#define TRUE 1
#define FALSE 0
#define HEAP_LEN 100
 
struct heap_element // 정수 값을 저장하는 힙의 노드 구조체로 우선 순위를 지정할 수 있다.
{
    int data;
};
typedef struct heap_element heap_element;
 
struct heap
{
    int count;
    heap_element heap_arr[HEAP_LEN]; // 힙은 배열 구조를 기반으로 구현된다.
    int (*ComparePriority)(heap_element* h1, heap_element* h2);
};
typedef struct heap heap;
 
void HeapInit(heap* h, int (*CompFunc)(heap_element* h1, heap_element* h2));
int HIsEmpty(heap* h);
void HInsert(heap* h, int data);
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 ComparePriority(heap_element* n1, heap_element* n2);
 
void HeapSort(int arr[], int n, int (*CompFunc)(heap_element* h1, heap_element* h2))
{
    int i = 0;
    heap h;
    HeapInit(&h, CompFunc);
 
    for (i = 0; i < n; i++)
        HInsert(&h, arr[i]);
    ShowHeap(&h);
 
    printf("\n\n\n");
    for (i = 0; i < n; i++)
        arr[i] = HDelete(&h);
    ShowHeap(&h);
 
    return;
}
 
int main(void)
{
    int arr[21= { 20191817161514131211109876543210 };
    int i = 0;
 
    HeapSort(arr, sizeof(arr) / sizeof(int), ComparePriority);
 
    printf("\n\n\n");
    for (i = 0; i < 21; i++)
        printf("%d, ", arr[i]);
    fputc('\n', stdout);
    return 0;
}
 
int ComparePriority(heap_element* n1, heap_element* n2)
// 앞에꺼가 크면 양수, 뒤에꺼가 크면 음수
    return (n1->data - n2->data);
}
 
void HeapInit(heap* h, int (*CompFunc)(heap_element* h1, heap_element* h2))
{
    int i = 0;
    h->count = 0;
    for (i = 0; i < HEAP_LEN; i++)
        h->heap_arr[i].data = 0;
    h->ComparePriority = CompFunc;
    return;
}
int HIsEmpty(heap* h)
{
    return (h->count == 0) ? TRUE : FALSE;
}
 
void HInsert(heap* h, int data)
{
    int insert_index = h->count + 1;
    int parent_index = 0;
    heap_element new_element = { data };
 
    while (insert_index != 1// 처음 삽입되는 위치가 루트 노드가 아니면 혹은 갱신된 추가 인덱스가 루트 노드가 아니면
    {
        parent_index = GetParentIndex(insert_index);
        if (h->ComparePriority(&new_element, &(h->heap_arr[parent_index])) > 0// 우선 순위가 부모가 높다면 구조 재조정이 필요 없다.
            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 (h->ComparePriority(&last_element, &(h->heap_arr[child_index])) <= 0)// 자식 노드보다 우선 순위가 높다면 현재 구한 인덱스로의 변경만 하면 된다.
            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, \n", h->heap_arr[1].data);
 
    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, ", h->heap_arr[i].data);
        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->ComparePriority(&(h->heap_arr[left_child_index]), &(h->heap_arr[right_child_index])) > 0)// 오른쪽 자식 노드가 더 우선 순위가 높다면
            return right_child_index; // 오른쪽 자식 노드의 인덱스 반환
        else // 왼쪽 자식 노드가 더 우선 순위가 높거나 양쪽 노드의 우선 순위가 같다면
            return left_child_index; // 왼쪽 자식 노드의 인덱스 반환
    }
}
 
cs

----

 

힙 정렬 성능

힙 데이터 저장 시간 복잡도 : O(log2 n) - 2는 아래 첨자의 2이다.
힙 데이터 삭제 시간 복잡도 : O(log2 n) - 2는 아래 첨자의 2이다.
힙 데이터 정렬의 경우, 저장과 삭제를 각각 n번씩 수행하므로 시간 복잡도 : O(n log 2 n) - 2는 아래 첨자의 2이다.

힙 구조에 데이터 삽입, 삭제 등이 이루어진다는 것을 고려한다고 하더라도, 앞서 봤던 O(n^2) 정렬들 보다도 성능이 좋은 것이다.

728x90
반응형
728x90
반응형

하 코딩해놓고 묵혀뒀다가 포스팅하느라 다시 보고 시간 버리는게 너무 아깝다 앞으로는 정말 열심히 바로바로 정리해서 올려야겠다.

그래도 다시 복습할 기회가 되었으니 의미있는 시간이라 생각하고 일반 연결 리스트에 대한 기록이다.

내가 (윤성우 교수님의 자료구조 책의 ADT를 보고) 코딩한 연결 리스트는 살짝 형태가 다르다. 그러므로 특징에 대해서 그림과 글의 적절한 특징 설명 이후 코드와 실행결과를 기록 하겠다.

일단,

단순 연결 리스트란?

구조체와 주소 값을 이용하여 현재 노드 내부에 다음 순서 노드의 주소 값이 저장되어 있는 형태로 이 이어진 연결을 통해 값을 순회할 수 있는 자료 구조이다.

노드 내부에 다음 순서 노드의 주소 값이 저장되어 있다는 것은 이러한 경우를 뜻한다.

구조

원래는 아래 그림과 같은 형식으로 리스트가 구성되어졌어야 했는데 

원래 이런 형식으로 초기 구성되어야 하는데

머리 회전이 덜 된 관계로 아래 처럼 다른 리스트를 만들어버렸다. 그건 뭐 그거대로 기록할 것임.

이렇게 해버렸음. 이걸로 설명을 적는다
노드를 추가한 형태

일단 위 구조로 생겨먹었고, 노드 구조체와 그 노드들을 담고 관리하는 자료구조 구조체가 있다

새 노드가 항상 헤드 다음 즉, 실질 데이터 노드 맨 앞에 항시 존재해야만 한다. 값을 추가할 때, 값과 링크 값만 변경하고 사용한다. 위와 같은 특성 때문에 리스트를 초기화할 때 다음 그림과 같이 꼭 -1 값으로 초기화 한다.

초기화 및 추가

값을 추가할 때에는 빈 노드의 값을 변경하여 사용하지만, 그래도 어찌됐던 빈 노드를 헤드 다음, 데이터 노드들 맨 앞에 두는 것이 원칙이기 때문에 데이터를 추가할 때 빈 노드 또한 추가해 놓아야 한다.

순회
다음은 순회이다.

삭제 
다음은 삭제이다. 내가 따로 만든 함수들이 있기 때문에 간단하게 설명한다.

 

코드 및 실행 결과

----

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
#include <stdio.h>
#include <stdlib.h>
 
struct ListNode {
    int data;
    struct ListNode* link;
};
 
struct ListManager
{
    struct ListNode* head;
    struct ListNode* ci; // current index; 
    struct ListNode* pi; // previous index;
 
    int node_count; // data 값이 유효한 node의 수
    int malloc_count; // 할당된 수
};
 
void ListInit(struct ListManager* lm);
void LInsert(struct ListManager* lm, int data);
int LFirst(struct ListManager* lm, int* data);
int LNext(struct ListManager* lm, int* data);
int LCount(struct ListManager* lm);
int LRemove(struct ListManager* lm);
 
void* CreateNodeMemory(struct ListManager* lm, int len);
void ShowList(struct ListManager* lm);
void DeleteNode(struct ListManager* lm, struct ListNode* target);
void DeleteList(struct ListManager* lm);
 
 
 
int main(void)
{
    struct ListManager lm;
    int data;
    ListInit(&lm);
 
    // 11, 11, 22, 22, 33 삽입
    LInsert(&lm, 11);
    LInsert(&lm, 11);
    LInsert(&lm, 22);
    LInsert(&lm, 22);
    LInsert(&lm, 33);
 
    printf("[11, 11, 22, 22, 33] 값 삽입 후 연결 리스트에 저장된 수 : (%d)\n", LCount(&lm));
    ShowList(&lm);
    fputc('\n', stdout);
 
    // 22, 22 삭제
    if (LFirst(&lm, &data))
    {
        if (data == 22)
            LRemove(&lm);
        while (LNext(&lm, &data))
        {
            if (data == 22)
                LRemove(&lm);
        }
    }
    printf("[22, 22] 값 삭제 후 연결 리스트에 저장된 수 : (%d)\n", LCount(&lm));
    ShowList(&lm);
    fputc('\n', stdout);
 
    // 나머지 11, 11, 33 삭제
    if (LFirst(&lm, &data))
    {
        if (data == 11 || data == 33)
            LRemove(&lm);
        while (LNext(&lm, &data))
        {
            if (data == 11 || data == 33)
                LRemove(&lm);
        }
    }
 
    printf("[11, 11, 33] 값 삭제 후 연결 리스트에 저장된 수 : (%d)\n", LCount(&lm));
    ShowList(&lm);
    fputc('\n', stdout);
 
    // 원래대로 삽입 복원
    LInsert(&lm, 11);
    LInsert(&lm, 11);
    LInsert(&lm, 22);
    LInsert(&lm, 22);
    LInsert(&lm, 33);
 
    printf("원래 [11, 11, 22, 22, 33] 값 삽입 후 연결 리스트에 저장된 수 : (%d)\n", LCount(&lm));
    ShowList(&lm);
    fputc('\n', stdout);
 
    printf("완전 리스트 자체 삭제 이후 : (%d)\n", LCount(&lm));
    DeleteList(&lm);
    ShowList(&lm);
 
    printf("\n\n");
    return 0;
}
 
 
void ListInit(struct ListManager* lm)
{
    // count 초기화
    lm->node_count = 0;
    lm->malloc_count = 0;
 
    // head 다음으로 항상 유지하는 빈 노드 생성
    struct ListNode* new_node = (struct ListNode*)CreateNodeMemory(lm, sizeof(struct ListNode));
    new_node->data = -1;
    new_node->link = NULL// 제일 끝이므로 무조건 NULL을 갖는다.
 
    // 연결리스트 헤드 초기화
    lm->head = (struct ListNode*)CreateNodeMemory(lm, sizeof(struct ListNode));
    lm->head->data = -1;
    lm->head->link = new_node;
 
    // 인덱스 초기화
    lm->ci = NULL;
    lm->pi = NULL;
 
    return;
}
 
void LInsert(struct ListManager* lm, int data)
{
    struct ListNode* new_node = (struct ListNode*)CreateNodeMemory(lm, sizeof(struct ListNode));
 
    // head 다음의 빈 노드에 값을 반영, 링크는 건들지 않는다.
    lm->head->link->data = data;
 
    // empty_node 설정
    new_node->data = -1;
    new_node->link = lm->head->link;
    lm->head->link = new_node;
 
    lm->node_count++;
    return;
}
 
int LFirst(struct ListManager* lm, int* data)
{
    if (lm->node_count == 0)
    {
        printf("순회할 노드가 없습니다.\n");
        return false;
    }
    lm->ci = lm->head->link->link;
    lm->pi = lm->head->link;
 
    *data = lm->ci->data;
    return true;
}
 
int LNext(struct ListManager* lm, int* data)
{
    if (lm->ci->link == NULL)
        return false;
 
    lm->pi = lm->ci;
    lm->ci = lm->ci->link;
    *data = lm->ci->data;
    return true;
}
 
int LCount(struct ListManager* lm)
{
    return lm->node_count;
}
 
int LRemove(struct ListManager* lm)
{
    int remove_value = lm->ci->data;
    struct ListNode* rtarget = lm->ci;
 
    lm->pi->link = rtarget->link;
    lm->ci = lm->pi;
 
    DeleteNode(lm, rtarget);
    return remove_value;
}
 
void* CreateNodeMemory(struct ListManager* lm, int len)
{
    lm->malloc_count++;
    return (void*)malloc(len);
}
 
void ShowList(struct ListManager* lm)
{
    struct ListNode* index_node = NULL;
 
    if (lm->malloc_count == 0)
    {
        printf("head와 new node 및 일반 node들까지 모두 존재하지 않습니다.");
        return;
    }
    else if (lm->node_count == 0)
        printf("추가된 노드는 모두 제거된 상태이지만, head와 new node가 존재하고 추가 가능한 상태입니다.\n");
 
    for (index_node = lm->head; index_node != NULL; index_node = index_node->link)
    {
        printf("[%d|%p]", index_node->data, index_node);
        if (index_node->link != NULL)
            printf("->");
    }
    fputc('\n', stdout);
    return;
}
 
void DeleteNode(struct ListManager* lm, struct ListNode* target)
{
    if (target->data != -1)
        lm->node_count--;
 
    free(target);
    lm->malloc_count--;
 
    return;
}
 
void DeleteList(struct ListManager* lm)
{
    struct ListNode* index_node = NULL;
    struct ListNode* next_node = NULL;
    for (index_node = lm->head; index_node != NULL; index_node = next_node)
    {
        next_node = index_node->link;
        DeleteNode(lm, index_node);
    }
    if (lm->malloc_count != 0)
        printf("메모리 해체에 문제가 있습니다\n");
    return;
}
 
 
cs

----

메모리 관리가 중요한 것 같다.ㅋ

728x90
반응형

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

C Data Structure - 병합 정렬  (0) 2021.01.05
C Data Structure - 힙 정렬  (0) 2021.01.05
C Data Structure - 삽입 정렬  (0) 2021.01.04
C Data Structure - 선택 정렬  (0) 2021.01.04
C Data Structure - 버블 정렬  (0) 2021.01.03
728x90
반응형

삽입 정렬 어렵진 않은데 뭔가 설명을 적기에는 복잡하다 휴 영상이면 좋을텐데 말이지

일단 기록해둔다.

삽입 정렬이란?

삽입해서 정렬하는 것이다. 예를 들어서 

이와 같은 배열이 있을 때, 아래의 그림 순으로 설명하겠다.

그림으로 설명을 좀 대체했다. 나중에 알아먹을 수 있을란가 모르겠군.

그리고 각 그림 속 번호는 그림을 처음 봤을 때 읽어야 할 순서의 동글뱅이 번호를 뜻한다.

코드 및 결과

----

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
#include <stdio.h>
 
void InsertSort(int arr[], int n)
{
    int i = 0, j = 0;
    int target_value = 0;
 
    for (i = 1; i < n; i++// 각 i가 조정할 타겟이 된다.
    {
        target_value = arr[i]; // 삽입할 target value를 따로 저장한다.
        for (j = i-1; j >= 0; j--)
        { // 역순으로 가는 것이니 j가 j+1 보다 먼저이며 앞에서 뒤로 땡기는 행위를 할 것이다.
            if (arr[j] > target_value) // 앞에 것과 비교했을 때 값이 크면 오름차순 위배 
                arr[j+1= arr[j]; // 뒤로 땡긴다.
            else // 오름차순에 맞으므로 
                break// 일단 반복문 하나를 탈출하고,
        }
        arr[j+1= target_value; // 방금 비교한 앞에 요소(arr[j]) 바로 뒤에 빈 공간에 대입
    }
    
    return;
}
 
int main(void)
{
    int i = 0;
    int arr[4= { 3, 5, 9, 1 };
    InsertSort(arr, sizeof(arr) / sizeof(int));
    
    for (i = 0; i < 4; i++)
        printf("%d, ", arr[i]);
    fputc('\n', stdout);
    return 0;
}
cs

----

 

삽입 정렬 성능

이미 정렬되어있는 부분들이 많을수록 성능은 좋다.

최악의 경우 비교, 대입 횟수는 버블 정렬과 마찬가지로 O(n^2) 이다
왜냐하면 안쪽, 바깥쪽 반복의 횟수가 일치하기 때문이다.

최악의 경우 비교 횟수 : (n-1) + (n-2) + ... + 2 + 1 -> O(n^2)
최악의 경우 대입 횟수 : (n-1) + (n-2) + ... + 2 + 1 -> O(n^2)

728x90
반응형
728x90
반응형

 

오늘도 짤막하게 코딩해보고 바로 포스팅한다. 이렇게 하니 매우 여유있게 보고 정리할 수 있는 것 같아서 좋다.
선택 정렬 또한 간단하다 ㅋ

선택 정렬이란?

예시임

위와 같은 배열이 있다. 이를 오름차순으로 정렬할 예정이다.

버블 정렬이 이전 요소들 앞뒤를 비교해가면서 결국, 맨 마지막 대상을 확정 짓는 것이라면 선택 정렬은 반대이다.

맨 왼쪽 요소를 확정 지어가면서 범위를 좁혀나가는 것인데, 버블 정렬처럼 비교한 후 더 변경할 조건이 맞는 경우 값을 교환하는 것이 아니라 변경할 '인덱스'를 저장해 둔 후, 자리를 확정 지을 때 이 인덱스를 활용하여 교환을 한다.

일련의 프로세스 정리

!! 수정 !! :: 3번 동그라미에서 1(인덱스 2)1(인덱스 3)로 변경되어야 합니다!!!!!!!!!!!!!!!!!!!!!

코드 및 실행 결과

----

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
#include <stdio.h>
 
void SelectSort(int arr[], int n)
{
    int min_index = -1;
    int temp = -1;
 
    int i = 0, j = 0;
    for (i = 0; i < n-1; i++// 마지막에서 -1 인덱스까지만 확정 지으면 나머지 하나는 자동 확정~
    {
        min_index = i; // 오름차순이라는 가정 하에, 가장 작은 값이 존재하는 인덱스
        for (j = i+1; j < n; j++// 확정 지을 자리를 비교할 필요가 없으므로 i + 1
        {
            if (arr[min_index] > arr[j])
                min_index = j;
        }
        temp = arr[i];
        arr[i] = arr[min_index];
        arr[min_index] = temp;
    }
    return;
}
 
int main(void)
{
    int i = 0;
    int arr[4= { 5931 };
    SelectSort(arr, sizeof(arr) / sizeof(int));
    
    for (i = 0; i < 4; i++)
        printf("%d, ", arr[i]);
    fputc('\n', stdout);
    return 0;
}
cs

9번 라인에서 i의 조건을 n-1 미만으로 두는 이유를 다음 그림을 통해서 설명하겠다.

 

----

 

 

선택 정렬 성능

최악의 경우 비교 횟수 : (n-1) + (n-2) + ... + 2 + 1 -> O(n^2)
최악의 경우 대입 횟수 : n-1 회 무조건 대입이 일어난다. -> O(n)

버블 정렬에 비하면 양호하지만 버블 정렬에서는 최선의 경우에는 한 번도 대입 연산이 없을 수 있으므로, 비교하기 난감할 뿐이다.

728x90
반응형
728x90
반응형

버블 정렬을 학습 후에 바로 정리한다.

버블 정렬이란?

위와 같은 4칸 크기의 배열 구조가 존재하고 그 안에 값이 정렬되지 않은 채로 존재한다. 여기서 우리는 오름차순으로 값을 정렬하고자 한다.

각 화살표 시작에 적힌 원 숫자 순서대로 값을 결정하는 방법이다. 어떻게? 인덱스를 하나씩 증가시켜가면서 인덱스와 뒤 인덱스를 비교하여 값의 위치를 조정하는 것. 그것이 버블 정렬이다.

총 4개의 값이 들어왔다고 가정.

3번째 자리, 2번째 자리, 1번째 자리, 0번째 자리 순으로 값이 정해져야 한다.

(1) 3번째 자리에 들어올 값을 정하기 위해서는 [0]과 [1](0+1)을 비교, [1]과 [2](1+1)를 비교, [2]와 [3](2+1)을 비교 ( 3번 반복 ) -> 3번째 자리에 들어올 값 결정


(2) 2번째 자리에 들어올 값을 정하기 위해서는 [0]과 [1](0+1)을 비교, [1]과 [2](1+1)를 비교 ( 2번 반복 ) -> 2번째 자리에 들어올 값 결정
(3) 1번째 자리에 들어올 값을 정하기 위해서는 [0]과 [1](0+1)을 비교 ( 1번 반복 ) -> 1번째 자리에 들어올 값 결정
(4) 0번째 자리에 들어올 값은 자연스럽게 결정

결국 총 3번 반복하는데 그 반복 하나하나마다 3번, 2번, 1번 반복을 해야한다.
이를 코드로 나타낸다면

for ( ... )
{
    for( ... )
    {
        ...
    }
}

이러한 형태가 되는 것이다.

코드 구현 및 실행 결과

----

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
#include <stdio.h>
 
void BubbleSort(int* arr, int n)
{
    int i = 0, j = 0;
    int temp = 0;
 
    for (i = 0; i < n - 1; i++)
    {
        for (j = 0; j < n - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                temp = arr[j + 1];
                arr[j + 1= arr[j];
                arr[j] = temp;
            }
        }
    }
    return;
}
 
int main(void)
{
    int arr[4= { 591002 };
    int i = 0;
 
    BubbleSort(arr, sizeof(arr) / sizeof(int));
 
    for (i = 0; i < 4; i++)
        printf("%d, ", arr[i]);
    fputc('\n', stdout);
 
    return 0;
}
cs

i의 역할은 j가 어느 값까지만 접근 가능한지를 결정해주며, j는 i의 제한만큼만 순회하면서 각 i에 해당하는 최대 j값 + 1 번째 인덱스를 가장 큰 값으로 세팅해놓는다.

----

 

버블 정렬 성능

최악의 경우 비교 횟수 : (n-1) + (n-2) + ... + 2 + 1 -> O(n^2)
최악의 경우 대입 횟수 : 비교 후 바로 대입이 진행되므로 O(n^2), temp 연산으로 인한 *3 -> 3 * O(n^2) -> O(n^2)

728x90
반응형
728x90
반응형

공부하고 나면 그 때 그 때 정리해야한다는 것을 뼈저리게 느끼는 시작이다..

이번에 정리할 내용은 단순 배열 리스트이다. 다만, 그냥 배열을 쫘르륵 순회하고, 삭제 띡 하고, 추가하고 그러면 일반 배열 사용법을 정리하는 것과 다를 바가 없으므로, 약간 형식이 있는 리스트 틱한 배열로 만들어 정리한다ㅋㅋ

일단은 정의와 특징, 장 단점을 간단하게 상기시키며 넘어가도록 하겠다.

단순 배열 리스트란?

말 그대로 단순한 배열이다. 그런데 리스트라는 자료구조 형태이므로 메모리 공간 뿐만 아니라 삽입, 삭제, 탐색에 대한 함수를 추가 보완함으로써 하나의 자료 구조로 완성을 시킬 셈이다.

배열

배열 리스트의 장점

Random Access, 임의 접근이 인덱스를 기반으로 가능하기 때문에 데이터의 참조가 쉽다.  O(1) <- 개쩐다ㄷㄷ

배열 리스트의 단점

배열의 길이가 초기에 결정되며, 이 길이는 변경이 불가능하다.
삭제 과정에서 데이터의 이동이 매우 많이 일어난다.
탐색할 경우 최악의 경우에는 O(n)이다. <- 매우 성능이 나오지 않는 것이다. 데이터의 수에 매우 정비례하여 시간이 늘어나기 때문이다.

코드 구현 및 실행

---

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
#include <stdio.h>
#define MAX_LIST_SIZE 20
 
struct List
{
    int insert_index;
    int search_index;
    int arr[MAX_LIST_SIZE];
};
 
void ListInit(struct List* list)
{
    int i = 0;
    list->insert_index = 0// 값을 추가할 때 사용하는 인덱스 -> 모든 값은 0 ~ (insert_index-1) 까지만 유효함!
    list->search_index = 0// 탐색을 위한 순회 시 사용하는 인덱스
 
    // 모두 0으로 초기화
    for (i = 0; i < MAX_LIST_SIZE; i++)
        list->arr[i] = 0;
    return;
}
void LInsert(struct List* list, int value)
{
    if (list->insert_index >= MAX_LIST_SIZE) 
    {
        printf("저장이 불가능합니다.\n");
        return;
    }
    list->arr[list->insert_index++= value;
    return;
}
int LCount(struct List* list) // 현재 추가된 수
{
    return list->insert_index;
}
 
int LFirst(struct List* list, int* data) // 첫 번째 요소에 인덱스를 위치시키는 역할이라고 보면 된다.
{
    if (list->insert_index >= 1// 한 번이라도 값이 추가된 적이 있는 경우에만 사용.
    {
        list->search_index = 0;
        *data = list->arr[list->search_index];
        return 1;
    }
    // search_index를 증가시키지 않는다. 필요하다면 증가를 미리 시키고 접근해야함.
    return 0;
}
int LNext(struct List* list, int* data) // LFirst 함수에 이어 바로 다음 요소에 인덱스를 위치시키는 역할이라고 보면 된다.
{
    if ((list->search_index + 1< list->insert_index) // 접근해야하는 인덱스(search_index+1)가 유효한 인덱스 범위 내에 존재한다면
    {
        *data = list->arr[++(list->search_index)];
        return 1;
    }
    return 0;
}
void LRemove(struct List* list) // 현재 위치한 인덱스의 값을 제거하고, 빈 요소 자리를 채우는 작업까지 진행한다.
{
    int target = 0;
 
    for (target = list->search_index; target < (list->insert_index - 1); target++// 유효 인덱스의 바로 전 인덱스까지만 가서 땡겨오면 끝.
        list->arr[target] = list->arr[target + 1];
    list->search_index--;
    list->insert_index--;
 
    printf("\n");
 
    return;
}
 
int main(void)
{
    struct List list;
    int data;
    ListInit(&list);
 
    LInsert(&list, 11);
    LInsert(&list, 11);
    LInsert(&list, 22);
    LInsert(&list, 22);
    LInsert(&list, 33);
 
 
    if (LFirst(&list, &data))
    {
        printf("%d ", data);
        while (LNext(&list, &data))
            printf("%d ", data);
    }
 
    if (LFirst(&list, &data))
    {
        if (data == 22)
            LRemove(&list);
        while (LNext(&list, &data))
        {
            if (data == 22)
                LRemove(&list);
        }
    }
 
    if (LFirst(&list, &data))
    {
        printf("%d ", data);
        while (LNext(&list, &data))
            printf("%d ", data);
    }
 
    printf("\n\n");
    return 0;
}
cs

---

 

728x90
반응형

+ Recent posts