본문 바로가기

NOTES/CS50

[CS50] Chapter 6. 자료구조

최초작성일: 2023-03-01
마지막 수정일: 2023-03-04

부스트코스 <모두를 위한 컴퓨터 과학 (CS50 2019)> 강의 수강

 

6. 자료구조 Data Structures

1. malloc과 포인터 복습

  • 포인터의 개념과 malloc 함수의 용법을 잘 이해할 수 있다.
  • 포인터, malloc

 

포인터 선언

int main(void)
{
    int *x;     // 정수를 가리키는 포인터를 만들고 x라고 부름
    int *y;     // 정수를 가리키는 변수 y

    x = malloc(sizeof(int));    // int 크기의 메모리를 만들어 x에 할당

    *x = 42;    // x에 저장된 주소로 가서 42를 저장
    *y = 13;    // 변수를 위한 공간을 할당하지 않음
}
  • 포인터를 할당하면 어떤 포인티를 가리킬지 별개로 설정하는 과정이 필요하다.
  • 위의 코드에서 y는 포인터로 선언만 되었을 뿐 어디를 가리킬지는 아직 정의되지 않은 상태
  • 초기화되지 않은 *y는 프로그램 어딘가를 임의로 가리키고 있을 수 있다.
    y = x;      // x가 가리키는 곳과 동일한 곳 가리킴
    *y = 13;    // y에 저장된 주소(=x에 저장된 주소)로 가서 13 저장.
    // 원래 x에 저장된 주소 위치에 저장했던 42는 13으로 덮어쓰기된다.

 

2. 배열의 크기 조정하기

  • 배열의 크기를 조정하는 코드를 작성할 수 있다.
  • malloc, realloc

C에서 배열의 크기는 미리 지정해야

  • 배열의 크기를 바꾸려면 다른 메모리로 이동시켜야 한다.
  • O(n): 배열의 길이에 따라 옮기는 데 더 많은 과정이 필요

malloc 이용해서 배열 크기 바꾸기

#include <stdio.h>
#include <stdlib.h>     // malloc

int main(void)
{
    // int 자료형 3개로 이루어진 list라는 포인터를 선언하고 메모리 할당
    int *list = malloc(3 * sizeof(int));    // list는 메모리 덩어리의 주소를 담고 있음

    // 포인터가 잘 선언되었는지 확인
    if (list == NULL)   // 메모리가 부족해서 NULL이 반환되었을 때
    {
        return 1;
    }

    list[0] = 1;        // 주소는 4바이트씩 차이남
    list[1] = 2;
    list[2] = 3;

    int *tmp = malloc(4 * sizeof(int));
    if (tmp == NULL)
    {
        return 1;
    }

    // Copy integers from old array into new array
    for (int i = 0; i < 3; i++)
    {
        tmp[i] = list[i];
    }

    tmp[3] = 4;

    free(list);     // 메모리 해제

    list = tmp;     // 이름 바꿈

    for (int i = 0; i < 4; i++)
    {
        printf("%i\n", list[i]);
    }
}

/*
1
2
3
4
*/

realloc 이용해서 배열 크기 바꾸기

  • realloc
    • 이미 할당받은 기존 메모리 덩어리를 새롭게 가져오고 새롭게 설정된 크기로 바꾸는 작업을 함
    • 기존 배열에서 새로운 배열로 데이터를 복사해줌
#include <stdio.h>
#include <stdlib.h>     // malloc   // realloc

int main(void)
{
    int *list = malloc(3 * sizeof(int));    // list는 메모리 덩어리의 주소를 담고 있음
    if (list == NULL)   // 메모리가 부족해서 NULL이 반환되었을 때
    {
        return 1;
    }

    list[0] = 1;        // 주소는 4바이트씩 차이남
    list[1] = 2;
    list[2] = 3;

    int *tmp = realloc(list, 4 * sizeof(int));
    if (tmp == NULL)
    {
        return 1;
    }

    list = tmp;

    tmp[3] = 4;

    for (int i = 0; i < 4; i++)
    {
        printf("%i\n", list[i]);
    }

    free(list);     // 메모리 누수 방지
}

