반응형

병합 정렬

: 대표적인 분할 정복 방법을 채택한 알고리즘으로 O(NlogN)의 시간 복잡도를 가지는 정렬 알고리즘

 

특징

-. 일단 반으로 정확히 나누고 나중에 합치는 방식으로, 퀵 정렬(Quick Sort)와 다르게 Pivot 값이 존재하지 않는다.

-. 기존 데이터를 담을 추가적인 배열이 필요하다는 점에서 메모리 활용은 비효율적이다.

-. 초기 단계에는 모두 원소가 1인 상태로 분할되어있으며, 한 번 병합할 때마다 2의 배수만큼 씩 병합한다.

-. 너비는 N이지만 높이는 logN이기 때문에 전체 시간 복잡도는 NlogN이다.

더보기

너비? 높이?

너비: i와 j를 비교해 더 작은 값을 k 위치에 저장

        N번의 시도 후 정렬 완료

높이: 너비 정렬을 총 logN 번 반복하면 정렬 완료

 

병합 정렬(Merge Sort) Sequence

 

코드

#include <iostream>
using namespace std;
#define number 8

int sorted[8]; // 정렬된 배열. 반드시 전역 변수로 선언.

void merge(int a[], int m, int middle, int n){
    // m: 정렬할 배열의 시작 지점
    // middle: 정렬할 배열의 중간 지점
    // n: 정렬할 배열의 끝 지점

    int i = m;
    int j = middle+1;
    int k = m;

    // 작은 순서대로 배열에 삽입
    while(i<=middle && j <= n){
        // i가 j보다 더 작으면 새로운 배열에 저장
        if(a[i] <= a[j]){
            sorted[k] = a[i];
            i++;
        }
        // 그렇지 않으면 j 값 저장
        else{
            sorted[k] = a[j];
            j++;
        }
        k++; // 한 번 저장하면 k 증가
    }

    // 남은 데이터 삽입
    // i가 먼저 끝났다면 j 모두 새로운 배열에 저장
    if(i > middle){
        for(int t = j; t <= n; t++){
            sorted[k] = a[t];
            k++;
        }
    }
    // j가 먼저 끝났으면 i 모두 새로운 배열에 저장
    else{
        for(int t = i;t<=middle;t++){
            sorted[k] = a[t];
            k++;
        }
    }

    // 정렬된 배열을 실제 배열에 삽입
    for(int t = m; t <= n; t++){
        a[t] = sorted[t];
    }
}

void merge_sort(int a[], int m, int n){
    // 크기가 1보다 큰 경우 return
    if(m < n){
        int middle = (m+n) / 2;
        merge_sort(a, m, middle); // middle을 기준으로 왼쪽 병합정렬
        merge_sort(a, middle+1, n); // middle을 기준으로 오른쪽 병합정렬
        merge(a, m, middle, n); // 정렬 후 합치기
    }
}

int main(void){
    int arr[number] = {7, 6, 5, 8, 3, 5, 9, 1};
    merge_sort(arr, 0, number-1);
    for(int i=0;i<number;i++)
        cout << arr[i] << " ";
    cout << endl;
    return 0;
}

출력

1 3 5 5 6 7 8 9

 

시간 복잡도

병합정렬은 정확히 반씩 나눈다는 점에서 최악의 경우에도 NlogN을 보장한다.

☞ 퀵 정렬이 가지는 한계(Pivot 값에 따라 편향되게 분할할 가능성이 있다는 점에서 최악의 경우 O(N^2)의 시간 복잡도를 가짐)를 보완할 수 있음.

반응형

'Algorithm' 카테고리의 다른 글

[Algorithm] 힙 정렬(Heap Sort)  (0) 2023.04.09
[Algorithm] C++ STL sort 함수  (0) 2023.04.08
[Algorithm] 기초 정렬 문제  (0) 2023.04.08
[Algorithm] 퀵 정렬(Quick Sort)  (0) 2023.04.08
[Algorithm] 삽입 정렬(Insertion Sort)  (0) 2023.04.03
반응형

백준 사이트

초심자라면 단계별로 풀어보기, 알고리즘 분류에서 문제 풀어보는 게 좋다.

그 중에서도 단계별로 풀어보기에서 정렬해보기부터 시작하면 좋다.

 

기본적으로 1초에 1억 번 정도 연산한다고 생각하고 문제 풀면 됨.

 

1. 수 정렬하기(2750)

https://www.acmicpc.net/problem/2750

 

2750번: 수 정렬하기

첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000)이 주어진다. 둘째 줄부터 N개의 줄에는 수가 주어진다. 이 수는 절댓값이 1,000보다 작거나 같은 정수이다. 수는 중복되지 않는다.

www.acmicpc.net

데이터의 개수가 최대 1,000개. N^2 = 1,000,000개

N^2 알고리즘으로 풀어도 크게 문제 없이 해결 가능하다.

