이 포스팅은 윤성우 님의 열혈 자료구조를 기반으로 합니다.

스택의 이해와 ADT 정의

스택은 자료구조중에 하나로서 흔히 박스를 쌓는 구조로 설명을 한다. 이를 그림으로 표현하면 다음과 같다.

스택의 그림


위의 그림을 보면 2, 4, 6, 8, 10 의 순서로 삽입을 한 형태이다. 먼저 들어온 2가 바닥에 있고 나중에 넣은 10이 위에 있음을 알 수 있다. 이를 상자를 쌓았다고 가정해보자. 상자의 밑 부분 부터 뺄 수 없으므로 당연히 맨 위에 부분부터 뺄 수 있을 것이다. 스택 역시 마찬가지이며 10 부터 뺼 수 있음을 알 수 있다. 이를 후입선출이라 하며 영어로 LIFO(Last In First Out)이다. 그렇다면 스택의 ADT를 살펴보자. 다음과 같다.

void StackInit(Stack * pstack);
// 스택의 초기화를 진행한다.
// 스택 생성 후 제일 먼저 호출되어야 하는 함수이다.

int SIsEmpty(Stack * pstack);
// 스택이 빈 경우 TRUE(1)을, 그렇지 않은 경우 FALSE(0)을 반환한다.

void SPush(Stack * pstack, Data data);
// 스택에 데이터를 저장한다. 매개변수 data로 전달된 값을 저장한다.

Data SPop(Stack * pstack);
// 마지막에 저장된 요소를 삭제한다.
// 삭제된 데이터는 반환이 된다.
// 본 함수의 호출을 위해서는 데이터가 하나 이상 존재함이 보장되어야 한다.

Data SPeek(Stack * pstack);
// 마지막에 저장된 요소를 반환하되 삭제하지 않는다.
// 본 함수의 호출을 위해서는 데이터가 하나 이상 존재함이 보장되어야 한다.

이제 이 ADT를 기반으로 배열 기반, 연결 리스트 기반으로 구현해보자.


배열 기반 스택 구현

스택에서 구현해야 하는 함수는 push, pop이 있고 이를 top이라는 스택의 가장 윗 부분을 담당하는 변수를 이용하여 구현한다.
가장 중요한 함수는 push, pop으로 push의 경우 top을 한칸 올리고 top이 가리키는 위치에 데이터를 저장하고
pop의 경우 top이 가리키는 데이터를 반환하고, top을 아래로 한칸 내린다. push연산을 그림으로 표현하면 다음과 같다.

push 연산

확인할 수 있는 점은 top의 시작은 -1 이며 하나의 데이터가 들어올 때 0이 된다는 것이다. 처음에 A를 추가할 때
top이 하나 증가한 뒤 해당 top의 위치에 원소가 추가되는것을 확인할 수 있으며 이후로도 마찬가지 이다.

pop연산역시 그림으로 표현하면 다음과 같다.

pop 연산

소스코드는 다음과 같다.

<ArrayBaseStack.h>

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
#ifndef __AB_STACK_H__
#define __AB_STACK_H__
 
#define TRUE    1
#define FALSE    0
#define STACK_LEN    100
 
typedef int Data;
 
typedef struct _arrayStack
{
    Data stackArr[STACK_LEN];
    int topIndex;
} ArrayStack;
 
typedef ArrayStack Stack;
 
void StackInit(Stack * pstack);
int SIsEmpty(Stack * pstack);
 
void SPush(Stack * pstack, Data data);
Data SPop(Stack * pstack);
Data SPeek(Stack * pstack);
 
#endif
cs

<ArrayBaseStack.h>

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
#include "ArrayBaseStack.h"
#include <stdio.h>
 
void StackInit(Stack* pstack) {
    pstack->topIndex = -1;
}
 
int SIsEmpty(Stack* pstack) {
    if (pstack->topIndex == -1)
        return TRUE;
    else
        return FALSE;
}
 
void SPush(Stack* pstack, Data data) {
    pstack->topIndex += 1;
    pstack->stackArr[pstack->topIndex] = data;
}
 
Data SPop(Stack* pstack) {
    if (SIsEmpty(pstack)) {
        printf("Stack memory error!");
        return -1;
    }
 
    Data rdata = pstack->stackArr[pstack->topIndex];
    pstack->topIndex -= 1;
 
    return rdata;
}
 
