[개념]

 

이분탐색이란 탐색기법중 하나로 중간값을 기준으로 탐색 범위를 절반씩 줄여가며  탐색한다.

 

따라서 처음부터 끝까지 모두 탐색하는 기법은 worst case의 경우 시간복잡도가 (O(N))이다.

하지만 이분탐색의 경우 탐색범위를 절반씩 줄여나가기 때문에 O(logN)으로 완전탐색보다 빠르다.

 

1. 탐색하고자 하는 배열은 이미 정렬이 되어있다.

2. left, right 값을 통해 중간값인 mid 값을 구한다.

3-1. 찾고자하는 값이 mid 위치에 존재하는 값보다 클 경우 , left를 mid 뒤로 옮긴다.

3-2. 찾고자하는 값이 mid 위치에 존재하는 값보다 작을 경우 , right를 mid 앞으로 옮긴다.

4. 다시 위의 과정을 left가 right 값보다 커질 때까지 반복한다. 

 

 

- 예시

배열의 총 길이는 8이므로 

left =0 , right= 7이되고 mid=3가 된다.

 

 

mid에 존재하는 값이 찾고자 하는 값인 7보다 크므로 right를 mid-1로 update 한다.

이 때 , mid에 존재하는 값보다 찾고자 하는 값인 7이 더 뒤에 있으므로 left를 mid+1로 update 한다.

 

 

마지막으로 left,right,mid 모두 같은 위치에 존재하고 드디어 찾고자 하는 7을 찾을 수 있게 되었다.

 

 

[코드]

 

int main(void)
{
    int arr[8]  = {1,3,7,8,9,15,17,21};
    
    int target = 7;
    int left = 0;
    int right = 7;
    int mid;
    
    while(left <= right)
    {
        mid = (left+right)/2;
        if(arr[mid] < target )  left = mid+1;
        else if( arr[mid] > target) right = mid-1;
        else    return mid;
    }
}

[개념]

 

위상 정렬이란 ? 

 

위상 정렬(Topology Sort) 알고리즘이란 순서가 정해진 작업을 차례대로 수행해야 할 때, 순서를 결정하기 위해 사용하는 알고리즘이다.

 

 

하나의 노드에 들어오기 위해 필요한 노드의 개수를 '진입차수'라고 한다.

위의 예제에서 보면 2번 노드의 진입차수는 13,7에 의해 2가 된다.

 

위상 정렬은 방향 그래프에서 진입차수가 0인 노드가 하나 존재해야 된다. 그리고 해당 노드가 위상 정렬의 시작 노드가 된다.

위상 정렬은 진입차수가 0인 노드를 시작으로 순서대로 queue에 넣어준다. 

 

하나의 방향 그래프에서 여러 가지의 위상 정렬이 가능하다.

 

1. 진입차수가 0인 노드를 큐에 넣는다.

2. 큐에서 꺼내고 해당 노드에 부속된 간선 삭제

1,2번을 계속 반복해가면서 모든 노드가 선택되면 종료된다.

 

첫번째로 진입 차수가 0인 노드 5,7을 큐에 넣는다.

 

 

큐에서 노드를 꺼내고 해당 노드와 연결된 간선을 삭제해준 뒤 큐에 넣는다.

 

 

마찬가지로 큐에서 노드를 꺼내고 해당 노드의 간선을 삭제해준 뒤 연결된 노드를 큐에 넣어준다.

 

 

위와 같은 방법으로 계속해서 반복한다.

 

 

 

 

더이상 큐에 넣을 노드가 없으므로 위상 정렬이 끝나고 아래와 같이 순서가 정렬된 것을 확인할 수 있다.

 

[코드]

 

백준에서 위상 정렬 관려 코드로

아래와 같이 위상 정렬을 구현할 수 있다.

(www.acmicpc.net/problem/2252)

 

#include <iostream>
#include <queue>
#include <vector>

#define MAX 32001

using namespace std;

int N,M;
int Degree[MAX]={0,};

vector <int> v[MAX];