#include <iostream>
using namespace std;

void Print(int *data, int N){
    for(int i=0;i<N;i++)
        cout << data[i] << endl;
}

void Selection_Sort(int *data, int N){
    int tmp, min, idx = 0;
    for(int i=0;i<N;i++){
        min = 1001;
        for(int j=i;j<N;j++){
            if(min > data[j]){
                min = data[j];
                idx = j;
            }
        }

        tmp = data[i];
        data[i] = data[idx];
        data[idx] = tmp;
    }
}

void Bubble_Sort(int *data, int N){
    int tmp = 0;
    for(int i=0;i<N;i++){
        for(int j=0;j<N-1;j++){
            if(data[j] > data[j+1]){
                tmp = data[j];
                data[j] = data[j+1];
                data[j+1] = tmp;
            }
        }
    }
}

int main(void){
    int N;
    int data[1000];

    cin >> N;
    for(int i=0;i<N;i++)
        cin >> data[i];

    Selection_Sort(data, N);
    Bubble_Sort(data, N);
    //Insert_Sort(data, N);
    //Quick_Sort(data, N);
    Print(data, N);
    return 0;
}

 

2. 세수정렬

https://www.acmicpc.net/problem/2752

 

2752번: 세수정렬

숫자 세 개가 주어진다. 이 숫자는 1보다 크거나 같고, 1,000,000보다 작거나 같다. 이 숫자는 모두 다르다.

www.acmicpc.net

#include <iostream>
using namespace std;
#define N 3

int arr[3]={0,};

void Input(){
    for(int i=0;i<N;i++)
        cin >> arr[i];
}

void Output(){
    for(int i=0;i<N;i++)
        cout << arr[i] << " ";
}

void Insertion_Sort(){
    int j, tmp;
    for(int i=0;i<N-1;i++){
        j = i;
        while(arr[j] > arr[j+1]){
            tmp = arr[j];
            arr[j] = arr[j+1];
            arr[j+1] = tmp;
            j--;
        }
    }
}

int main(void){

    Input();
    //Selection_Sort();
    Insertion_Sort();
    Output();

    return 0;
}

 

3. 수 정렬하기 2

https://www.acmicpc.net/problem/2751

 

2751번: 수 정렬하기 2

첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄부터 N개의 줄에는 수가 주어진다. 이 수는 절댓값이 1,000,000보다 작거나 같은 정수이다. 수는 중복되지 않는다.

www.acmicpc.net

#include <iostream>
using namespace std;

int N;
int arr[1000000+10];

void Input(){
    cin >> N;
    for(int i=0;i<N;i++)
        cin >> arr[i];
}

void Output(){
    for(int i=0;i<N;i++)
        cout << arr[i] << "\n";
}

void Quick_Sort(int start, int end){
    
    if(start >= end)
        return;

    int mid = start;
    int i = start+1;
    int j = end;
    int tmp;

    while(i <= j){
        // 오른쪽으로 가면서 pivot값보다 큰 값 찾기
        while(arr[mid] >= arr[i]){
            i++;
        }

        // 왼쪽으로 가면서 pivot값보다 작은 값 찾기
        while(arr[mid] <= arr[j] && start < j){
            j--;
        }

        // 엇갈렸으면 pivot 값과 작은 값 변경
        if(i > j){
            tmp = arr[j];
            arr[j] = arr[mid];
            arr[mid] = tmp;
        }
        // 엇갈리지 않았으면 두 값 변경
        else{
            tmp = arr[j];
            arr[j] = arr[i];
            arr[i] = arr[j];
        }
    }

    // start, end 다시 조정해 재귀
    Quick_Sort(start, j-1);
    Quick_Sort(j+1, end);
}

int main(void){
    Input();
    Quick_Sort(0, N-1);
    Output();
}
반응형
반응형

선택 정렬, 버블 정렬, 삽입 정렬의 한계

: O(N*N)은 데이터 갯수가 10만 개만 넘어가도 일반적인 상황에서 사용하기가 매우 어려운 알고리즘이다.

2^10 = 1,000

2^20 = 1,000,000

log(2)N = (N이 1,000,000일 때) 20

 

퀵 정렬(Quick Sort)

: 대표적인 분할 정복 알고리즘으로 특정한 값을 기준으로 큰 숫자와 작은 숫자를 서로 교환한 후 배열을 반으로 나누는 정렬 알고리즘

더보기

분할 정복

: 배열을 분할해서 계산하는 알고리즘

 

퀵 정렬(Quick Sort) Sequence

더보기

기준값 = 피벗(Pivot)

왼쪽에서 오른쪽으로 이동하면서 Pivot 보다 큰 값 하나 선택

오른 쪽에서 왼쪽으로 이동하면서 Pivot보다 작은 값 하나 선택

작은 값의 index < 큰 값의 index: Pivot과 작은 값 교환