/*
1
2
3
4
*/

 

3. 연결 리스트: 도입

  • 연결 리스트의 정의를 설명할 수 있다.

연결 리스트 (Linked Lists)

배열

  • 물리적으로 연속된 메모리상에 데이터가 저장되어 있다.
  • 쉽게 인덱싱할 수 있고 대괄호를 통해 일정한 시간에 접근(random access)할 수 있어 탐색이 매우 빠르다. 이진탐색 이용 가능
  • 그러나 배열의 크기를 변경할 때 메모리의 다른 공간에 값들을 복사하는 과정이 이루어진다.
  • 많은 양의 데이터를 저장하려면 메모리상에 연속된 여유 공간이 존재해야 한다.

연결 리스트

  • 배열의 메모리 문제를 해결해보자
  • 메모리 덩어리에 자신의 값과 배열의 다음 요소를 가리키는 포인터(next)를 저장
  • 배열의 끝에서는 NULL

image

typedef struct node     // 구조체 안에서 node를 사용하기 위해 미리 말해줌
{
    int number;             // 각 노드가 가지는 값
    struct node *next;      // 다음 노드를 가리키는 포인터
}
node;

 

4. 연결 리스트: 코딩

  • 연결 리스트를 구현하고 사용할 수 있다.

연결 리스트 구현

#include <stdio.h>
#include <stdlib.h>     // malloc, free

// Represents a node
// 연결 리스트의 기본 단위 node 구조체 정의
typedef struct node
{
    int number; 
    struct node *next;
}
node;

int main(void)
{
    // List of size 0
    // list라는 이름의 node 포인터 정의. 연결 리스트의 가장 첫 번째 node를 가리킴. 
    // 현재 아무 것도 가리키고 있지 않으므로 NULL 로 초기화.
    node *list = NULL;

    // 새로운 node를 위해 메모리를 할당하고 포인터 *n으로 가리킴(임시로 쓰는 변수).
    node *n = malloc(sizeof(node));
    if (n == NULL)      // 메모리가 제대로 할당되었는지 검사.
    {
        return 1;
    }
    // NULL이 아닐 경우 *n의 number와 next 값을 저장하는 것으로도 쓸 수 있음.


    // n의 number 필드에 1의 값을 저장.
    // n이 가리키는 node의 number 필드에 접근해서 1로 만들어준다.
    n->number = 1;      // “(*n).numer”와 동일한 의미

    // n 다음에 정의된 node가 없으므로 NULL로 초기화.
    n->next = NULL;

    // 이제 첫번째 node를 정의했기 떄문에 list 포인터를 n 포인터로 바꿈.
    list = n;

    // list에 다른 node를 더 연결하기 위해 n에 새로운 메모리를 다시 할당.
    n = malloc(sizeof(node));
    if (n == NULL)
    {
        return 1;
    }

    // n의 number와 next의 값을 각각 저장.
    n->number = 2;
    n->next = NULL;

    // list가 가리키는 것은 첫 번째 node 
    //이 node의 다음 node를 n 포인터로 지정.
    list->next = n; // list의 next 필드에 포인터 n 저장

    // 다시 한 번 n 포인터에 새로운 메모리를 할당하고 number과 next의 값을 저장.
    n = malloc(sizeof(node));
    if (n == NULL)
    {
        return 1;
    }

    n->number = 3;
    n->next = NULL;

    // 현재 list는 첫번째 node를 가리키고, 이는 두번째 node와 연결되어 있다. 
    // 따라서 세 번째 node를 더 연결하기 위해 첫 번째 node (list)의 
    // 다음 node(list->next)의 다음 node(list->next->next)를 n 포인터로 지정.
    list->next->next = n;       // 보통은 루프를 이용

    // 이제 list에 연결된 node를 처음부터 방문하면서 각 number 값을 출력. 
    // 마지막 node의 next에는 NULL이 저장. 이것이 for 루프의 종료 조건.
    for (node *tmp = list; tmp != NULL; tmp = tmp->next)
    {
        printf("%i\n", tmp->number);
    }

    // Free list
    // 메모리를 해제해주기 위해 list에 연결된 node들을 처음부터 방문하면서 free 해 줌.
    while (list != NULL)        
    {
        node *tmp = list->next;
        free(list);
        list = tmp;
    }
}
// 내가 무엇을 가리키고 있든 next 필드를 가리켜라

