반응형

데이터 베이스 설계시 기본키 설정 조건

 

  • null값은 허용하지 않는다.
  • 유일해야 한다.
  • 변해선 안 된다.

 

 

과거에는 자연 키를 기본 키로 설정하는 것이 정석이었지만, 최근에는 변할 가능성이 적은 대리 키 방식을 선호

 


 

https://www.inflearn.com/questions/27694

 

테이블 설계 관련 문의 및 MongoDB JPA 관련 - 인프런 | 질문 & 답변

안녕하세요 영한님, 며칠 전 복합키 관련 질문을 올렸고,  원하는 답변을 얻을 수 있었습니다. 그리고 추가 질문이 있어서 다시 문의드립니다. 1. 테이블 PK 관련.. 설계를 진행하면서 사수 분 생

www.inflearn.com

 

 


 

반응형
반응형

[알고리즘] 다익스트라 최단 경로 알고리즘 (Dijkstra Algorithm)


다익스트라 알고리즘은 출발지 -> 경유지 -> 목적지를 이용하여 최단 경로를 구하게 된다.

이 과정을 반복하게 되면 고정된 출발지에서 모든 edge의 최단 경로를 구할 수 있다.

 

 

필요 사항

우선 순위 큐 (순서대로 확인하는 것이 아닌, 거리가 가장 짧은 것부터 수행하기 때문에 수행 시간을 단축)

고정된 출발지에서 목적지까지의 경로 값 저장 배열

 


 

최소 비용을 만들기 위해 필요한 Node의 개수는 항상 3개라고 생각해야 한다. [출발, 경유, 목적]

 

출발지 -> 경유지, 경유지 -> 목적지로 따로 생각해야 한다.

경유지를 반복문을 돌리므로 목적지는 복수개다.

 

주목할 부분은 경유지 -> 목적지 인 부분이다.

목적지였던 부분이 다음부터 경유지로 사용이 되어

경유지가 향하는 모든 edge의 비용을 확인한다.

 

퍼져나가는 방식으로 최종적으로 모든 노드의 최소 비용을 알 수 있다.

 

round 1 = 경유지 -> 목적지

round 2 = 경유지 -> 목적지 

round 3 = 경유지 -> 목적지

 

[목적지가 최소 경로 비용으로 처리되면 다음부터는 목적지가 경유지로 쓰임]

 

BFS 동작 방식과 유사하다.

 


 

동작 예시

 

 

다익스트라 알고리즘은 경유지로 가장 먼저 자기 자신을 기준으로 update 한다.

1번 노드에서 3, 4번 노드를 update 시켜줄 수 있다.

[1 -> 1 -> 3]     [1 -> 1 -> 4]

dist배열 = [INF, 0, INF, 6, 3, INF]  // 0번은 편의를 위해 사용하지 않는다.

 

3, 4번 노드가 update 되고,

큐에 3, 4번 노드가 들어갔다.

 

 

 

 

4번 노드의 가중치가 가장 작으므로 우선순위 큐에 의해 가장 먼저 나온다.

4번 노드에서 2, 3번 노드를 update 시켜줄 수 있다.

[1 -> 4 -> 2]      [1 -> 4 -> 3]

dist배열 = [INF, 0, 4, 4, 3, INF] 

 

2, 3번 노드가 update 되고,

큐에 2, 3번 노드가 들어갔다.

 

현재 큐에 3번 노드가 두 번 들어갔다.

이 경우는 먼저 들어간 3번 노드는 가중치가 6으로 dist [3]의 값보다 크므로 사용되지 않고 버려진다.

1 -> 1 -> 3 경로보다 1 -> 4 -> 3 경로가 최소 비용이므로 1 -> 4 -> 3을 사용하면 되기 때문이다.

(update 된 거리보다 크다면, update 한 노드는 큐에 들어간 것이므로 최소가 보장. 버려도 되는 이유)

 

 

 

이 같은 방식으로 시작 노드를 기준으로 퍼져나가며 모든 노드의 최소 비용을 계산한다.

다익스트라 알고리즘은 방향 그래프여야 하며, 거리가 음수면 안된다.

