210620. Today I Learned(TIL) : 자료구조(BFS, DFS)

syong·2021년 6월 20일
1

TIL

목록 보기
14/32

BFS(Breadth First Search)

BFS는 '너비 우선 탐색'으로 트리 구조 탐색 방식 중 하나이다.

아래 코드는 방향이 없는 간선들의 목록이 주어질 때, 연결된 정점의 컴포넌트(그룹들)가 몇 개인지 반환하는 함수를 구현한 코드이다. 이 알고리즘은 인접 리스트와 인접 행렬의 두 가지 방식으로 구현할 수 있다.

인접 리스트(레퍼런스)

function connectedVertices(edges) {

	// 최대 버텍스를 찾는다.
	const maxVertex = edges.reduce((a, c) => {
		const bigger = Math.max(...c);
		if (bigger > a) return bigger;
		return a;
	}, 0);

	const adjList = {};

  // 인접 리스트에 최대 버텍스 크기만큼 반복해 버텍스를 만들어 준다.
	for (let i = 0; i <= maxVertex; i++) {
		adjList[i] = [];
	}

  // edges를 순회하며, (무향 그래프이므로 쌍방으로) 간선을 추가한다.
	for (let i = 0; i < edges.length; i++) {
		adjList[edges[i][0]].push(edges[i][1]);
		adjList[edges[i][1]].push(edges[i][0]);
	}

  // 방문한 버텍스를 담을 객체 선언
	const visited = {};
	// 컴포넌트가 몇 개인지 카운트할 변수 선언
	let count = 0;

  // 그래프에 있는 버텍스를 전부 순회한다.
	for (let vertex = 0; vertex <= maxVertex; vertex++) {

		// 만약 i 번째 버텍스를 방문하지 않았다면 bfs로 해당 버텍스와, 버텍스와 연결된(간선) 모든 버텍스를 방문한다.
		// BFS로 갈 수 있는 모든 정점들을 방문하며 visited에 담기 때문에, 방문한 버텍스는 bfs를 돌지 않는다.
		// 컴포넌트 확인
		if (!visited[vertex]) { // 아직 방문하지 않은 버텍스라면?
			// 그래프와 버텍스, 방문했는지 확인할 visited를 bfs 함수에 인자로 넘겨주어 실행한다.
			bfs(adjList, vertex, visited);

			// bfs 함수 실행이 끝나면 카운트
			count++;
		}
	}

  // 카운트를 반환
	return count;
}


// bfs 함수
function bfs(adjList, vertex, visited) {

	// bfs는 가장 가까운 정점부터 탐색하기 때문에 queue를 사용(인접 행렬 길찾기 로직과 같음)
	// queue에 vertex를 담는다.
	const queue = [vertex];
	// 해당 버텍스를 방문했기 때문에 visited에 담아 주고, 방문했다는 표시로 true를 할당한다.
	visited[vertex] = true;

  // queue의 길이가 0일 때까지(queue가 빌 때까지) 순회한다.
	while (queue.length > 0) {

		// cur 변수에 정점을 할당
		// queue는 선입선출이기 때문에 shift 메소드를 사용하여 버텍스를 추출한다.
		const cur = queue.shift();

		// 그래프의 cur 정점에 있는 간선들을 전부 순회한다.
		for (let i = 0; i < adjList[cur].length; i++) {

			// 만약, 해당 버텍스를 방문하지 않았다면 queue에 삽입한다.
			if (!visited[adjList[cur][i]]) {
				queue.push(adjList[cur][i]);
              		// 방문했다는 표시로 visited에 해당 버텍스를 삽입하고 true를 할당한다.
				visited[adjList[cur][i]] = true;
			}

			// queue에 다음으로 방문할 버텍스가 있기 때문에 while은 멈추지 않는다.
			// 만약, queue가 비어 있다면 더 이상 갈 곳이 없는 것이기 때문에 bfs 함수를 종료하고 카운트를 센다.
		}
	}
}