int main(void)
{
    queue <int> q;
    
    scanf("%d %d",&N,&M);
    for(int i=0;i<M;i++)
    {
        int x1,x2;
        scanf("%d %d",&x1,&x2);
        
        Degree[x2]++;
        v[x1].push_back(x2);
    }
    
    for(int i=1;i<=N;i++)    if(Degree[i]==0)    q.push(i);
    
    while(!q.empty())
    {
        int x = q.front();
        q.pop();
        
        printf("%d ",x);
        
        for(int i=0;i<v[x].size();i++){
            int tmp = v[x][i];
            Degree[tmp]--;
            if(Degree[tmp]==0)  q.push(tmp);
        }
    }
    return 0;
}

[개념]

 

 

 

구간 합을 구할 때 사용되는 알고리즘으로 배열의 길이가 짧은 경우 이중 for문을 사용해서 구하면 되지만

배열의 길이가 길어지면 시간 초과가 발생하므로 투 포인터 알고리즘을 사용한다.

 

 

아래 코드에서 보면  N은 배열의 크기,  M은 구하고자 하는 합이다.

구간의 합이 M을 만족하는 경우의 수를 구한다고 하자.

 

start, end 모두 배열의 첫번째부터 시작한다.

 

- M = sum

구간의 합이 M과 같다면 경우의 수 total에 1을 추가한다.

그리고 end를 한칸 뒤로 움직인다.

 

- M < sum

현재 구간의 합이 구하고자 하는 구간의 합보다 크다면 start를 1씩 증가시켜 주면서 구간의 범위를 좁혀준다.

이 때 start가 end보다 커지게 되는 경우 start, end를 같은 위치로 처리한다.

 

- M > sum

현재 구간의 합이 구하고자 하는 구간의 합보다 작다면 end를 한칸 뒤로 움직여 구간의 범위를 넓혀준다.

 

 

[코드]

 

    int start =0 ;
    int end =0;
    int sum =arr[0];
    
    while(start <= end && end < N)
    {
        if(sum < M) sum += arr[++end];
        else if( sum > M)
        {
            sum -= arr[start++];
            if( start > end && start <N){
                end=  start;
                sum= arr[start];
            }
        }
        else if (sum == M)
        {
            total++;
            sum += arr[++end];
        }
    }

[개념]

 

크루스칼 알고리즘은 

모든 노드들을 최소 비용으롤 연결하는 알고리즘이다.

N개의 노드를 N-1개의 간선으로 연결 

 ex) 모든 도시를 도로로 연결할 때 가장 적은 비용으로 연결하는 방법

 

1. 노드 간의 연결 정보를 저장하고 비용을 기준으로 오름차순으로 정렬한다.

2. 비용이 적은 것부터 연결 

이 때 사이클이 발생하지 않도록 주의

==> Union-Find 를 사용해 확인, 노드를 연결할 때 부모가 같다면 이미 연결된 것이므로 제외

3. 현재 간선을 현재  MST 집합에 추가 

 

 

아래 그림과 같이 가중치를 기준으로 오름차순으로 정렬한다.

가중치가 작은 것부터 선택하여 노드를 서로 연결한다.

해당 예제에서는 2번 노드와 5번 노드를 연결한다.

 

(아래 그림에서 방향 그래프로 그렸는데 잘못 그렸습니다. 크루스칼 알고리즘에서는 방향이 없습니다. ) 

 

 

그 다음으로 가중치가 작은 6번 노드와 1번 노드를 연결한다.

 

 

 

 

이제 5->4번을 연결해준다.

 

 

 

다음으로 4->2를 연결해야 하는데

연결하게 되는 경우 사이클이 발생한다.

따라서 연결하지 않고 다음으로 넘어가서 4->3을 연결한다.

 

 

 

 

 

마지막으로 3->6까지 연결하면 5개의 간선 ( N-1)으로 N개의 노드를 모드 연결할 수 있다.

이와 같은 방법으로 MST를 구하는 것을 크루스칼 알고리즘이라고 한다.

 

 

 

[코드]

 

노드가 총 5개라고 하자.

node배열에는 node간의 연결 정보를 저장한다.

처음에는 각 노드에 대해 부모를 자기 자신으로 초기화한다.

vector에는 간선 길이와 node 배열에서의 index를 저장한다.

 

즉 node[0] = {1,2};는 1번 노드와 2번 노드를 연결한 것이다.

또한 v[0]={10,0}에는 0번 index에 대한 간선의 길이가 저장되어있다. 

따라서 1번 노드와 2번 노드 사이의 간선 길이는 10이라는 것을 알 수 있다.

 