거리가 음수, 그리고 사이클이 존재한다면 거리를 계속 줄이는 음수 사이클이 발생. -무한대 형성

 

 


 

 

public class Dijkstra {
    static int[] dist;
    static int[] path; // 자신의 앞 경로를 갖고 있다. ** 모든 경로를 확인할 수 있다.

    static List<List<Node>> list = new ArrayList<>(); // 간선들

    static final int edgeCount = 5 + 1;
    static final int vCount = 7;

    public static void main(String[] args) {
        //노드 5개
        dist = new int[edgeCount]; // 0 사용안함.
        path = new int[edgeCount];
        //간선 7개
        for(int i = 0; i<=vCount; i++) {
            list.add(new ArrayList<>());
        }

        // (1 - > 5) 2000 쓰는 경로
        list.get(1).add(new Node(4, 2000));
        list.get(1).add(new Node(5, 2000));

        // (1 -> 5) 10 쓰는 경로
        list.get(1).add(new Node(2, 5));
        list.get(2).add(new Node(5, 5));

        // (1 -> 5) 3 쓰는 경로
        list.get(1).add(new Node(3, 1));
        list.get(3).add(new Node(4, 1));
        list.get(4).add(new Node(5, 1));

        
        // TODO: 이 부분 주의 자신은 0으로 만들어야함
        // 자기 자신 빼고 나머지 INF 로 초기화
        Arrays.fill(dist, 100_000_000);
        dist[1] = 0; // 시작

        
        dijk();

        // 결과
        System.out.println(Arrays.toString(dist));
        // 경로
        System.out.println(Arrays.toString(path));
    }

    // TODO: 출발지 1번은 계속 고정
    public static void dijk(){
        Queue<Node> q = new PriorityQueue<>((o1,o2) -> o1.w - o2.w);
        q.add(new Node(1, 0)); // 경유지, 비용 (1 -> 1 -> n)

        while(!q.isEmpty()) { // TODO: 경유지 뽑기 // 1 -> M
            Node mid = q.poll(); // 1과 연결된 경유지
            
            // 과거 ,  현재
            if(mid.w > dist[mid.dest]) // update 된 거리보다 큐에서 뽑은게 더 크면 사용 안하고 버림
                continue;

            // TODO: 경유 -> 도착 꺼내는 부분
            for(Node d : list.get(mid.dest)){
                if(dist[d.dest] > dist[mid.dest] + d.w){ // 갱신이 되는 부분.
                    dist[d.dest] = dist[mid.dest] + d.w;
                    // 갱신이 된 것. 1->n , n->M
                    // (1 -> 도착지)  [[ (1 -> 경유(테이블)  + 경유(테이블) - > 도착지 ]]
                    // 계속 갱신       계속 생신

                    // TODO: 현재 목적지가 다음부터는 경유지로 쓰인다.
                    q.offer(new Node(d.dest, dist[d.dest]));
                    // 갱신된 (1 -> 목적지) 를 넣어준다.
                    // 갱신된걸 넣지 않으면 for 부분을 또 확인하기 때문에 갱신된 값을 넣어주는게 좋다.

                    path[d.dest] = mid.dest; // 길찾기 용도 (자신의 앞 노드)
                }
            }
        }
    }
}

// 경유지로도 쓰고, 목적지로도 쓰임
class Node{
    int dest; // 도착지
    int w; // 가중치

    Node(int dest, int w){
        this.dest = dest;
        this.w = w;
    }

    public String toString(){
        return this.dest + " " + this.w;
    }
}

 

 

 

 

 

 

 

 

 

 


 

 

 

반응형
반응형

[알고리즘] 플로이드 워셜 알고리즘 (Floyd Warshall Algorithm)


 

 

 

플로이드 워셜 알고리즘은 edge에서 edge로 이동할 때 최소비용을 구하는 알고리즘이다.

모든 경로의 최소비용을 구할 수 있다.

 


구현

 

 

플로이드 워셜 알고리즘의 구현은 간단하다.

k, i, j를 사용하는 3중 for문이 있을 때