node *tmp = list;
while (tmp->next != NULL)   // NULL이 아니라면
{
    tmp = tmp->next;        // 계속해서 자기 자신을 업데이트해서 그 자신이 가리키고 있는 것을 가리키도록 함
}
// 

구현 시 주의점

  • A, B 노드 사이에 C 노드를 추가한다고 할 때, 새로 추가되는 노드(C)에 뒤에 이어질 노드(B)의 주소값을 저장해 둔 뒤에(이때 A와 C 모두가 B를 가리킨다) 원래 그 노드(B)를 가리키고 있던 노드(A)에 저장된 다음 노드 주소를 바꿔주어야(B 주소를 C 주소로) 한다.
  • 노드 C에 값을 넣은 뒤에 다음 노드(B)에 대한 정보를 넣지 않고 바로 A에 저장된 다음 노드 주소를 바꿔버리면 노드 B 이후의 노드들은 버려지게 된다.(메모리 누수(memory leak) 발생)
    • 이 경우 이 값들이 어디에 있는지 기억하고 있는 변수가 없기 때문에 이 메모리에 다시 접근할 수 없다.

 

5. 연결 리스트: 시연

  • 연결 리스트와 배열의 장단점을 설명할 수 있다.
  • 연결 리스트, 배열

연결 리스트의 장단점

장점

  • 배열처럼 데이터를 추가하기 위해 크기를 조절하고 기존의 값을 모두 옮기지 않아도 된다.
  • 원래 있었던 것을 움직이지 않고도 리스트를 늘릴 수 있음.

단점

  • 컴퓨터는 한 번에 한 개의 메모리만 볼 수 있다.
    • 특정 값을 찾으려면 포인터를 따라가면서 모든 메모리를 하나씩 열어봐야 한다.
    • 순서를 맞춰서 중간에 삽입할 곳을 찾을 때에도 마찬가지.
  • 임의 접근(random access)을 할 수 없다. -> 이진 탐색을 효과적으로 할 수 없다.

연결 리스트를 이용한 작업의 시간복잡도

  • 탐색: O(n)
  • 삽입: O(n) - 배열의 경우는 이진 탐색을 이용하면 O(logn)

 

6. 연결 리스트: 트리

  • 트리의 구조를 설명하고 활용하는 코드를 작성할 수 있다.
  • 트리, 루트

이진 탐색 트리

  • 트리에서 노드들의 연결은 2차원적.
  • 트리의 노드들이 모두 최대 두 개의 자식 노드가 있는 '이진 트리'
  • 탐색을 위해 데이터의 순서에 신경 쓴 '탐색 트리'
  • 트리에 있는 모든 노드에서, 왼쪽 자식 노드는 그 노드보다 더 작고 오른쪽 자식 노드는 더 큼.(재귀적 정의)
  • 연결 리스트처럼 역동성을 가지면서 이진 탐색도 가능한 자료 구조
  • 트리를 저장하기 위해 하나의 포인터가 루트를 가리켜야.
  • 루트: 트리가 시작되는 노드. 부모가 없는 노드.
//이진 검색 트리의 노드 구조체
typedef struct node
{
    int number;
    struct node *left;      // 왼쪽 자식 노드
    struct node *right;     // 오른쪽 자식 노드
} node;