위의 인접 리스트 레퍼런스를 참고하여 인접 행렬로도 구현해볼 수 있었다. 리스트와 코드가 매우 유사하나 세부적인 코드들에서 차이를 보이는 것을 알 수 있다.

인접 행렬

function connectedVertices(edges) {
    // TODO: 여기에 코드를 작성합니다.
    // 인풋값의 정보를 받아서 matrix의 뼈대를 만들고 그 뼈대에 간선도 추가
    // 1. 매트릭스를 만든다
      // 가장 큰 정점을 구함
    let max = Math.max(...edges.flat());
    let matrix = new Array(max+1).fill(0).map(el => new Array(max+1).fill(0));
    
      // 2. 간선 추가
    edges.forEach((el) => {
    const [row, col] = el;
    matrix[row][col] = 1;
    matrix[col][row] = 1;
    });
    
      // 3. count 세기
    const visited = {};
    let count = 0;
    for(let vertex = 0; vertex <= max; vertex++) {
    if(!visited[vertex] /*visited[vertex] === false */) {
        dfs(matrix, vertex, visited);
        count++;
        }
    }
    return count;
};
    
    // bfs 함수
function bfs(matrix, vertex, visited) {
    const enqueue = (q) => queue.push(q);
    const dequeue = () => queue.shift();
    
    let queue = [vertex];
    visited[vertex] = true;

    while(queue.length > 0) {
    let now = dequeue();
    for(let next = 0; next < matrix.length; next++) {
    if(next === now) continue;

    else if(matrix[now][next] === 1 && !visited[next]) {
        visited[next] = true;
        enqueue(next);
            }
        }
    }   
};

DFS는 '깊이 우선 탐색'으로 역시 트리 구조 탐색 방식 중 하나이다. BFS와는 반대되는 개념으로 볼 수 있다.

DFS 방법을 사용하여 위 알고리즘 문제를 동일하게 풀 수 있다. 우선 인접 리스트와 인접 행렬 각각의 케이스에서의 dfs 함수를 살펴보면 다음과 같다.

인접 리스트

// dfs 함수
// 컴포넌트를 세는 로직은 위에 작성한 코드와 동일하므로 생략, bfs 함수 대신 dfs 함수를 호출하면 동일한 결과를 얻을 수 있음.
function dfs(adjList, vertex, visited) {
	// 해당 버텍스에 방문했다는 표시로 visited key에 vertex를 담고 값에 true를 할당
	visited[vertex] = true;

	// 해당 버텍스의 모든 간선들을 전부 순회
	for (let i = 0; i < adjList[vertex].length; i++) {

		// 만약 i번째 간선과 이어진 버텍스를 방문하지 않았다면
		if (!visited[adjList[vertex][i]]) {
			// dfs를 호출해(재귀) 방문한다.
			dfs(adjList, adjList[vertex][i], visited);
		}
		// 모든 방문이 종료되면 다음 버텍스를 확인
		// 재귀가 종료되면(한 정점에 이어진 모든 간선들을 확인했다면) dfs 함수를 종료하고 카운트 한다.
	}
}

여기서 중요하게 뽑을 포인트는 재귀함수를 사용하여 dfs를 구현했다는 점이다. bfs와 다르게 수직 아래로 파고들며 정점들을 탐색해야 하기 때문에 재귀 호출로 이를 구현할 수 있음을 알 수 있다.

인접 행렬

function dfs(matrix, vertex, visited) {
    visited[vertex] = true;

    for(let next = 0; next < matrix.length; next++) {
    if(matrix[vertex][next] === 1 && !visited[next]) {
        dfs(matrix, next, visited);
        }
    }
};

dfs도 bfs와 마찬가지로 인접 리스트 구현과 인접 행렬 구현 코드에서 세부적인 코드의 차이만 존재할 뿐 큰 틀은 같다는 것을 알 수 있다.

BFS vs. DFS

0개의 댓글