3중 for문으로 가장 바깥 k를 사용하는 for문은 edge의 총개수만큼 반복한다.

 

k는 중간에 거쳐가는 edge이다.

i는 시작 edge, j는 도착 egde로 사용한다.

 

현재 비용보다 거쳐가는 비용이 더 적다면 거쳐가는 비용을 사용한다.

 

자기 자신으로 가는 경로는 0, 도착이 불가능한 경로는 INF(충분히 큰 값)로 표시한다.

 


이해

 

구현은 간단하지만 이해는 쉽지 않았다.

플로이드 워셜 알고리즘은 가장 바깥 for문

즉, 거쳐가는 k가 계속 증가한다고 생각하면 된다.

(경로 값이 크다면 증가하지 않겠지만, 이해를 위해 증가한다고 생각)

 

[거쳐가는 edge]

 

 시작 -> 0 -> 도착

시작 -> 0->1 -> 도착

시작 -> 0->1->2 -> 도착

시작 ->  0->1->2->3... -> N개의 edge까지 -> 도착

 

단순히 거쳐가는 경우가 증가하게끔 나타냈지만

 모든 순열의 경로를 알 수 있다.

 

시작 -> 0->1 -> 도착

시작 -> 1->0 -> 도착

시작 -> 2->4->1 -> 도착

시작 -> 1->4->2->3->5 -> 도착

...

 


다른예제

 

k는 0번 edge부터 가장 마지막 edge까지  증가하며

i 부터 j까지 가는 모든 경우를 계속 갱신한다.

 

예를 들어

가장 처음 0번을 거쳐가는 경우를 생각하자. (k= 0)

0번을 거치며 2번 edge에서 4번 edge의 경로는 갱신되어

2번 (시작)  ->  0번 (거쳐감)  ->  4번 (도착)

2번 -> 4번 는 현재까지의 최소비용으로 갱신되었다.

 

(k=1)인 경우는 잠시 제쳐두고

그다음 2번을 거쳐가는 경우를 생각하자. (k=2)

그럼 2번을 거쳐가는 경우에서

1번 (시작) -> 2번 (거쳐감) -> 4번 (도착)

을 갱신하려 할 때

 

보기에는 단순히 2번을 거쳐가는 것으로 보이지만 사실

2번 -> 4번

2번 -> 0번 -> 4번 을 사용한다. 이게 현재까지의 최소비용이었기 때문이다.

 

dist[2][4]2->0->4로 update되어있다.

1번 -----------> 2번 ------(0번을 지나는 경로)------> 4번

 

                                            

 


 

결국, 앞에서 구한 최소 경로를 반복적으로 사용하기 때문에

보기완 달리 사실 많은 edge들을 이동하는 경로의 최소 비용을 사용하는 것이다.

 


 

 

반응형
반응형

[데이터베이스] SQL 순서 (문법과 실행 순서)


SQL 문법 순서

 

  • SELECT
  • FROM
  • JOIN
  • WHERE
  • GROUP BY
  • HAVING
  • ORDER BY

 

 


SQL 실행 순서

 

  • FROM
  • JOIN
  • WHERE
  • GROUP BY
  • HAVING
  • SELECT
  • ORDER BY

 

DISTINCT, ORDER BY, LIMIT 등 을 제외하면 SELECT의 실행은 최종적으로 이루어진다.

 

JOIN으로 테이블을 결합하고, WHERE 조건을 적용, 그룹화를 진행한다.

 

그후 형성된 테이블을 SELECT 한다.

반응형
반응형

[JAVA] 자바 배열 array 사용법


자바의 배열

 

기본적으로 배열이란 하나의 공통 타입을 갖는 여러 개의 변수들을 모아서 관리하기 위해 사용합니다.

 

배열 사용

여러 변수들을 라인마다 입력하는 것 보다는 사진처럼 한 줄로 처리하는 게 더 깔끔하고 추후에 관리도 편합니다.

 

배열을 사용하기 위해 선언하는 방식은 자료형뒤에 [ ]를 붙여주시면 됩니다.