작은 값의 index > 큰 값의 index: 작은 값과 큰 값 교환

 

 

Code

#include <iostream>
using namespace std;

int number = 10;
int Data[10] = {1, 10, 5, 8, 7, 6, 4, 3, 2, 9};

void quickSort_up(int *data, int start, int end){
    // start: 정렬을 수행하는 부분집합의 첫 번째 원소
    // end: 정렬을 수행하는 부분집합의 마지막 원소

    // 원소가 1개인 경우
    if(start >= end){
        return;
    }

    int key = start; // pivot

    int i = start + 1; // pivot 값에서 오른쪽으로 이동하면서 pivot값 보다 큰 값을 찾기 위해 존재하는 index
    int j = end; // 정렬을 수행하는 부분집합의 마지막 원소에서부터 왼쪽으로 이동하면서 pivot 값보다 작은 값을 찾기 위해 존재하는 index
    int tmp;

    // 엇갈리는 경우 while 탈출
    while(i <= j){
        // i번째 원소가 pivot 값 보다 작으면 i 오른쪽으로 이동(pivot 값보다 큰 값을 만날 때 까지 오른쪽으로 이동)
        while(data[i] <= data[key]){
            i++;
        }

        // j번째 원소가 pivot 값 보다 크면 j 왼쪽으로 이동(pivot 값보다 작은 값을 만날 때 까지 왼쪽으로 이동)
        // index가 엇갈렸을 때 작은 값과 pivot 값을 변경해주기 때문에 j는 배열을 벗어나지 못하도록 해주어야 함
        while(data[j] >= data[key] && j > start){
            j--;
        }

        // 엇갈린 경우 pivot값과 현재 data 변경
        if(i > j){
            tmp = data[j];
            data[j] = data[key];
            data[key] = tmp;
        }
        // 엇갈리지 않은 경우 i와 j index의 data 변경
        else{
            tmp = data[j];
            data[j] = data[i];
            data[i] = tmp;
        }
    }

    // 엇갈리는 경우 pivot 값이 정렬되었으므로, pivot 값을 재설정해주어야 함
    quickSort_up(data, start, j-1); // 왼쪽 data
    quickSort_up(data, j+1, end); // 오른쪽 data
}

void quickSort_down(int *data, int start, int end){
    // start: 정렬을 수행하는 부분집합의 첫 번째 원소
    // end: 정렬을 수행하는 부분집합의 마지막 원소

    // 원소가 1개인 경우
    if(start >= end){
        return;
    }

    int key = start; // pivot

    int i = start + 1; // pivot 값에서 오른쪽으로 이동하면서 pivot값 보다 큰 값을 찾기 위해 존재하는 index
    int j = end; // 정렬을 수행하는 부분집합의 마지막 원소에서부터 왼쪽으로 이동하면서 pivot 값보다 작은 값을 찾기 위해 존재하는 index
    int tmp;

    // 엇갈리는 경우 while 탈출
    while(i <= j){
        // i번째 원소가 pivot 값 보다 작으면 i 오른쪽으로 이동(pivot 값보다 큰 값을 만날 때 까지 오른쪽으로 이동)
        while(data[i] >= data[key]){
            i++;
        }

        // j번째 원소가 pivot 값 보다 크면 j 왼쪽으로 이동(pivot 값보다 작은 값을 만날 때 까지 왼쪽으로 이동)
        // index가 엇갈렸을 때 작은 값과 pivot 값을 변경해주기 때문에 j는 배열을 벗어나지 못하도록 해주어야 함
        while(data[j] <= data[key] && j > start){
            j--;
        }

        // 엇갈린 경우 pivot값과 현재 data 변경
        if(i > j){
            tmp = data[j];
            data[j] = data[key];
            data[key] = tmp;
        }
        // 엇갈리지 않은 경우 i와 j index의 data 변경
        else{
            tmp = data[j];
            data[j] = data[i];
            data[i] = tmp;
        }
    }

    // 엇갈리는 경우 pivot 값이 정렬되었으므로, pivot 값을 재설정해주어야 함
    quickSort_down(data, start, j-1); // 왼쪽 data
    quickSort_down(data, j+1, end); // 오른쪽 data
}

int main(void){
    quickSort_up(Data, 0, number-1); // 오름차순
    for(int i=0;i<10;i++){
        cout << Data[i] << " ";
    }
    cout << endl;
    quickSort_down(Data, 0, number-1); // 내림차순
    for(int i=0;i<10;i++){
        cout << Data[i] << " ";
    }
    return 0;
}

출력

1 2 3 4 5 6 7 8 9 10
10 9 8 7 6 5 4 3 2 1

 

만약 아래 배열을 정렬한다면?

선택 정렬의 시간 복잡도: N^2 = 100

퀵 정렬의 시간 복잡도: 

1 2 3 4 5 => 5 * 5 = 25

6 7 8 9 10 => 5 * 5 = 25