노드간의 연결정보를 저장하고  Kruskal 함수에서 간선 길이를 기준으로 오름차순으로 정렬한다.

이제 길이가 짧은 간선부터 각 노드간 연결을 수행할 지 확인한다.

이 때 Find 함수를 통해 각 노드의 parent가 같은지 확인한다.

Parent가 같다는 것은 이미 연결이 된 것이고 , 여기서 또 Union을 하면 Cycle이 발생한다.

따라서 부모가 같은 경우에는 이미 연결이 되었기 때문에 Union을 수행하지 않는다.

 

이와 같이 간선의 길이를 기준으로 짧은 것부터 연결을 하고

마지막에 Find 함수를 통해서 각 노드의 parent를 통해 모든 노드가 연결이 되었는지 확인할 수 있다.

(parent가 하나라도 다르면 모두 연결되지 않을 것)

 

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

struct Node{
    int start, end;
};
Node node[10];
vector <pair<int,int>> v;// len, node number

int parent[10];

int Find(int idx)
{
    if(idx == parent[idx] ) return idx;
    else{
        parent[idx] = Find(parent[idx]);
        return parent[idx];
    }
}

void Union(int idx1 , int idx2)
{
    idx1 = Find(idx1);
    idx2 = Find(idx2);
    
    if(idx1 != idx2)    parent[idx2] = idx1;
    
}

int Kruskal()
{
    int ans =0;
                            
    sort(v.begin(),v.end());
    
    for(int i=0;i<v.size();i++)
    {
        int len = v[i].first;
        int idx = v[i].second;
       
        if(Find(node[idx].start) == Find(node[idx].end))    continue;
        Union(node[idx].start , node[idx].end);
        ans += len;
    }
    
    int num = Find(1);
    for(int i=2;i<=5;i++){
        if(Find(i) != num)  return -1;
    }
    return ans;
}

int main(void)
{
    for(int i=1;i<=5;i++)   parent[i]=i;
    
    node[0] = {1,2};
    node[1] = {1,3};
    node[2] = {2,3};
    node[3] = {2,5};
    node[5] = {3,5};
    node[6] = {4,5};
    node[7] = {3,4};
    node[8] = {1,4};
    
    v.push_back({10,0});
    v.push_back({4,1});
    v.push_back({7,2});
    v.push_back({13,3});
    v.push_back({2,4});
    v.push_back({42,5});
    v.push_back({33,6});
    v.push_back({5,7});
    v.push_back({17,8});
    
    Kruskal();

}

 

크루스칼 알고리즘에 대해 좀더 자세한 내용은 아래 링크를 통해 확인할 수 있습니다.

https://blog.naver.com/ndb796/221230967614

 

17. Union-Find(합집합 찾기)

  Union-Find(유니온-파인드)는 대표적인 그래프 알고리즘입니다. 바로 '합집합 찾기'라는 의미를 ...

blog.naver.com

[개념]

 

이진 탐색은 데이터가 정렬되어 있는 상태에서 특정 데이터를 찾는다.

배열의 시작, 중간, 끝 index를 조절해가며 탐색을 한다.

찾고자 하는 값인 x가 배열의 중간 값보다 작으면 중간을 기준으로 왼쪽에 있는 데이터 중에서만 찾고

x가 배열의 중간 값보다 클 경우 중간을 기준으로 오른쪽에 있는 데이터 중에서만 찾는다.

 

이와 같이 탐색 범위를 좁혀가면서 찾고자하는 값을 찾아낸다.

 

 

 

 

위의 그림은 이진 탐색의 예시로 정렬된 배열을 가지고 시작한다.

찾고자 하는 값인 13은 처음 mid인 7보다 크므로 

low = mid +1이 되고 , mid = (low+high)/2 = 3 이 된다.

arr[3]이 13이므로 true를 반환하면서 탐색을 종료한다.

 

 

[코드]

 

 

아래 코드와 같이 low는 배열의 시작, high는 배열의 끝 index부터해서

중간 값인 mid보다 찾고자하는 값인 target이 더 크면 low값을 mid+1 로 update 하고

target 값이 더 작은 경우 high의 값은 mid-1로 update 하여 탐색하는 범위를 좁혀 나갈 수 있다.

 