변수 뒤에 [ ]를 붙일 수도 있지만, 코드 가독성에서 자료형 뒤에 [ ]를 붙여주는 것이 "해당 자료형으로 선언된 배열이구나."라고 인식하기 더 좋습니다.

 


배열 선언의 방법 3가지

 

배열 선언 방법

방법1은 선언 이후에 초기화를 다음 라인에서 합니다. 주의할 점은 new int [ ] { 요소 1, 요소 2... }를 반드시 작성해주어야 합니다.

방법 2와 방법 3은 동일하며, 방법 3이 간단하여 더 자주 사용합니다.

 

 

 

배열에 요소를 넣지 않고 빈 배열로 크기만 지정해서 사용하는 방법도 있습니다. 크기만 선언하면 요소들은 default로 자동 초기화 됩니다.

int형의 default값은 0이기 때문에 해당 배열은 0으로 모든 요소가 초기화 됩니다.

빈 배열

new int[ ] 내부에 개수를 얼마큼 잡을 것인지 숫자를 넣어주면 됩니다. 10을 넣으면 10개의 int 타입을 저장할 공간을 메모리에 잡습니다.

배열 크기의 확인은 배열변수.length 로 확인할 수 있습니다.

 


배열 사용의 주의점

 

배열은 첫번째 요소가 0번부터 시작합니다. 만약 첫 번째 요소를 찾고 싶다면 배열 변수[0]로 찾을 수 있습니다.

첫 번째 요소가 0이므로 마지막 요소는 배열의 크기-1입니다. 

배열 변수[배열 변수. length - 1]로 마지막 요소를 찾을 수 있습니다.

 

또한 배열 사용의 주의점은 한번 배열의 크기가 결정되면, 이 크기를 변경할 수 없습니다.

그 이유는 배열은 메모리에서 연이어서 생성이 되어있습니다.

배열 변수를 출력해보면 참조값이 나오는데 이 값은 배열 첫 번째 요소의 참조 주소 값입니다.

배열에서 index 위치에 해당되는 값을 (첫 주소의 위치 + 타입 크기 * 찾는 index)로 빠르게 접근할 수 있는 이유입니다. (이어서 연결되어있기 때문)

크기를 변경하면 사용 중인 배열 메모리의 뒷 메모리가 사용하고 있을 수 있기 때문에 배열 크기를 변경할 수 없습니다.

크기를 늘리고 싶다면 새로운 더 넓은 크기의 배열을 선언해서 요소들을 복사해주는 과정을 거쳐야 합니다.

 

 


 

 

반응형

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

[JAVA]자바 PriorityQueue 우선 순위 큐  (0) 2022.03.19
반응형

[데이터베이스] 보이스 코드 정규형 BCNF


 

데이터베이스의 정규화를 공부 중 보이스 코드 정규형의 이해가 쉽지 않았어서 정리하고자 글을 작성합니다.

우선 보이스 코드 정규형의 정의는 이렇습니다.

 

모든 결정자가 후보키(유일, 최소)여야 한다.

 

일단 결정자라 함은 릴레이션 내에서 다른 속성을 결정할 수 있는 것입니다.

예를 들면 (이름, 주민등록번호)로 구성된 릴레이션이 있다고 했을 때, 이름 속성은 동명이인이 있을 수 있으므로 주민등록번호 속성을 결정하지 못합니다.

하지만 주민등록번호 속성은 릴레이션에서 유일하기 때문에 이름 속성을 결정할 수 있습니다.

이렇게 한 속성이 다른 속성을 유일하게 결정할 수 있을 때 결정자라 합니다.

자연스럽게 이름은 주민등록번호의 종속자가 됩니다.

* 주민등록번호(결정자) -> 이름(종속자)

 

이러한 결정자의 역할을 주로 하는 것은 보통 기본키라고 할 수 있습니다.

하지만 보이스 코드 정규형에서 문제가 되는 부분은 이러한 결정자의 역할을 할 수 있는 속성이 후보키가 아니란 말입니다.

 

후보키라는 것은 유일성과 최소성을 갖습니다. 유일성은 릴레이션 내에서 유일하다는 것이고, 최소성은 최소 집합 관계로 키를 구성하는 것을 말합니다.

 