-> 25 + 25 = 50

∴ O(NlogN)의 시간복잡도를 갖는 알고리즘을 이용하면 100만개 정도의 데이터는 어렵지 않게 정렬이 가능하다.

 

시간복잡도

평균 : N*logN

최악의 경우: N^2

 

만약 이미 오름차순 정렬되어있는 배열을 퀵정렬을 이용해 오름차순 정렬한다면?

=> 분할 정복의 이점을 사용하지 못하고 있으므로, 이런 경우에는 insertion sort를 사용하는 게 효율적이다.

 

 

반응형
반응형

삽입 정렬(Insertion Sort)

: 각 숫자를 적절한 위치에 삽입하는 정렬 방법으로, 정렬 대상 숫자를 기준으로 앞의 원소들은 이미 정렬이 되어 있다고 가정한다.

적절한 위치?
선택 정렬(Selection Sort)와 버블 정렬(Bubble Sort)은 무조건 위치를 변경하는 정렬 방식으로, 이미 정렬이 이루어져 있는 상황에서도 반드시 반복을 수행한다.
반면에, 삽입 정렬(Insertion Sort)은 필요할 때만 위치를 변경한다.

 

Insertion Sort Sequence

더보기
정렬 DATA
반복 1
반복 2
반복 3
반복 4
반복 5
반복 6
반복 7
반복 8
반복 9

 

코드

#include <iostream>
using namespace std;

int main(void){
    int i, j, tmp;
    int arr[10] = {1, 10, 5, 8, 7, 6, 4, 3, 2, 9};
    for(i=0;i<9;i++){
        j = i;
        while(arr[j] > arr[j+1]){
            tmp = arr[j];
            arr[j] = arr[j+1];
            arr[j+1] = tmp;
            j--;
        }
    }

    for(i=0;i<10;i++)
        cout << arr[i] << " ";
    return 0;
}

출력

1 2 3 4 5 6 7 8 9 10

 

시간 복잡도 

최악의 경우 : 10 + 9 + 8 + 7 + ... + 1

=> O(N*N)

 

Insertion Sort(삽입 정렬)은 특정 조건에서는 속도가 빠르다.

더보기

특정 조건?

만약 Insertion Sort(삽입 정렬)을 이용해 아래 배열을 정렬한다면?

정렬 DATA
반복 1 - 8
반복 9

→ 거의 정렬된 상태의 배열의 경우 효율적인 정렬 알고리즘으로, 자원 소모가 적으며 연산 자체도 적게 수행한다.
    (= Quick Sort / Heap Sort / Merge Sort와 비슷하거나 더 빠르게 정렬 수행)

∴ Insertion Sort(삽입 정렬)는 O(N^2)의 시간 복잡도를 가지는 정렬 알고리즘 중 가장 강력한 알고리즘이다.

 

 

 

반응형

'Algorithm' 카테고리의 다른 글

[Algorithm] 기초 정렬 문제  (0) 2023.04.08
[Algorithm] 퀵 정렬(Quick Sort)  (0) 2023.04.08
[Algorithm] 버블 정렬(Bubble Sort)  (0) 2023.04.02
[Algorithm] 선택 정렬(Selection Sort)  (0) 2023.04.02
DFS/BFS  (0) 2022.04.07
반응형

버블 정렬(Bubble Sort)

: 옆에 있는 값과 비교해 더 작은 값을 반복적으로 앞으로 보내는 정렬 방법

 

버블 정렬 Sequence

더보기
정렬 DATA
반복 1
반복 2
반복 3
반복 4
반복 5
반복 6
반복 7
반복 8
반복 9

한 번 반복이 끝났을 때 가장 큰 값이 가장 마지막 위치로 이동

 

코드

#include <iostream>
using namespace std;

int main(void){
    int i, j, tmp;
    int arr[10] = {1, 10, 5, 8, 7, 6, 4, 3, 2, 9};
    for(i=0;i<10;i++){
        for(j=0;j<9-i;j++){ // i for문이 1번 반복될 때마다 가장 마지막 위치의 원소는 채워지기 때문에 idx 9-i까지만 확인
            if(arr[j] > arr[j+1]){
                tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
            }
        }
    }

    for(i=0;i<10;i++){
        cout << arr[i] << " ";
    }
    return 0;
}

출력

1 2 3 4 5 6 7 8 9 10

 

시간 복잡도

10 + 9 + 8 + ... + 1

= 10 * (10 - 1) / 2

= N * (N - 1) / 2

= O(N^2)

 

실제로 실행시켜보면 Bubble Sort가 Selection Sort보다 더 느리게 작동한다.
: Bubble Sort는 당장 옆에 있는 것과 비교해 자리를 바꾸는 연산을 수행하며, 자리를 바꾸는 연산은 총 3번의 연산을 필요로 하기 때문에 컴퓨터가 실질적으로 수행해야하는 명령어가 더 많기 때문이다.
반면에 선택 정렬은 가장 작은 수를 찾아 마지막에 한 번만 자리를 바꾸는 연산을 수행한다.