// 이진 검색 함수 (*tree는 이진 검색 트리를 가리키는 포인터)
bool search(node *tree)
{
    // 트리가 비어있는 경우 ‘false’를 반환하고 함수 종료
    if (tree == NULL)       // 재귀의 종료 조건
    {
        return false;
    }
    // 현재 노드의 값이 50보다 크면 왼쪽 노드 검색
    else if (50 < tree->number)
    {
        return search(tree->left);  // 왼쪽 서브 트리
    }
    // 현재 노드의 값이 50보다 작으면 오른쪽 노드 검색
    else if (50 > tree->number)
    {
        return search(tree->right); // 오른쪽 서브 트리
    }
    // 위 모든 조건이 만족하지 않으면 노드의 값이 50이므로 ‘true’ 반환
    else {
        return true;
    }
}
  • 탐색의 시간복잡도는 O(logn)
  • 재귀에 가장 적합한 방법
  • 값을 삽입 또는 삭제할 경우 트리의 균형을 유지해줄 수 있는 알고리즘이라면 logn의 실행 시간.

 

7. 해시 테이블

  • 해시 테이블의 원리와 구조를 설명할 수 있다.
  • 해시 테이블, 해시 함수

해시 테이블

  • 연결 리스트의 배열
  • 해시 함수라는 맞춤형 함수를 통해 어떤 칸에 담기는지 결정
  • 같은 칸에 담기는 값들(충돌 발생)은 그 칸에서 새롭게 정의되는 연결 리스트로 이어짐

 

  • 해시 함수가 이상적이라면 해시 테이블의 각 칸에는 단 하나의 값들만 담겨 검색 시간은 O(1)이 된다.
  • 최악의 경우에는 단 하나의 칸에 모든 값들이 담겨 O(n)이 된다.
  • 일반적으로는 최대한 충돌이 일어나지 않도록 하는 해시 함수를 만들어서 거의 O(1)에 가깝다.

 

8. 트라이

  • 트라이의 원리와 구조를 설명할 수 있다.

트라이

  • 각 노드가 배열로 구성되어 있는 트리 형태의 자료 구조

예시

  • 영어 알파벳으로 이루어진 문자열 값을 저장한다고 하면 이 노드는 a부터 z까지의 값을 갖는 배열
  • 배열의 각 요소는 다음 층의 노드(a-z 배열)를 가리킴
  • 영어 이름을 트라이에 저장한다고 했을 때, 문자 순서대로 따라가면서 노드들이 이어진다.
    • 그런 트라이에서 값을 검색하는 데 걸리는 시간은 문자열의 길이에 의해 한정.
    • 영어 이름의 길이를 n이라 할 때, 검색 시간은 O(n)이지만, 대부분의 이름의 길이는 그리 크지 않은 상수값이므로 O(1)이나 마찬가지.

 

9. 스택, 큐, 딕셔너리

  • 스택, 큐, 딕셔너리의 원리와 구조를 설명할 수 있다.

큐 (Queue)

FIFO. 선입선출의 특징을 가진 자료구조

큐의 연산

  • enqueue
  • dequeue

스택 (Stack)

LIFO. 후입선출의 특징을 가진 자료구조

스택의 연산

  • push
  • pop

딕셔너리 (Dictionary)

키와 값의 쌍으로 이루어진 자료구조

  • 일종의 해시 테이블
  • 를 어떻게 정의할 것인지가 중요하다.

 

'NOTES > CS50' 카테고리의 다른 글

[CS50] Chapter 5. 메모리  (0) 2023.02.25
[CS50] Chapter 4. 알고리즘  (0) 2023.02.13
[CS50] Chapter 3. 배열  (2) 2023.02.04
[CS50] Chapter 2. C언어  (1) 2023.01.30
[CS50] Chapter 1. 컴퓨팅 사고  (0) 2023.01.30