Data SPeek(Stack* pstack) {
    if (SIsEmpty(pstack)) {
        printf("Stack memory error!");
        return -1;
    }
 
    return pstack->stackArr[pstack->topIndex];
}
cs

<ArrayBaseStackMain.c>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include "ArrayBaseStack.h"
 
int main(void)
{
    // Stack의 생성 및 초기화 ///////
    Stack stack;
    StackInit(&stack);
 
    // 데이터 넣기 ///////
    SPush(&stack1);  SPush(&stack2);
    SPush(&stack3);  SPush(&stack4);
    SPush(&stack5);
 
    // 데이터 꺼내기 ///////
    while(!SIsEmpty(&stack))
        printf("%d ", SPop(&stack)); 
 
    return 0;
}
cs

스택의 연결 리스트 기반 구현

앞서 스택을 배열로 구현하였는데 연결 리스트로도 구현할 수 있다. 단순하게 자료구조가 바뀐것 이외에는 개념적 단계에서의 구현에는 큰 차이가 없으므로 바로 헤더파일과 이를 통한 구현을 보일것이다.

<ListBaseStack.h>

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
#ifndef __LB_STACK_H__
#define __LB_STACK_H__
 
#define TRUE    1
#define FALSE    0
 
typedef int Data;
 
typedef struct _node
{
    Data data;
    struct _node * next;
} Node;
 
typedef struct _listStack
{
    Node * head;
} ListStack;
 
 
typedef ListStack Stack;
 
void StackInit(Stack * pstack);
int SIsEmpty(Stack * pstack);
 
void SPush(Stack * pstack, Data data);
Data SPop(Stack * pstack);
Data SPeek(Stack * pstack);
 
#endif
cs

<ListBaseStack.c>

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
#include "ListBaseStack.h"
#include <stdio.h>
#include <stdlib.h>
 
void StackInit(Stack* pstack) {
    pstack->head = NULL;
}
 
int SIsEmpty(Stack* pstack) {
    if (pstack->head == NULL)
        return TRUE;
    else
        return FALSE;
}
 
void SPush(Stack* pstack, Data data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
 
    newNode->next = pstack->head;
    pstack->head = newNode;
}
 
Data SPop(Stack* pstack) {
    if (SIsEmpty(pstack)) {
        printf("stack memory error!");
        exit(-1);
    }
 
    Data rdata;
    Node * rNode;
 
    rNode = pstack->head;
    rdata = pstack->head->data;
    
    pstack->head = pstack->head->next;
 
    free(rNode);
 
    return rdata;
}
 
Data SPeek(Stack* pstack) {
    if (SIsEmpty(pstack)) {
        printf("stack memory error!");
        exit(-1);
    }
 
    return pstack->head->data;
}
cs

<ListStackBaseMain.c>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include "ListBaseStack.h"
 
int main(void)
{
    // Stack의 생성 및 초기화 ///////
    Stack stack;
    StackInit(&stack);
 
    // 데이터 넣기 ///////
    SPush(&stack1);  SPush(&stack2);
    SPush(&stack3);  SPush(&stack4);
    SPush(&stack5);
 
    // 데이터 꺼내기 ///////
    while(!SIsEmpty(&stack))
        printf("%d ", SPop(&stack)); 
 
    return 0;
}
cs

'자료구조' 카테고리의 다른 글

4. 연결 리스트  (0) 2021.07.19
3. 배열 기반 리스트  (0) 2021.07.19
2. 재귀 (Recursion)  (0) 2021.06.27
1. 자료구조와 알고리즘의 이해  (0) 2021.06.27
이 포스팅은 윤성우 님의 열혈 자료구조를 기반으로 합니다.

추상 자료형: Abstract Data Type

추상 자료형은 간단히 ADT라고 불리며 기능만 언급하며 기능의 과정은 포함하지 않는다.
이번에 배울 배열기반 리스트 자료구조의 ADT는 다음과 같다.

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
void ListInit(List * plist); 
// 초기화할 리스트의 주소 값을 인자로 전달한다.
// 리스트 생성 후 제일 먼저 호출되어야 하는 함수이다.
 
void LInsert(List * plist, LData data);
// 리스트에 데이터를 저장한다. 매개변수 data에 전달된 값을 저장한다.
 
int LFirst(List * plist, LData * pdata);
// 첫 번째 데이터가 pdata가 가리키는 메모리에 저장된다.
// 데이터의 참조를 위한 초기화가 진행된다.
// 참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환
 