참고로, 정렬 알고리즘 중 가장 느린 건 Bubble Sort이다.
반응형

'Algorithm' 카테고리의 다른 글

[Algorithm] 퀵 정렬(Quick Sort)  (0) 2023.04.08
[Algorithm] 삽입 정렬(Insertion Sort)  (0) 2023.04.03
[Algorithm] 선택 정렬(Selection Sort)  (0) 2023.04.02
DFS/BFS  (0) 2022.04.07
[Algorithm] 구현(Implementation)  (0) 2022.04.06
반응형

선택 정렬(Selection sort)

: 가장 작은 것을 선택해서 가장 앞으로 보내는 알고리즘

 

1 10 5 8 7 6 4 3 2 9
1 10 5 8 7 6 4 3 2 9
1 2 5 8 7 6 4 3 10 9
1 2 3 8 7 6 4 5 10 9
1 2 3 4 7 6 8 5 10 9
1 2 3 4 5 6 8 7 10 9
1 2 3 4 5 6 8 7 10 9
1 2 3 4 5 6 7 8 10 9
1 2 3 4 5 6 7 8 10 9
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10

 

코드

#include <iostream>
using namespace std;

int main(void){
    // 선택 정렬: 가장 작은 것을 선택해서 가장 앞으로 보내자
    int i, j, min, idx, tmp;
    int array[10] = {1, 10, 5, 8, 7, 6, 4, 3, 2, 9};
    for(i=0;i<10;i++){
        min = 9999;
        for(j=i;j<10;j++){
            if(min > array[j]){
                min = array[j];
                idx = j;
            }
        }
        tmp = array[i];
        array[i] = array[idx];
        array[idx] = tmp;
        cout << array[i] << " ";
    }
    return 0;
}

출력

1 2 3 4 5 6 7 8 9 10

 

시간 복잡도

10 + 9 + 8 + ... + 1

= 10 * (10 - 9) / 2

=> N * (N - 1) / 2

∴ O(N^2)

반응형

'Algorithm' 카테고리의 다른 글

[Algorithm] 삽입 정렬(Insertion Sort)  (0) 2023.04.03
[Algorithm] 버블 정렬(Bubble Sort)  (0) 2023.04.02
DFS/BFS  (0) 2022.04.07
[Algorithm] 구현(Implementation)  (0) 2022.04.06
[Algorithm] Greedy  (0) 2022.04.05
반응형

1. 탐색(Search) 알고리즘

: 많은 양의 Data 중에서 원하는 Data를 찾는 과정

e.g., DFS, BFS ☞ 코딩 테스트에서 매우 자주 등장하는 유형

 

2. 스택(Stack) 자료구조

: 먼저 들어온 데이터가 나중에 나가는 선입후출(FILO, First In Last Out), 후입선출(LIFO, Last In First Out)의 자료구조

입구와 출구가 동일한 형태로 시각화

stack = []

# 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
stack.append(5) # append(), pop()의 시간 복잡도는 O(1)
stack.append(2)
stack.append(3)
stack.append(7)
stack.pop()
stack.append(1)
stack.append(4)
stack.pop()

print(stack) # 최하단 원소부터 출력
print(stack[::-1]) # 최상단 원소부터 출력
[5, 2, 3, 1]
[1, 3, 2, 5]

 

3. 큐(Queue) 자료구조

: 먼저 들어온 데이터가 먼저 나가는 선입선출(FIFO, First In First Out), 후입후출(LILO, Last In Last Out)의 자료구조

입구와 출구가 모두 뚫려있는 터널과 같은 형태로 시각화

from collections import deque

# 큐(Queue) 구현을 위해 deque 라이브러리 사용
# deque : Stack과 Queue의 장점을 모두 채택한 것으로 데이터를 넣고 빼는 속도가 List
#         자료형에 비해 효율적이며 queue 라이브러리를 이용하는 것 보다 간단
#         대부분의 코딩 테스트에서 collections 모듈과 같은 라이브러리 사용을 허용
queue = deque()

# 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
stack.append(5) # append(), leftpop()의 시간 복잡도는 O(1)
stack.append(2)
stack.append(3)
stack.append(7)
stack.popleft()
stack.append(1)
stack.append(4)
stack.popleft()

print(queue) # 먼저 들어온 순서대로 출력
queue.reverse() # 다음 출력을 위해 역순으로 바꾸기
print(queue) # 나중에 들어온 원소부터 출력
print(list(queue)) # List 자료형으로 변환
deque([3, 7, 1, 4])
deque([4, 1, 7, 3])
[4, 1, 7, 3]

 

4. 재귀함수(Recursive Fuction)

: 자기 자신을 다시 호출하는 함수

최대 재귀 깊이 초과

