42서울 7기 라피신을 대비하여 정리한 CS50 2019 과정을 간략히 복기하여 재업로드한 내용입니다.
선형 검색
이진 검색
For i from 0 to n–1
If i'th element is 50
Return true
Return false
If no items
Return false
If middle item is 50
Return true
Else if 50 < middle item
Search left half
Else if 50 > middle item
Search right half
O(n)
과 O(n/2)
의 그래프는 매우 비슷해져 구분이 무의미해진다.O(log2n)
의 경우 log2
, log3
...log
등등 밑으로 어떤 숫자가 오든 구분이 무의미해진다.O(n^2)
O(n log n)
O(n)
O(log n)
O(1)
Big O
가 알고리즘 실행 시간의 상한을 나타낸 것이라면, 반대로 Big Ω
는 알고리즘 실행 시간의 하한을 나타낸 것.Ω(n^2)
Ω(n log n)
Ω(n)
Ω(log n)
Ω(1)
O(n)
이 되지만 운이 좋다면 한 번만에 검색을 끝낼수도 있으므로 하한은 Ω(1)
이다.n
이라고 했을 때, 최악의 경우 리스트의 모든 원소를 확인해야 하므로 n
번만큼 실행된다.#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
string names[] = {"EMMA", "RODRIGO", "BRIAN", "DAVID"};
string numbers[] = {"617–555–0100", "617–555–0101", "617–555–0102", "617–555–0103"};
for (int i = 0; i < 4; i++)
{
if (strcmp(names[i], "EMMA") == 0)
{
printf("Found %s\n", numbers[i]);
return 0;
}
}
printf("Not found\n");
return 1;
}
names
배열과 numbers
배열로 각각 따로 정의하였다.names
배열과 numbers
배열이 서로 같은 인덱스를 가져야 한다는 한계가 있다.typedef struct
{
string name;
string number;
}
person;
int main(void)
{
person people[4];
people[0].name = "EMMA";
people[0].number = "617–555–0100";
people[1].name = "RODRIGO";
people[1].number = "617–555–0101";
people[2].name = "BRIAN";
people[2].number = "617–555–0102";
people[3].name = "DAVID";
people[3].number = "617–555–0103";
// EMMA 검색
for (int i = 0; i < 4; i++)
{
...
}
return 1;
}
버블 정렬
이란 그러한 정렬 알고리즘 중 하나이다.Repeat n–1 times
For i from 0 to n–2
If i'th and i+1'th elements out of order
Swap them
n
개의 값이 주어졌을 때 각 루프는 각각 n-1
번, n-2
번 반복되므로 (n-1)*(n-2) = n^2-3n+2
번의 비교 및 교환이 필요하다.n^2
이므로 위와 같은 코드로 작성한 버블 정렬 실행 시간의 상한은 O(n^2)
이라고 말할 수 있다.Ω(n^2)
이다.For i from 0 to n–1
Find smallest item between i'th item and last item
Swap smallest item with i'th item
O(n^2)
이며, 하한도 마찬가지로 Ω(n^2)
이다.Repeat until no swaps
For i from 0 to n–2
If i'th and i+1'th elements out of order
Swap them
Repeat n–1 times
로, 정렬이 전부 끝나도 무조건 n-1
번 만큼 반복되어야 했다.교환이 일어나지 않을때
까지만 수행하도록 조건을 수정하면, 불필요한 교환을 없앨 수 있다.Ω(n)
이 되어, 경우에 따라서는 선택 정렬보다 빠른 방법이 될 수 있다.#include <cs50.h>
#include <stdio.h>
void draw(int h);
int main(void)
{
int height = get_int("Height: ");
draw(height);
}
void draw(int h)
{
// 높이가 0이라면 (그릴 필요가 없다면)
if (h == 0)
{
return;
}
// 높이가 h-1인 피라미드 그리기
draw(h - 1);
// 피라미드에서 폭이 h인 한 층 그리기
for (int i = 0; i < h; i++)
{
printf("#");
}
printf("\n");
}
#
##
###
####
draw
함수를 따라가다 보면 h = 0
인 상황이 온다. 그 때는 아무것도 출력을 하지 않도록 하는 조건문을 추가해줘야 한다.Base case
와 Recursive case
를 구분해야 한다.depth
를 한 단계 늘리는 것이고, for
반복문은 해당 depth
에서의 노드의 개수, 즉 폭 을 넓힌다고 이해해 봤다.O(n log n)
의 시간 복잡도가 보장된다.Merge Sort(A[], p, r) { // 배열 A[p...r]을 정렬한다.
if(p<r) then {
q <- (p+q)/2;
mergesort(A, p, q);
mergesort(A, q+1, r);
merge(A, p, q, r);
}
}
merge(A[], p, q, r) {
정렬되어 있는 두 배열 A[p...q]와 A[q+1...r]을 합하여
정렬된 하나의 배열 A[p...r]을 만든다.
}
realloc
O(n)
, 즉 배열의 크기 n
만큼의 실행 시간이 소요될 것이다.#include <stdio.h>
#include <stdlib.h>
int main(void)
{
//int 자료형 3개로 이루어진 list 라는 포인터를 선언하고 메모리 할당
int *list = malloc(3 * sizeof(int));
// 포인터가 잘 선언되었는지 확인
if (list == NULL)
return 1;
// list 배열의 각 인덱스에 값 저장
list[0] = 1;
list[1] = 2;
list[2] = 3;
//int 자료형 4개 크기의 tmp 라는 포인터를 선언하고 메모리 할당
int *tmp = malloc(4 * sizeof(int));
if (tmp == NULL)
return 1;
// list의 값을 tmp로 복사
for (int i = 0; i < 3; i++)
tmp[i] = list[i];
// tmp배열의 네 번째 값도 저장
tmp[3] = 4;
// list의 메모리를 초기화
free(list);
// list가 tmp와 같은 곳을 가리키도록 지정
list = tmp;
// 새로운 배열 list의 값 확인
for (int i = 0; i < 4; i++)
printf("%i\n", list[i]);
// list의 메모리 초기화
free(list);
}
malloc()
함수를 사용하여 메모리 상의 더 큰 공간을 새로 할당했다.tmp
를 인덱싱해, 각 요소에 기존 배열 list
의 값들을 복사했다.list
가 사용하고 있던 공간은 더 이상 필요가 없으므로, 반드시 free()
함수를 이용해 할당을 해제한다. malloc()
과 값 복사 과정을 한 번에 수행하는 realloc()
함수를 사용할 수 있다.#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *list = malloc(3 * sizeof(int));
if (list == NULL)
return 1;
list[0] = 1;
list[1] = 2;
list[2] = 3;
// tmp 포인터에 메모리를 할당하고 list의 값 복사
int *tmp = realloc(list, 4 * sizeof(int));
if (tmp == NULL)
return 1;
// list가 tmp와 같은 곳을 가리키도록 지정
list = tmp;
// 새로운 list의 네 번째 값 저장
list[3] = 4;
// list의 값 확인
for (int i = 0; i < 4; i++)
printf("%i\n", list[i]);
//list 의 메모리 초기화
free(list);
}
O(n)
의 실행 시간이 소요된다.typedef struct node
{
int number;
struct node *next;
}
node;
node
라는 이름의 구조체는 number
와 *next
두 개의 필드가 함께 정의되어 있다.number
는 각 node
가 가지는 값, *next
는 다음 node
를 가리키는 포인터다.typedef struct
대신에 typedef struct node
라고 node
를 함께 명시해 주는 것은, 구조체 안에서 node
를 사용하기 위함이다.#include <stdio.h>
#include <stdlib.h>
// 연결 리스트의 기본 단위가 되는 node 구조체를 정의
typedef struct node
{
//node 안에서 정수형 값이 저장되는 변수
int number;
//다음 node의 주소를 가리키는 포인터
struct node *next;
}
node;
int main(void)
{
// list라는 이름의 node 포인터를 정의한다. 연결 리스트의 가장 첫 번째 node를 가리킬 것이다.
// 현재에는 아무것도 가리키고 있지 않기 때문에 NULL 로 초기화한다.
node *list = NULL;
// 새로운 node를 위해 메모리를 할당하고 포인터 *n으로 가리킨다.
node *n = malloc(sizeof(node));
if (n == NULL)
return 1;
n->number = 1;
// 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 = 2;
n->next = NULL;
// list가 현재 가리키는 것은 첫 번째 node다.
// 이 (첫 번째) node의 다음 node를 n 포인터로 지정한다.
list->next = n;
n = malloc(sizeof(node));
if (n == NULL)
return 1;
n->number = 3;
n->next = NULL;
// 현재 list는 여전히 첫 번째 node를 가리킨다.
list->next->next = n;
// 메모리를 해제해줄 때에는 list에 연결된 node들을 처음부터 방문하면서 free 한다.
while (list != NULL)
{
// 임시 포인터 변수 tmp 를 선언하여 사용한다.
node *tmp = list->next;
// list가 현재 가리키는 것은 첫 번째 node다.
// tmp에 list->next 를 미리 저장해놓지 않았다면 list가 free되고 나서 나머지 요소들에 접근할 방법이 없어진다.
free(list);
list = tmp;
}
}
배열과 비교해서 연결 리스트는 새로운 값을 추가할 때 다시 메모리를 할당하지 않아도 된다는 장점이 있다.
예를 들어, 연결 리스트에 값을 추가하거나 검색하는 경우 해당 위치까지 연결 리스트의 각 node
들을 따라 이동해야 한다.
O(n)
이 된다.O(log n)
의 실행 시간이 소요 되는 것에 비해서 다소 불리하다.연결리스트
에서의 각 노드 (연결 리스트 내의 한 요소를 지칭) 들의 연결이 1차원적으로 구성되어 있다면, 트리
에서의 노드들의 연결은 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(log n)
이다.O(n)
으로 모든 변수를 다 찾아야 하는 반면 이진 검색 트리를 활용하면 검색 실행 시간이 O(log n)
이 되어 실행 시간 측면에서 효율성을 향상시킬 수 있다.해시 테이블
: 연결 리스트의 배열해시 함수
라는 맞춤형 함수를 통해서 어떤 바구니에 담기는지가 결정된다.해시 테이블
이 된다.O(1)
이다.O(n)
이 될 수도 있다.O(1)
에 가깝다고 볼 수 있다.트라이
: 트리 형태의 자료 구조큐
: 값이 아래로 쌓이는 구조선입 선출
또는 FIFO
라는 방식을 따른다. 즉 가장 먼저 들어온 값이 가장 먼저 나간다.배열
이나 연결 리스트
를 통해 구현 가능스택
: 값이 위로 쌓이는 구조후입 선출
또는 LIFO
라는 방식을 따른다. 즉 가장 나중에 들어온 값이 가장 먼저 나간다.배열
이나 연결 리스트
를 통해 구현 가능딕셔너리
: 키
와 값
이라는 요소로 이루어짐키
에 해당하는 값
을 저장하고 읽어온다. 일반적인 의미에서 해시 테이블
과 동일한 개념이라고도 볼 수 있다.키
를 어떻게 정의할 것인지가 탐색의 효율성에 큰 영향을 미친다.이렇게 부스트캠프의 CS50 강좌를 모두 수강 및 정리해 보았다.
앞 부분은 상당히 기초적인 내용이고, 뒷 부분은 스택이나 큐처럼 가장 흔히 접하게 되는 자료구조에 대해 다소 간단히 설명하고 끝낸 것 같아 추가 학습의 필요성을 느꼈다.
그래도 컴퓨터 과학의 가장 기초적인 흐름에 대해 짧은 시간 안에 간단히 정리하기 좋았던 강좌다.
(벨로그로 옮겨 정리하는 김에 수료증도 발급받아 보았다. 이왕이면 영어로... )