int LNext(List * plist, LData * pdata);
// 참조된 데이터의 다음 데이터가 pdata가 가리키는 메모리에 저장된다.
// 순차적인 참조를 위해서 반복 호출이 가능하다.
// 참조를 새로 시작하려면 먼저 LFirst함수를 호출해야 한다.
// 참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환
 
LData LRemove(List * plist);
// LFirst 또는 LNext 함수의 마지막 반환 데이터를 삭제한다.
// 삭제된 데이터는 반환된다.
// 마지막 반환 데이터를 삭제하므로 연이은 반복 호출을 허용하지 않는다.
 
int LCount(List * plist);
// 리스트에 저장되어 있는 데이터의 수를 반환한다.
cs

배열을 이용한 리스트의 구현

리스트에는 크게 두가지의 종류가 있고 이는 다음과 같다.

  • 순차 리스트
  • 연결 리스트

리스트 자료구조는 데이터를 나란히 저장한다는 점과 중복 데이터의 저장을 허용한다는 큰 특성이 있다.
이제 위의 ADT를 기반으로 main함수를 작성하면 다음과 같다.

<ListMain.c>

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
#include <stdio.h>
#include "ArrayList.h"
 
int main(void)
{
    /*** ArrayList의 생성 및 초기화 ***/
    List list;
    int data;
    ListInit(&list);
 
    /*** 5개의 데이터 저장 ***/
    LInsert(&list, 11);  LInsert(&list, 11);
    LInsert(&list, 22);  LInsert(&list, 22);
    LInsert(&list, 33);
 
    /*** 저장된 데이터의 전체 출력 ***/
    printf("현재 데이터의 수: %d \n", LCount(&list));
 
    if (LFirst(&list, &data))    // 첫 번째 데이터 조회
    {
        printf("%d ", data);
 
        while (LNext(&list, &data))    // 두 번째 이후의 데이터 조회
            printf("%d ", data);
    }
    printf("\n\n");
 
    /*** 숫자 22을 탐색하여 모두 삭제 ***/
    if (LFirst(&list, &data))
    {
        if (data == 22)
            LRemove(&list);
 
        while (LNext(&list, &data))
        {
            if (data == 22)
                LRemove(&list);
        }
    }
 
    /*** 삭제 후 저장된 데이터 전체 출력 ***/
    printf("현재 데이터의 수: %d \n", LCount(&list));
 
    if (LFirst(&list, &data))
    {
        printf("%d ", data);
 
        while (LNext(&list, &data))
            printf("%d ", data);
    }
    printf("\n\n");
    return 0;
}
cs

위의 ListMain.c를 기반으로 하는 ArrayList.h, ArrayList.c도 다음과 같다.
<ArrayList.h>

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
#ifndef __ARRAY_LIST_H__
#define __ARRAY_LIST_H__
 
#define TRUE    1
#define FALSE   0
 
/*** ArrayList의 정의 ****/
#define LIST_LEN        100
typedef int LData;
 
typedef struct __ArrayList
{
        LData arr[LIST_LEN];
        int numOfData;
        int curPosition;
} ArrayList;
 
 
/*** ArrayList와 관련된 연산들 ****/
typedef ArrayList List;
 
void ListInit(List* plist);
void LInsert(List* plist, LData data);
 
int LFirst(List* plist, LData* pdata);
int LNext(List* plist, LData* pdata);
 
LData LRemove(List* plist);
int LCount(List* plist);
 
#endif
cs

<ArrayList.c>

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
#include <stdio.h>
#include "ArrayList.h"
 
void ListInit(List* plist)
{
        (plist->numOfData) = 0;
        (plist->curPosition) = -1;
}
 
void LInsert(List* plist, LData data)
{
        if (plist->numOfData > LIST_LEN)
        {
                puts("저장이 불가능합니다.");
                return;
        }
 
        plist->arr[plist->numOfData] = data;
        (plist->numOfData)++;
}
 
int LFirst(List* plist, LData* pdata)
{
        if (plist->numOfData == 0)
                return FALSE;
 
        (plist->curPosition) = 0;
        *pdata = plist->arr[0];
        return TRUE;
}
 
int LNext(List* plist, LData* pdata)
{
        if (plist->curPosition >= (plist->numOfData) - 1)
                return FALSE;
 
        (plist->curPosition)++;
        *pdata = plist->arr[plist->curPosition];
        return TRUE;
}
 