실제로 컴퓨터 시스템 상에서 함수가 재귀적으로 호출되면 컴퓨터 시스템의 Stack 프레임에 함수가 반복적으로 쌓여 가장 마지막에 호출된 하뭇가 처리된 후에 이 함수를 불렀던 함수가 처리되는 방식으로 수행된다.
☞ 실제로는 Stack과 같은 형태로 동작한다.
    = 자료구조 안에 함수에 대한 정보가 차례대로 담겨 컴퓨터 메모리에 올란간다.
☞ 컴퓨터 메모리는 한정된 크기 만큼의 자원을 가지고 있어 무작정 함수가 종료되지 않고 쌓아올려 재귀적으로 호출만 하게 되면 빠르게 메모리가 가득 차서 문제가 발생할 수 있으므로 재귀 깊이에 제한을 걸어야 한다.

만약, 제한 없이 재귀함수를 호출하고자 한다면?
① 재귀 제한을 느슨하게 만드는 방법
② 별도로 Stack 자료구조를 이용해 Stack 개체를 따로 만들고 그것을 이용하는 방법

 

▷ '재귀 함수를 호출합니다.'라는 문자열을 무한히 출력하는 예제

더보기
def recursive_fuction():
	print('재귀 함수를 호출합니다')
	recursive_function()

recursive_function()
재귀 함수를 호출합니다.
...
재귀 함수를 호출합니다.
Traceback (most recent call last):
  File "main.py", line 12, in <module>
    recursive_function()
  File "main.py", line 10, in recursive_function
    recursive_function()
  File "main.py", line 10, in recursive_function
    recursive_function()
  File "main.py", line 10, in recursive_function
    recursive_function()
  [Previous line repeated 992 more times]
  File "main.py", line 9, in recursive_function
    print('재귀 함수를 호출합니다.')
RecursionError: maximum recursion depth exceeded while calling a Python object
☞ 최대 재귀 깊이 초과 메시지 출력
☞ Python에서는 기본적인 재귀를 호출하는 과정에서 깊이 제한이 있어 별다른 설정을 하지
	 않고 함수를 재귀적으로 호출하면 오류 메시지가 나올 수 있음

 

4.1. 재귀함수의 종료 조건

: 재귀함수를 문제 풀이에 사용하는 경우 재귀함수의 종료 조건을 반드시 명시해야 한다.

☞ 종료 조건을 제대로 명시하지 않으면 함수가 무한히 호출될 수 있다.

 

▷ 종료 조건을 포함한 재귀 예제

더보기
def recursive_function(i):
	# 100번 째 호출을 했을 때 종료되도록 종료 조건 명시
	if i == 100:
		return
	print(i, '번째 재귀함수에서', i+1, '번째 재귀함수를 호출합니다.')
	recursive_function(i+1)
	print(i, '번째 재귀함수를 종료합니다.')

recursive_function(1)
1 번째 재귀함수에서 2 번째 재귀함수를 호출합니다.
2 번째 재귀함수에서 3 번째 재귀함수를 호출합니다.
3 번째 재귀함수에서 4 번째 재귀함수를 호출합니다.
4 번째 재귀함수에서 5 번째 재귀함수를 호출합니다.
...
99 번째 재귀함수에서 100 번째 재귀함수를 호출합니다.
99 번째 재귀함수를 종료합니다.
98 번째 재귀함수를 종료합니다.
97 번째 재귀함수를 종료합니다.
96 번째 재귀함수를 종료합니다.
...
2 번째 재귀함수를 종료합니다.
1 번째 재귀함수를 종료합니다.

 

▷ Factorial 구현 예제

더보기
# 반복적으로 구현한 n!
def factorial_iterative(n):
	result = 1
	# 1 부터 n 까지의 수를 차례대로 곱하기
	for i in range(1, n + 1):
		result *= i
	return result

# 재귀적으로 구현한 n!
def factorial_recursive(n):
	if n <= 1: # n이 1 이하인 경우 1을 반환
		return 1
	# n! = n * (n-1)!을 그대로 코드로 작성하기
	return n * factorial_recursive(n - 1)

# 각자의 방식으로 구현한 n! 출력 (n = 5)
print('반복적으로 구현:', factorial_iterative(5))
print('재귀적으로 구현:', factorial_recursive(5))
반복적으로 구현: 120
재귀적으로 구현: 120

 

▷ 최대공약수 계산(유클리드 호제법) 예제

더보기
유클리드 호제법
: 두 개의 자원수에 대한 최대공약수를 구하는 대표적인 알고리즘

두 자연수 A, B(A > B)에 대해 A를 B로 나눈 나머지를 R이라고 하면 A와 B의 최대 공약수는 B와 R의 최대공약수와 같다.
e.g., GCD(192, 162) # GCD: Greatest Common Devisor, 최대 공약수
def gcd(a, b):
	if a % b == 0: # a가 b의 배수인 경우
		return b
	else:
		return gcd(b, a % b)