다시 보이스 코드 정규형 정의로 돌아가서 보면, 결정자인데 후보키(유일성, 최소성)를 만족하지 못하고 있는 것입니다.

이 말은 릴레이션에서 결정자 속성이 다른 속성을 결정할 수 있음에도 튜플로 여러 번 등장한다는 말입니다.

그리고 이러한 결정자가 결정하는 속성인 종속자가 기본키의 부분 집합인 경우 이를 제거해야 합니다.

 

 

학번 과목 교수
6014 데이터 베이스 브라운
5813 데이터 베이스 브라운
4831 데이터 베이스  블랙
1231 정보 보안 레드

 

학번과 과목이 기본키로 설정돼있고 한명의 교수는 한개의 과목을 가르친다고 가정했을 시 교수 속성은 결정자입니다.

 

결정자 -> 종속자

브라운 -> 데이터 베이스

블랙 -> 데이터 베이스

레드 -> 정보 보안

 

현재 교수만 알면 과목을 알 수 있습니다. 교수 속성은 결정자입니다.

하지만 결정자인데도 불구하고 교수 속성은 현재 후보키가 아닙니다. (유일성을 만족하지 않는다.)

한 릴레이션 내에서 튜플로 2번 등장했기 때문입니다. (브라운)

후보키의 조건인 유일성을 만족시키지 않습니다. 

 

교수 결정자 속성의 종속자는 과목 속성입니다.

현재 종속자인 과목은 기본키의 부분집합이므로 이 부분을 제거해주어야 합니다.

 

결국 이러한 릴레이션은 보이스 코드 정규형을 적용시켜야 하는 조건을 만족합니다. 

 

 


 

무손실 분해

 

한 릴레이션을 분리했을 때 R1, R2 두가지 릴레이션이 나오게 됩니다.

이때 R1, R2 를 교집합 했을 때 나오는 값은 R1의 키 이거나, R2의 키여야 합니다.

 

(학번,과목), (교수, 과목) 은 교집합의 결과가 과목이고, 과목은 한쪽의 키가 아니기 때문에 무손실 분해가 아닙니다.

 

 

보이스 코드 정규화를 적용하여 무손실 분해 결과로 분리된 테이블은

(학번,교수) , (교수, 과목)가 됩니다.

 

(학번,교수), (교수, 과목) 은 교집합의 결과가 교수이고, 교수는 한쪽 테이블의 기본키이기 때문의 무손실 분해입니다.

 

 


REF. 키의 종류

 

슈퍼키 : 유일하게 식별, 단위는 최소가 아니어도 된다.

후보키 : 유일하게 식별, 최소 단위여야 한다.

 

기본키 : 후보키 중 선정된 키

대체키 : 기본키로 선정되지 않은 후보키

 

대리키 : 가상의 키

외래키 : 다른 릴레이션의 기본키를 참조

 

 

반응형
반응형

[자바] PriorityQueue  우선순위 큐


 

  • 우선순위 큐
  • 시간 복잡도 O(logN)
  • 자바 코드

 


우선순위 큐

 

자바에는 자료구조 heap을 구현한 우선순위 큐 클래스가 있다.

큐를 생각해보면 가장 먼저 들어온 요소가 가장 먼저 나가게 되는데, 우선순위 큐에서는 요소들이

우선순위를 가져서 들어온 순서와 상관없이 우선순위가 높은 요소가 큐에서 가장 먼저 나가게 된다.

이때 나오는 요소는 우선순위가 가장 높은 heap 자료구조의 최상단 root node이다.

 

heap 자료구조는 가장 적은 값이나 가장 큰 값을 항상 최상단 root node에 위치시켜 이 값을 내보내는 자료구조이다.

우선순위가 가장 적은 값을 root node로 두면 최소 힙, 우선순위가 가장 높은 값을 root node로 두면 최대 힙이다.

 

  • 중복값을 허용한다.
  • 완전이진트리이다.

 

우선순위큐는 배열로 구현할 수 있다.

부모 node index = (index - 1) / 2

자식 node index = index * 2 + 1 ,  index * 2 + 2

 

 