LData LRemove(List* plist)
{
        int rpos = plist->curPosition;
        int num = plist->numOfData;
        int i;
        LData rdata = plist->arr[rpos];
 
        for (i = rpos; i < num - 1; i++)
                plist->arr[i] = plist->arr[i + 1];
 
        (plist->numOfData)--;
        (plist->curPosition)--;
        return rdata;
}
 
int LCount(List* plist)
{
        return plist->numOfData;
}
cs

위의 코드들을 자세히 살펴보면 배열 기반 리스트의 삭제가 복잡한 것을 알 수 있다.


먼저 배열 리스트의 데이터 삭제를 살펴보자
배열의 특성상, 그리고 리스트의 특성상 데이터가 나란히 존재해야 하므로 다음의 그림처럼 되는 것을 확인할 수 있다.

또한 가장 최근에 참조가 이루어진 데이터의 인덱스 정보를 담는 변수 curPosition 역시 참조하던 데이터가 삭제되면
앞의 데이터를 참조해야 한다. 이는 다음의 그림과 같다.


실제로 리스트에는 예시의 정수 이외에 다른 자료들도 들어간다. 이번에는 그렇다면 구조체 변수의 주소 값을 저장하여 보자 구조체는 다음과 같다.

1
2
3
4
typedef struct _point {
    int xpos; // x좌표
    int ypos; // y좌표
} Point;
cs

<Point.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef __POINT_H__
#define __POINT_H__
 
typedef struct _point {
    int xpos;
    int ypos;
} Point;
 
// Point 변수의 xpos, ypos 값 설장
void SetPointPos(Point* ppos, int xpos, int ypos);
 
// Point 변수의 xpos, ypos정보 출력
void ShowPosition(Point* ppos);
 
// 두 Point 변수의 비교
int PointComp(Point* pos1, Point* pos2);
 
#endif
cs

<Point.c>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include "Point.h"
 
void SetPointPos(Point* ppos, int xpos, int ypos) {
    ppos->xpos = xpos;
    ppos->ypos = ypos;
}
 
void ShowPosition(Point* ppos) {
    printf("[%d, %d] \n", ppos->xpos, ppos->ypos);
}
 
int PointComp(Point* pos1, Point* pos2) {
    if (pos1->xpos == pos2->xpos && pos1->ypos == pos2->ypos)
        return 0;
    else if (pos1->xpos == pos2->xpos)
        return 1;
    else if (pos1->ypos == pos2->ypos)
        return 2;
    else
        return -1;
}
cs

이 후 ArrayList.h, ArrayList.c를 기반으로 위의 Point 구조체를 저장할 수 있도록 하면 이 과정에서
헤더 파일은 변경이 되어도 되지만 소스파일은 변경이 되면 안 된다.
헤더 파일에서 달라진 점은 typedef int LData; 에서 typedef Point * LData; 으로 변경되었다.
또한 ArrayList.h의 선언문에 #include "Point.h" 를 추가한다. 이제 main함수는 다음과 같다.
<PointListMain.c>

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
#include <stdio.h>
#include <stdlib.h>
#include "ArrayList.h"
#include "Point.h"
 
int main(void) {
    List list;
    Point comPos;
    Point* ppos;
 
    ListInit(&list);
 
    // 4개의 데이터 저장
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 21);
    LInsert(&list, ppos);
 
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 22);
    LInsert(&list, ppos);
 
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 31);
    LInsert(&list, ppos);
 
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 32);
    LInsert(&list, ppos);
 
    // 저장된 데이터의 출력
    printf("현재 데이터의 수는 : %d \n", LCount(&list));
 
    if (LFirst(&list, &ppos)) {
        ShowPosition(ppos);
 
        while (LNext(&list, &ppos)) {
            ShowPosition(ppos);
        }
    }
    printf("\n");
 
    // xpos가 2인 모든 데이터 삭제
    comPos.xpos = 2;
    comPos.ypos = 0;
 
    if (LFirst(&list, &ppos)) {
        if (PointComp(ppos, &comPos) == 1) {
            ppos = LRemove(&list);
            free(ppos);
        }
 
        while (LNext(&list, &ppos)) {
            if (PointComp(ppos, &comPos) == 1) {
                ppos = LRemove(&list);
                free(ppos);
            }
        }
    }
 
    // 삭제 후 남은 데이터 전체 출력
    printf("현재 데이터의 수는 : %d \n", LCount(&list));
 
    if (LFirst(&list, &ppos)) {
        ShowPosition(ppos);
 
        while (LNext(&list, &ppos)) {
            ShowPosition(ppos);
        }
    }
    printf("\n");
 
    return 0;
}
cs