bool BinarySearch()
{
    int mid;
    int low =0;
    int high = N-1;
    
    while(low <= high)
    {
        mid = (low + high)/2;
        if(arr[mid] == target)  return true;
        else if(arr[mid] < target)  low = mid +1;
        else    high= mid-1;
    }
    return false;
}

- 선택 정렬 ( Selection Sort)

 

[ 개념 ]

 

선택 정렬은 현재 자신의 index보다 뒤에 있는 index들 중에서 배열의 원소 값이 가장 작은 원소랑 자리를 서로 바꿔줍니다.

이러한 방식으로 배열의 처음부터 끝까지 진행하여 정렬을 할 수 있습니다.

 

아래 그림은 선택 정렬의 예시로 다시 한번 설명해드리겠습니다.

 

먼저 아래와 같이 아직 정렬이 되지 않은 배열이 있습니다.

 

 

첫번째 index부터 시작하여 자신의 값보다 작은 값들 중에서 가장 작은 값을 찾습니다.

해당 배열에서는 '13'보다 작은 값 중에서 제일 작은 '2'를 택하게 됩니다.

제일 작은 값을 찾은 뒤 서로 자리를 swap합니다. 

 

 

 

다음 index로 넘어가 두번째 index를 기준으로 뒤에 있는 값들 중에서 가장 작은 값을 찾습니다.

해당 배열에서는 '7'보다 작은 값 중에서 가장 작은 값인 '3'이 되겠습니다.

마찬가지로 서로 자리를 바꿔줍니다.

 

 

마찬가지로 3번째 index를 기준으로 뒤에 있는 index 중에서 가장 작은 값을 찾습니다.

위의 배열에서는 5가 되고 '13'과 '5'의 위치를 서로 바꿔줍니다. 

 

위와 같은 방식으로 배열의 마지막 index까지 계속 진행합니다.

다음 index에 존재하는 값인 13를 기준으로 뒤에서 가장 작은 값을 찾습니다.

7이 가장 작으므로 '7'과 '13'의 위치를 서로 바꿔줍니다. 

 

 

마지막으로 16과 13까지 위치를 바꿔주면 배열이 오름차순으로 정렬된 것을 확인할 수 있습니다.

 

 

 

[ 코드 ]

 

아래 코드와 같이 selection sort를 구현할 수 있다.

첫번째 index부터 시작하고

idx는 i부터 시작하여 자신보다 작은 값이 없을 경우 자리를 바꿔주지 않을 수 있다. 

 

void selection_sort(int arr[], int size)
{
    int idx =0;
    
    for(int i=0;i<size-1;i++){
        idx = i;
        
        for(int j=i+1;j<size;j++)
            if(arr[j] < arr[idx] )  idx = j;
        
        int tmp = arr[i];
        arr[i]= arr[idx];
        arr[idx]= tmp;
    }
}

 

위의 코드와 같이 전체 비교를 진행하므로 시간 복잡도는 O(N^2)이다.

 

 

- 삽입 정렬 ( Insertion Sort)

 

[개념]


삽입 정렬은 현재 index를 기준으로 앞에 있는 값들 중에서 현재 index에 존재하는 값이 들어갈 위치를 찾아서 삽입한다.

 

1. 현재 값이 들어갈 위치를 찾는다.

 

예시를 통해서 삽입 정렬을 다시 한번 설명하겠습니다.

 

 

먼저 아래와 같이 아직 정렬이 되지 않은 배열이 있습니다.

 

 

삽입 정렬은 현재 위치를 기준으로 앞에 있는 원소들과 비교하므로 배열 index로 1부터 시작합니다.

7을 기준으로 자신보다 앞에 있는 원소들 중에서 삽입될 자리를 찾습니다.

7은 13보다 작으므로 13 앞에 삽입됩니다.

 

 

 

 

다음 배열 값인 2는 7,13 보다 모두 작으므로 

제일 첫번째 자리에 삽입됩니다.

 

 

 

그 다음 4번째 index의 값인 5를 기준으로 5가 들어갈 자리를 찾습니다.

5는 2보다 크고 7보다 작으므로 2와 7사이에 들어갑니다.

 

 

 

5번째 index에 존재하는  16은 처음부터 16까지 모두 오름차순으로 정렬되어 있으므로 다음 index로 넘어갑니다.

 

 