우선순위 큐의 시간 복잡도

 

우선순위 큐에서의 시간 복잡도는 삽입, 삭제 시 O(logN)의 시간이 걸린다.

 

삽입시 가장 마지막 위치에 요소를 추가시킨 후 부모 노드와의 비교 후 Swap 과정을 통해 알맞은 위치를 찾아간다.

upHeap

 

삭제 시 root node를 반환 후 가장 마지막 요소를 root node에 위치시킨 후 자손과의 비교를 통해 Swap 과정을 거쳐

알맞은 위치를 찾아간다. 

자손과의 Swap은 둘 중 우선순위가 큰 node와 Swap한다.

downHeap

 

큐와 비슷한 성질을 이용하지만, 큐에서는 front 요소의 반환 시간은 O(1)이다. 하지만 우선순위 큐에서는 가장 우선순위가

높은 값을 가져온 후 완전 이진트리로 다시 만드는 과정인 Swap 과정 때문에 O(logN)이 걸린다.

 

 


자바 코드

 

import java.util.*;
 
public class PriorityQueueT{
    public static void main(String[] args) {
        Queue<Integer> pq= new PriorityQueue<>();
        pq.offer(1);
        pq.offer(2);
        pq.offer(3);
        pq.offer(4);
 
        System.out.println(pq);
        System.out.println(pq.poll());
 
        Queue<Integer> pqr= new PriorityQueue<>(Collections.reverseOrder());
        pqr.offer(1);
        pqr.offer(2);
        pqr.offer(3);
        pqr.offer(4);
 
        System.out.println(pqr);
        System.out.println(pqr.poll());
 
 
        Queue<Item> qi= new PriorityQueue<>();
        qi.offer(new Item(1,"가"));
        qi.offer(new Item(3,"나"));
        qi.offer(new Item(0,"다"));
        qi.offer(new Item(2,"라"));
        qi.offer(new Item(4,"마"));
 
        System.out.println(qi);
        System.out.println(qi.poll());
 
    }
}
 
class Item implements Comparable<Item>{
    int number;
    String name;
 
    Item(int number, String name){
        this.number = number;
        this.name = name;
    }
    @Override
    public int compareTo(Item i){
        return this.number - i.number;
    }
 
    @Override
    public String toString(){
        return number +" "+ name;
    }
}
 
cs

 

출력 결과

 

우선순위 큐를 사용하는 PriorityQueue 클래스를 이용하여 1, 2, 3, 4 요소를 넣었다.

각각 최소 힙을 사용하는 pq와 최대 힙을 pqr을 코드를 작성하고 결과를 봤다.

 

기본형이 아닌 객체 간의 비교를 통해 우선순위 큐를 이용하는 방법은

Item Class를 작성하여 Comparable 인터페이스를 구현하여 compareTo 함수를 통해 비교 후 

Item Class의 인스턴스 필드인 number를 통해 우선순위 큐의 최소 힙을 사용했다.

 

반응형

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

[JAVA] 자바 배열 array 사용법  (0) 2022.04.02
반응형

[JAVA] 알고리즘 DFS [Depth-First Search] 깊이 우선 탐색


DFS [Depth-First Search]는 깊이 우선 탐색이며 깊이를 우선적으로 탐색합니다. 

2차원 배열이나 그래프 탐색에서 한번 방문한 노드는 다시 방문하지 않으며 깊이 탐색을 통해 방문하여 모든 노드를 탐색하는 방법입니다.

BFS와 마찬가지로 모든 노드를 탐색하는 방법 중 하나입니다.

 

  • DFS - 재귀, stack 자료 구조 활용
  • BFS - Queue 자료 구조 활용

 

DFS를 코드로 구현하기 위해선 3가지가 필요합니다.

  • 탐색할 2차원 배열 또는 그래프
  • 방문처리를 하기 위한 boolean type 
  • Stack 자료 구조 또는 재귀 함수
1
2
static boolean[] visited = new boolean[9]; // 방문 노드 체크
static int[][] graph = {{}, {2, 3, 8}, {1, 6, 8}, {1, 5}, {5, 7}, {3, 4, 7}, {2}, {4, 5}, {1, 2}}; // 그래프
cs