print(gcd(192, 162))
6

 

4.2. 재귀함수 사용의 유의사항

  • 재귀함수를 잘 활용하면 복잡한 Algorithm을 간결하게 작성할 수 있다.
    ☞ 단, 오히려 다른 사람이 이해하기 어려운 형태의 코드가 될 수 있으므로 신중하게 사요해야 한다.
  • 모든 재귀함수는 반복문을 이용해 동일한 기능으로 구현할 수 있다.
  • 재귀함수가 반복문보다 유리한 경우도 있으며 불리한 경우도 있다.
  • 컴퓨터가 함수를 연속적으로 호출하면 메모리 내부의 Stack Frame에 쌓인다
    ☞ Stack을 사용할 때 구현상 Stack Library 대신 재귀함수를 이용하는 경우가 많다.

 

5. 그래프(Graph)의 기본 구조

  • 노드(Node) = 정점(Vertex)
  • 간선(Edge)
  • 두 Node가 Edge로 연결되어 있다 = 두 Node는 인접하다 (Adjacent)

 

5.1. Graph를 표현하는 방식

① 인접 행렬(Adjacent Matrix)

: 2차원 배열로 Graph의 연결 관계를 표현하는 방식

☞ 연결되어있지 않은 Node끼리는 무한(Infinity)의 비용이라고 작성

INF = 999999999 # 무한의 비용 선언

# 2차원 리스트를 이용해 인접 행렬 표현
graph = [
	[0, 7, 5],
	[7, 0, INF],
	[5, INF, 0]
]

print(graph)
[[0, 7, 5], [7, 0, 999999999], [5, 999999999, 0]]

 

② 인접 리스트(Adjacent List)

: List로 Graph의 연결 관계를 표현하는 방식

☞ 연결 리스트라는 자료 구조를 이용해 구현하며 C++, JAVA와 같은 프로그래밍 언어에서는 별도로 연결 리스트 기능을 위한 표준 라이브러리를 제공하지만 Python은 기본 자료형인 List 자료형이 append()와 같은 Method를 제공하므로 전통적인 Programming 언어에서의 배열과 연결 리스트의 기능을 모두 기본적으로 제공한다.

# 행(Row)이 3개인 2차원 리스트로 인접 리스트 표현
graph = [[] for _ in range(3)]

# 노드 0에 연결된 노드 정보 저장(노드, 거리)
graph[0].append((1, 7))
graph[1].append((2, 5))

# 노드 1에 연결된 노드 정보 저장(노드, 거리)
graph[1].append((0, 7))

# 노드 2에 연결된 노드 정보 저장(노드, 거리)
graph[2].append((0, 5))

print(graph)
[[(1, 7)], [(2, 5), (0, 7)], [(0, 5)]]

 

6. DFS(Depth-First Search, 깊이 우선 탐색)

: Graph에서 깊은 부분을 우선적으로 탐색하는 Algorithm

  • Stack 자료구조 또는 재귀 함수를 이용
    1. 탐색 시작 Node를 Stack에 삽입하고 방문 처리
    2. Stack의 최상단 Node에 방문하지 않은 인접한 Node가 하나라도 있으며 그 Node를 Stack에 넣고 방문 처리
    3. Stack의 최상단 Node에 방문하지 않은 인접한 Node가 없다면 Stack에서 최상단 Node 꺼냄
    4. 더 이상 2번 과정을 수행할 수 없을 때까지 반복

▷ DFS 동작 과정 예시

# DFS 메서드 정의
def dfs(graph, v, visited):
  # 현재 노드를 방문 처리
  visited[v] = True
  print(v, end = ' ')
  # 현재 노드와 연결된 다른 노드를 재귀적으로 방문
  for i in graph[v]:
    if not visited[i]:
      dfs(graph, i, visited)

# 각 노드가 연결된 정보를 리스트 자료형으로 표현(2차원 리스트)
# 인접 리스트 방식으로 그래프 표현
graph = [
  [], # 0번 노드와 인접한 노드
  [2, 3, 8], # 1번 노드와 인접한 노드
  [1, 7],
  [1, 4, 5],
  [3, 5],
  [3, 4],
  [7],
  [2, 6, 8],
  [1, 7]
]

# 각 노드가 방문된 정보를 리스트 자료형으로 표현(1차원 리스트)
visited = [False] * 9

# 정의된 DFS 함수 호출
dfs(graph, 1, visited) # 시작 노드 : 1
1 2 7 6 8 3 4 5

 

7. BFS(Breadth-First Search, 너비 우선 탐색)

: Graph에서 가까운 Node부터 우선적으로 탐색하는 Algorithm​

  • Queue 자료구조 이용
    1. 탐색 시작 Node를 Queue에 삽입하고 방문 처리
    2. Queue에서 Node를 꺼낸 뒤 해당 Node에 방문하지 않은 인접한 Node가 하나라도 있으면 그 Node를 Queue에 넣고 방문 처리
    3. 더 이상 2번 과정을 수행할 수 없을 때까지 반복