이제 정리해보자.

  • 배열 기반 리스트의 단점
    • 배열의 길이가 초기에 결정되어야 한다. 또한 변경이 불가능하다.
    • 삭제의 과정에서 데이터의 이동이 일어난다. (마지막 경우가 아닌 경우)
  • 배열 기반 리스트의 장점
    • 데이터의 참조가 쉽다. 인덱스를 기준으로 한 번에 참조가 가능하다.

'자료구조' 카테고리의 다른 글

5. 스택  (0) 2022.01.10
4. 연결 리스트  (0) 2021.07.19
2. 재귀 (Recursion)  (0) 2021.06.27
1. 자료구조와 알고리즘의 이해  (0) 2021.06.27

시간 복잡도(Time Complexity)와 공간 복잡도(Space Complexity)

시간 복잡도는 속도에 관한 것이며 공간 복잡도는 메모리 사용량에 관한 것이다. 시간 복잡도는 연산 횟수로 구한다.


데이터의 개수가 n이하는 알고리즘 A가 유리하고 n이상은 알고리즘 B가 유리한것을 확인할 수 있다.
따라서 상황에 맞게 적절한 알고리즘을 택해야 한다. 다음의 코드를 살펴보자.

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
#include <stdio.h>
 
int LSearch(int arr[], int len, int target) { // Linear Search 함수
    for (int i = 0; i < len; i++) {
        if (arr[i] == target) // 찾으면 해당 위치의 인덱스 반환
            return i;
    }
    return -1;
}
int main(void) {
    int arr[] = { 25319 };
    int idx;
 
    idx = LSearch(arr, sizeof(arr) / sizeof(int), 9);
    if (idx == -1
        printf("탐색 실패\n");
    else 
        printf("타겟 인덱스: %d \n", idx);
 
    idx = LSearch(arr, sizeof(arr) / sizeof(int), 6);
    if (idx == -1)
        printf("탐색 실패\n");
    else
        printf("타겟 인덱스: %d \n", idx);
 
    return 0;
}
cs

이 경우 최악의 시간복잡도는 O(N)이다. 최선, 평균, 최악이 있지만 최악을 기준으로 잡는다.
이번에는 이진 탐색(Binary Search)알고리즘을 보자. 순차 탐색에 비해 좋은 성능을 내지만 정렬이 되어있어야 한다는 제약 조건이 존재한다.

이진탐색 (Binary Search)

이진탐색을 먼저 그림으로 나타내면 다음과 같다.

3을 찾기 위해 반씩 줄여나가며 총 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
35
36
37
#include <stdio.h>
 
int BSearch(int arr[], int len, int target) {
    int first = 0;
    int last = len - 1;
    int mid;
 
    while (first <= last) { // fist와 last가 뒤집어지면 종료
        mid = (first + last) / 2;
        if (arr[mid] == target)
            return mid;
        else if (arr[mid] > target) // 중간 값이 target보다 큰 경우
            last = mid - 1// mid 좌측에서 탐색 진행
        else // 중간 값이 target보다 작은 경우
            first = mid + 1;
    }
    return -1// 탐색하지 못한 경우
}
 
int main(void) {
    int arr[] = { 1357911 };
    int idx;
 
    idx = BSearch(arr, sizeof(arr) / sizeof(int), 7);
    if (idx == -1)
        printf("탐색 실패 \n");
    else
        printf("타겟 인덱스 위치: %d \n", idx);
 
    idx = BSearch(arr, sizeof(arr) / sizeof(int), 8);
    if (idx == -1)
        printf("탐색 실패 \n");
    else
        printf("타겟 인덱스 위치: %d \n", idx);
 
    return 0;
}
cs

이 경우 최악의 시간 복잡도는 O(logN)이다.

각 빅 - 오 표기법들의 성능 비교

각 빅-오 표기법들의 성능은 다음과 같다.


순서대로 O(1) < O(logN) < O(N) < O(NlogN) < O(𝑁^2) < O(2^N) < O(N!) 이다.

'자료구조' 카테고리의 다른 글

5. 스택  (0) 2022.01.10
4. 연결 리스트  (0) 2021.07.19
3. 배열 기반 리스트  (0) 2021.07.19
2. 재귀 (Recursion)  (0) 2021.06.27

+ Recent posts