방문체크를 위한 boolean 배열과 그래프를 선언합니다.

추가로 Stack 자료 구조를 활용하기보다는 재귀 함수를 통해서 구현해보도록 하겠습니다.

 

이제 깊이 탐색을 위한 DFS함수를 작성해봅시다.

1
2
3
4
5
6
7
8
9
    public static void dfs(int x) {
        visited[x] = true
        sb.append(x).append(" "); // StringBuilder 방문 순서 결과를 보기 위해 사용
        for (int i = 0; i < graph[x].length; i++) {
            if (!visited[graph[x][i]]){
                dfs(graph[x][i]);
            }
        }
    }
cs

 

가장 먼저 DFS의 매개변수로 들어온 int x 노드부터 깊이 탐색을 진행합니다.

x 노드를 다시 방문하지 않도록 방문처리를 가장 먼저 처리해주도록 하겠습니다. 

 

x 노드와 연결된 노드들을 확인하기 위해 for (int i = 0; i < graph [x]. length; i++) 반복문을 사용하여

x 노드와 연결된 모든 노드들을 확인해줍니다.

 

반복을 통해 하나씩 확인해가며  if (! visited [graph [x][i]]) 이용하여 이미 방문이 돼있다면 true,

방문하지 않았다면 false 이므로 방문하지 않아야만 재귀 함수를 통해 해당 노드를 DFS 인자로 넣은 후 호출해줍니다.

 

여기서 생각해야 할 점은 재귀 함수를 호출하여 제어를 넘겨주었으므로

for 반복문은 재귀 함수가 완전히 처리된 이후에 다시 제어를 얻어 진행하던 반복을 진행하게 됩니다.

호출한 함수가 완료되야 제어를 얻어 다음 노드로 반복을 진행하게 됩니다.

이러한 재귀 함수를 이용하는 점이 바로 Stack 자료 구조 방식으로 깊이 탐색을 가능하도록 해줍니다.

 

간단하게  말하자면, 해당 dfs함수에서 재귀함수를 호출하면 현재 진행하는 반복이 멈춘후 재귀함수를 먼저 진행한다. 라고 생각하시면 됩니다.

 

 


전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DFS_graph {
    static boolean[] visited = new boolean[9]; 
    static int[][] graph = {{}, {238}, {168}, {15}, {57}, {347}, {2}, {45}, {12}}; 
    static StringBuilder sb = new StringBuilder();
 
    public static void dfs(int x) {
        visited[x] = true
        sb.append(x).append(" ");
        for (int i = 0; i < graph[x].length; i++) {
            if (!visited[graph[x][i]]){
                dfs(graph[x][i]);
            }
        }
    }
 
    public static void main(String[] args) {
        dfs(1);
        System.out.println(sb);
    }
}
 
cs

 

반응형
반응형

[JAVA] 알고리즘 BFS [Breadth-First Search] 너비 우선 탐색


BFS [Breadth-First Search]는 너비 우선 탐색이며 너비를 우선적으로 탐색합니다. 

2차원 배열이나 그래프 탐색에서 한번 방문한 노드는 다시 방문하지 않으며 주변을 순차적으로 방문하여 모든 노드를 탐색하는 방법입니다.

 

모든 노드를 탐색하는 알고리즘으로 깊이 우선 탐색을 하는 DFS와는 달리 Stack을 이용하지 않으며 Queue를 이용하여 전개할 수 있습니다.

  • DFS - 재귀, stack 자료 구조 활용
  • BFS - Queue 자료 구조 활용

 

BFS를 코드로 구현하기 위해서 3가지를 우선적으로 구현해놓아야 합니다. 

  • 탐색할 2차원 배열 또는 그래프
  • 방문처리를 하기 위한 boolean type 
  • Queue 자료 구조

 

1
2
3
    static int[][] graph = {{}, {238}, {168}, {15}, {57}, {347}, {2}, {45}, {12}}; // 탐색할 그래프
    static boolean[] visited = new boolean[graph.length]; // 방문처리를 위해 사용
    static Queue<Integer> q = new LinkedList<>(); // Queue