▷ BFS 동작 과정 예시

from collections import deque

# BFS 메서드 정의
def bfs(graph, start, visited):
  # 큐(Queue) 구현을 위해 deque 라이브러리 사용
  queue = deque([start])
  # 현재 노드를 방문 처리
  visited[start] = True
  # 큐가 빌 때까지 반복
  while queue:
    # 큐에서 하나의 원소를 뽑아 출력
    v = queue.popleft()
    print(v, end = ' ')
    # 해당 원소와 연결된, 아직 방문하지 않은 원소들을 큐에 삽입
    for i in graph[v]:
      if not visited[i]:
        queue.append(i)
        visited[i] = True

# 각 노드가 연결된 정보를 리스트 자료형으로 표현(2차원 리스트)
graph = [
  [], # 노드 0과 인접한 노드
  [2, 3, 8], # 노드 1과 인접한 노드
  [1, 7],
  [1, 4, 5],
  [3, 5],
  [3, 4],
  [7],
  [2, 6, 8],
  [1, 7]
]

# 각 노드가 방문된 정보를 리스트 자료형으로 표현(1차원 리스트)
visited = [False] * 9

# 정의된 BFS 함수 호출
bfs(graph, 1, visited)
1 2 3 8 7 4 5 6

 

8. DFS vs BFS

DFS와 BFS의 동작 시간

재귀 함수로 DFS를 구현하면 컴퓨터 시스템의 동작 특성상 실제 프로그램의 수행 시간은 느려질 수 있다. 따라서 Stack Library를 이용해 시간 복잡도를 완화하는 테크닉이 필요할 때도 있다. 코딩 테스트에서는 보통 DFS보다는 BFS 구현이 조금 더 빠르게 동작한다.
반응형

'Algorithm' 카테고리의 다른 글

[Algorithm] 삽입 정렬(Insertion Sort)  (0) 2023.04.03
[Algorithm] 버블 정렬(Bubble Sort)  (0) 2023.04.02
[Algorithm] 선택 정렬(Selection Sort)  (0) 2023.04.02
[Algorithm] 구현(Implementation)  (0) 2022.04.06
[Algorithm] Greedy  (0) 2022.04.05
반응형
Python의 시간/메모리 제한

Python 3.7로 Code를 작성할 때 1초에 2,000만 번의 연산을 수행한다고 가정(2020년 기준)
시간 제한 1초
메모리 제한 128 MB
데이터 개수 100만 개
시간 제한이 1초, 데이터 개수가 100만개인 문제가 있다면 일반적으로 시간 복잡도는 이내의 Algorithm을 이용해 문제를 풀어야 한다.
일 때 이기 때문

 

1. 구현(Impletmentation)

: 머릿속에 알고 있는 Algorithm을 Code로 바꾸는 과정

☞ 흔히 Algorithm 대회에서의 구현 유형 문제는 풀이를 떠올리는 것은 쉽지만 Code로 옮기기 어려운 문제를 지칭한다.

e.g., 완전 탐색, 시뮬레이션

완전 탐색(Brute Forcing)
: 가능한 모든 경우의 수를 탐색하는 방법

시뮬레이션(Simulation)
: 문제에서 제시한 Algorithm을 한 단계씩 차례대로 직접 수행해야하는 문제 유형

 

2. 행렬(Matrix)

: 2차원 Data를 일종의 표와 같은 형태로 쉽게 나타내 줄 수 있도록 하는 수학 개념 중 하나 (Programming에서의 2차원 배열, 리스트)

☞ 일반적으로 Algorithm 문제에서의 2차원 공간은 행렬(Matrix)의 의미로 사용되며 Simulation 및 완전 탐색 문제에서는 2차원 공간에서의 방향 벡터가 자주 활용된다.

# 동, 북, 서, 남
dx = [0, -1, 0, 1] # 가로 축(행)
dy = [1, 0, -1, 0] # 세로 축(열)

# 현재 위치
x, y = 2, 2

for i in range(4):
# 다음 위치 -> 동, 북, 서, 남의 방향으로 1번씩 이동
nx = x + dx[i]
ny = y + dy[i]
print(nx, ny)​

 

참고

https://youtu.be/2zjoKjt97vQ

반응형

'Algorithm' 카테고리의 다른 글

[Algorithm] 삽입 정렬(Insertion Sort)  (0) 2023.04.03
[Algorithm] 버블 정렬(Bubble Sort)  (0) 2023.04.02
[Algorithm] 선택 정렬(Selection Sort)  (0) 2023.04.02
DFS/BFS  (0) 2022.04.07
[Algorithm] Greedy  (0) 2022.04.05

+ Recent posts