마지막 index에 존재하는 숫자 3은 2보다 크고 5보다 작으므로 2와 5 사이에 삽입됩니다.

 

이와 같이 삽입 정렬을 통해 배열이 오름차순으로 정렬된 것을 확인할 수 있습니다.

 

 

[코드]

 

아래 코드는 c언어를 사용해 insertion sort 함수를 구현한 것이다.

 

void insertion_sort(int arr[], int size)
{
    
    for(int i=1;i<size;i++){
        int j = i-1;
        int key = arr[i];
        
        while(j >= 0 && arr[j] > key){
            arr[j+1] = arr[j];
            j--;
        }
        arr[j+1] = key;
    }
}

 

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

하지만 만약 배열이 처음부터 정렬되어 있었다면 while문을 실행하지 않게 되므로 시간 복잡도는 O(N)이다.

 

 

- 버블 정렬 ( Bubble Sort)

[개념]

버블 정렬은 인접한 두 원소를 비교해 정렬합니다.

1회전을 수행하면 가장 큰 값이 제일 뒤에 위치하게 됩니다. 

 

예시를 통해서 버블 정렬을 다시 한번 설명하겠습니다.

먼저 아래와 같이 아직 정렬이 되지 않은 배열이 있습니다.

 

 

 

위의 그림은 버블 정렬을 1회전 실행한 결과로 가장 큰 값이 제일 뒤에 위치한 것을 확인할 수 있습니다.

 

 

위의 그림은 버블 정렬 2회전을 하고 난 뒤의 결과로 첫번째 index부터 마지막 index까지 모두 정렬된 것을 확인할 수 있습니다.

 

 

 

[코드]

 

 

최상, 평균, 최악의 경우 모두 

N-1, N-2, ... ,1 = N(N-1)/2번 비교를 하게된다.

따라서 시간 복잡도는 O(N^2)이다.

 

void bubble_sort(int arr[], int size)
{
    for(int i=size-1;i>0;i--)
    {
        for(int j=0;j<i;j++)
        {
            if(arr[j] > arr[j+1])
            {
                int tmp=arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
            }
        }
    }
}

[ 유니온 파인드란 ? ]

 

합집합을 찾는 대표적인 알고리즘 

Disjoint-Set이라고도 부른다.

 

- Union : 노드 X가 포함된 집합과 노드 Y가 포함된 집합을 하나로 합친다.

- Find :  노드 X가 포함된 집합을 찾아준다.

 

[ 구현 ]

 

- i는 노드번호, parent[i]는 i 노드의 부모 노드를 저장 

 

 

[그림 1]

[그림 1]과 같이 처음에는 parent[i] 를 자기 자신인  i로 초기화한다.

 

 

 

[그림 2]

Union(1,2) Union(3,4)을 통해 그림 2와 같이 변한다. 

노드 2의 부모 노드는 1이 되고 , 노드 4의 부모 노드는 3이 된다. 

 

 

 

[그림 3]

 

Union(1,3)을 통해 노드 3의 부모 노드는 1이 된다. 

따라서 1,2,3,4가 하나의 집합에 포함되는 것을 확인할 수 있다.

 

 

[ Find 함수 ] 

 

위에서 설명한 것과 같이 parent[i]는 처음에 i로 초기화 한다.

 

	for(int j=0;j<N;j++)
        {
            parent[j] = j;
        }
        

find 함수는 해당 노드의 부모 노드를 return 한다.

 

루트 노드는 자신의 index와 parent[idx]가 같으므로

idx == parent[idx] 를 만족할 때까지 재귀를 통해 find 함수를 호출한다.

 

int find(int idx)
{
    if(idx == parent[idx])  return idx;
    else{
        parent[idx] = find(parent[idx]);
        return parent[idx];
    }
}

 

 

[ Union  함수 ]

 

idx1과 idx2의 부모 노드를 find 함수를 통해서 찾는다.

이 때 indx1 ,idx2 가 다르다면 부모 노드가 다른 것이므로

idx2의 부모 노드를 idx1으로 update해주어 같은 set에 속하게 된다.

 

int unit(int idx1, int idx2)
{
    idx1 = find(idx1);
    idx2 = find(idx2);
    
    if(idx1 != idx2 ) // 서로 부모가 다르다면
    {
        parent[idx2] = idx1;        
    }
}

+ Recent posts