cs

 

graph는 탐색할 전체 노드의 구조입니다. visited는 방문처리를 위해 사용합니다. Queue는 너비 탐색을 하기위한 자료구조입니다.

Queue 자료구조를 모르신다면 Queue의 동작원리와 JAVA API를 통해 해당 클래스의 함수를 공부하고 보시길 바랍니다.

 

굳이 static으로 처리할 필요는 없습니다.

여러 함수에서 사용될 것이라 예상되면 매개변수로 넘겨주는 방법 대신에 클래스 변수로 처리하여

모든 함수에서 바로 사용할 수 있게끔 하는 편입니다.

 


앞서 선언된 3가지를 이용해서 BFS함수를 작성하도록 하겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    public static void BFS(int n){
        q.offer(n); // 시작 지점
        visited[n] = true;
        
        while (!q.isEmpty()) {
            int x = q.poll();
            sb.append(x).append(" ");
 
            for (int y : graph[x]) {
                if (!visited[y]) {
                    q.offer(y); // 큐에 넣어서 저장이 됐으니.
                    visited[y] = true// 해당 지점은 방문처리
                }
            }
        }
    }
cs

BFS의 구현 코드 자체는 어렵지 않습니다.

2줄) BFS(int n)으로 넘어온 첫 방문 노드를 Queue에 넣어줍니다. 그럼 현재 Queue에는 첫 방문 노드가 입력되어 들어갑니다. 

3줄) 첫 방문 노드는 Queue에 넣어두었으므로 방문처리를 처리해줍니다.

 

첫 방문 노드가 Queue에 있으므로, 이제부터 Queue가 빌 때까지 while 반복문을 돌려줄 수 있습니다.

5줄) while (! q.isEmpty()) 반복문의 종료 조건은 Queue가 비는 순간 종료됩니다.

6줄) 이제 반복문 안에서 Queue에 입력된 노드 하나를 poll() 함수를 통해 가져옵니다.

7줄) StringBuiler의 sb.append(x). append(" ")는 방문 순서를 BFS함수가 종료되고 난 후 출력을 보기 위함입니다.

 

9줄) for (int y : graph [x])  현재 Queue에서 가져온 노드[x]와 연결되어있는 모든 노드들을 순차적으로 확인합니다.

10줄) if (!visited [y]) 만약 방문처리가 false라면 방문하지 않았다는 의미이므로 

11줄) q.offer(y)   Queue에 넣어줍니다.

12줄) visited[y] = true 해당 노드는 Queue에 들어가는 순간 방문처리를 해주어 다시 방문하지 않도록 처리합니다. 

 

 

BFS가 너비탐색이 되는 이유는 Queue 자료구조의 특징인 먼저 들어간 요소가 먼저 나온다는 점 때문입니다.

첫 방문노드와 연결된 노드들을 반복문으로 Queue에 순차적으로 넣은 후 해당 노드들을 poll() 함수를 통해 가장 먼저 꺼내

우선적으로 처리하기 때문에 너비를 먼저 탐색하게 되는것입니다.

가장 최근에 방문처리한 노드는 Queue에서 순서가 맨 마지막에 있게 됩니다.

 


 

전체 코드

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
import java.util.*;
 
public class BFS_graph {
    static int[][] graph = {{}, {238}, {168}, {15}, {57}, {347}, {2}, {45}, {12}};
    static boolean[] visited = new boolean[graph.length];
    static Queue<Integer> q = new LinkedList<>();
    static StringBuilder sb = new StringBuilder();
 
    public static void main(String[] args) {
        System.out.print("방문순서: ");
        BFS(1);
        System.out.println(sb);
    }
 
    public static void BFS(int n){
        q.offer(n); 
        visited[n] = true;
        
        while (!q.isEmpty()) {
            int x = q.poll();
            sb.append(x).append(" ");
 
            for (int y : graph[x]) {
                if (!visited[y]) {
                    q.offer(y);
                    visited[y] = true
                }
            }
        }
    }
}
 
cs

 

 

 

 

반응형

+ Recent posts