자바스크립트 코딩테스트 강좌, 트리의 지름 구하기

1. 문제 설명

트리의 지름은 트리에서 가장 긴 경로의 길이를 의미합니다. 하나의 트리는 서로 연결된 노드 집합으로, 루트 노드를 기준으로 하여 각 노드가 가지를 이룹니다.
트리의 지름을 구하는 문제는 그래프 이론에서 자주 등장하는 문제로, 주로 깊이 우선 탐색(DFS)이나 너비 우선 탐색(BFS)을 이용하여 해결됩니다.
이 강좌에서는 트리의 지름을 구하는 방법과 이를 자바스크립트를 사용하여 구현하는 방법을 살펴보겠습니다.

2. 예제 입력 및 출력

입력

        7
        1 2 3
        1 3 4
        2 4 3
        2 5 2
        3 6 1
        3 7 3
        

출력

7

3. 문제 접근 방법

트리의 지름을 구하는 한 가지 방법은 다음과 같은 두 번의 DFS 또는 BFS를 사용하는 것입니다:

  1. 무작위의 노드에서 시작하여 가장 멀리 떨어진 노드를 찾습니다. 이 노드를 임시 노드라고 하겠습니다.
  2. 임시 노드에서 시작하여 다시 DFS 또는 BFS를 수행하여 가장 멀리 떨어진 노드까지의 거리를 구합니다. 이 거리가 트리의 지름입니다.

4. 자바스크립트 구현

이제 위의 방법을 사용하여 자바스크립트로 트리의 지름을 구하는 코드를 작성해보겠습니다. 시작하기에 앞서, 간단한 데이터 구조를 정의해야 합니다.


    class TreeNode {
        constructor(value) {
            this.value = value;
            this.children = [];
        }
    }

    class Tree {
        constructor() {
            this.root = null;
        }

        addEdge(parentValue, childValue) {
            const parentNode = this.findNode(this.root, parentValue);
            const childNode = new TreeNode(childValue);
            if (parentNode) {
                parentNode.children.push(childNode);
            } else {
                this.root = parentNode; // If root is null, set it as the first node
            }
        }

        findNode(node, value) {
            if (!node) return null;
            if (node.value === value) return node;
            for (const child of node.children) {
                const result = this.findNode(child, value);
                if (result) return result;
            }
            return null;
        }

        diameter() {
            return this.diameterHelper(this.root).diameter;
        }

        diameterHelper(node) {
            if (!node) return { height: 0, diameter: 0 };

            let maxHeight1 = 0, maxHeight2 = 0, diameter = 0;

            for (const child of node.children) {
                const { height, diameter: childDiameter } = this.diameterHelper(child);
                diameter = Math.max(diameter, childDiameter);
                if (height > maxHeight1) {
                    maxHeight2 = maxHeight1;
                    maxHeight1 = height;
                } else if (height > maxHeight2) {
                    maxHeight2 = height;
                }
            }

            diameter = Math.max(diameter, maxHeight1 + maxHeight2);
            return { height: maxHeight1 + 1, diameter };
        }
    }
    

5. 코드 설명

위 코드는 트리의 지름을 구하는 기능을 포함합니다. 주요 부분을 설명하겠습니다.

  • TreeNode 클래스: 트리의 각 노드를 나타냅니다. 노드 값과 노드의 자식 리스트를 포함합니다.
  • Tree 클래스: 트리를 관리하는 클래스입니다. 자식 노드를 추가하는 addEdge 메서드와 주어진 값을 가진 노드를 찾는 findNode 메서드가 포함되어 있습니다.
  • diameter 메서드: 트리의 지름을 구하기 위해 diameterHelper 함수를 호출합니다.
  • diameterHelper 함수: 재귀적으로 각 노드에서 최대 높이를 계산하고, 현재 노드에서의 지름을 갱신합니다.

6. 성능 분석

이 알고리즘의 시간 복잡도는 트리의 모든 노드를 한 번씩 방문하므로 O(n)입니다.
공간 복잡도는 스택 프레임이 최대 트리 높이까지 사용되므로 최악의 경우 O(h), 여기서 h는 트리의 높이를 의미합니다.

7. 결론

자바스크립트를 사용하여 트리의 지름을 구하는 방법을 알아보았습니다. 이와 같은 유형의 문제는 코딩 테스트에서 자주 출제되므로
예제 문제를 많이 풀어보며 익숙해지는 것이 좋습니다.
트리 구조와 DFS/BFS 탐색 방법을 이해하고 활용하는 것이 중요하며, 이러한 기본 개념은 다양한 곳에 응용될 수 있습니다.

자바스크립트 코딩테스트 강좌, 순열의 순서 구하기

문제 설명

주어진 숫자들로 만들 수 있는 순열 중에서 특정 순서의 순열을 구하는 문제가 있습니다.

예를 들어, 숫자 1, 2, 3이 주어졌을 때 만들 수 있는 모든 순열은 다음과 같습니다:

  • 123
  • 132
  • 213
  • 231
  • 312
  • 321

문제는 특정 숫자의 순서를 몇 번째로 가지는지를 구하는 것입니다. 위의 예시에서 231은 4번째 순서에 해당합니다.

입력 형식

첫 번째 줄에 숫자의 개수 n (1 ≤ n ≤ 10)이 주어집니다.

두 번째 줄에 n개의 자연수가 주어집니다. (각 숫자는 1부터 n까지의 서로 다른 자연수입니다.)

세 번째 줄에 원하는 순열의 인덱스 k (1 ≤ k ≤ n!)이 주어집니다.

출력 형식

원하는 순열을 출력합니다.

문제 풀이

접근 방법

이 문제를 해결하기 위해 우리는 다음의 단계를 정의합니다:

  1. 모든 숫자 조합을 생성하여 순열 리스트를 만듭니다.
  2. 순열 목록에서 원하는 순서의 순열을 찾습니다.

순열 생성 알고리즘

여기서는 자바스크립트를 이용하여 순열을 생성하고 특정 순서를 찾는 과정을 설명하겠습니다. DFS (깊이 우선 탐색) 방법을 사용하여 순열을 생성할 것입니다.

자바스크립트 코드

        
function getPermutations(arr) {
    const result = [];
    
    const backtrack = (current, remaining) => {
        if (remaining.length === 0) {
            result.push(current);
            return;
        }
        
        for (let i = 0; i < remaining.length; i++) {
            const newCurrent = [...current, remaining[i]];
            const newRemaining = [...remaining.slice(0, i), ...remaining.slice(i + 1)];
            backtrack(newCurrent, newRemaining);
        }
    };
    
    backtrack([], arr);
    return result;
}

function findPermutation(n, nums, k) {
    const permutations = getPermutations(nums);
    return permutations[k - 1].join('');
}

// 예시 입력
const n = 3;
const nums = [1, 2, 3];
const k = 4;

// 출력
console.log(findPermutation(n, nums, k)); // 231
        
    

코드 설명

위의 코드는 두 개의 함수로 구성되어 있습니다:

  • getPermutations: 주어진 배열의 모든 순열을 생성합니다.
  • findPermutation: 원하는 인덱스에 해당하는 순열을 반환합니다.

getPermutations 함수 상세 설명

이 함수는 재귀적인 방식으로 순열을 생성합니다:

  • 현재 배열의 요소 중 하나를 선택하고, 선택된 요소를 현재 조합에 추가합니다.
  • 선택된 요소를 제외한 나머지 요소들로 새로운 배열을 만들어 재귀 호출을 진행합니다.
  • 모든 요소가 선택될 때까지 이 과정을 반복하고, 완성된 순열을 결과에 추가합니다.

findPermutation 함수 상세 설명

이 함수는 다음과 같은 과정을 거칩니다:

  1. 주어진 숫자 배열에 대해 모든 순열을 생성합니다.
  2. 생성된 순열 배열에서 k-1 인덱스에 해당하는 순열을 찾아 문자열로 반환합니다.

시간 복잡도

이 알고리즘의 시간 복잡도는 O(n!)입니다. 모든 순열을 생성하기 때문에 숫자의 개수가 커질수록 연산 계산 시간이 매우 길어질 수 있습니다. 그러나 n값이 10 이하로 제한되었으므로 실용적인 수준에서 문제를 해결할 수 있습니다.

마무리

이제 순열을 만들고 특정 순서의 순열을 찾는 방법을 익혔습니다. 코딩테스트에서 자주 등장하는 문제 유형 중 하나이니, 연습을 통해 충분히 숙달하시기 바랍니다.

다음 시간에는 다른 알고리즘 문제를 가지고 다시 찾아뵙겠습니다. 감사합니다!

자바스크립트 코딩테스트 강좌, 문자열 찾기

문제 설명

주어진 문자열 text와 찾고자 하는 문자열 pattern이 있습니다.
이때 text 내에서 pattern이 가장 처음으로 나타나는 위치를
반환하는 함수를 작성하세요. 만약 patterntext 내에 존재하지 않는다면 -1을 반환합니다.

입력 예시

  • text: “hello world”
  • pattern: “world”

출력 예시

  • 결과: 6 (문자열 “world”가 인덱스 6부터 시작함)

문제 해결 전략

이 문제를 해결하기 위해 우리는 문자열 내에서 특정 패턴이 존재하는지 확인하고,
존재한다면 그 시작 인덱스를 찾아야 합니다. 문자열 탐색은 다양한 알고리즘이 있지만,
이 강좌에서는 가장 기본적이고 직관적인 방법인 ‘브루트 포스(Brute Force)’ 방법과
좀 더 효율적인 ‘KMP(카라츠바-모리셀)’ 알고리즘을 사용하여 문제를 해결해보겠습니다.
먼저 브루트 포스 방법을 살펴보겠습니다.

브루트 포스 접근법

브루트 포스 방법은 주어진 문자열과 찾고자 하는 패턴의 모든 조합을 비교하는 방식입니다.
이 방법은 간단하고 이해하기 쉽지만, 최악의 경우 시간 복잡도가 O(n*m)입니다. 여기서 n은
텍스트의 길이, m은 패턴의 길이입니다.

알고리즘 단계

  1. 텍스트의 길이(n)와 패턴의 길이(m) 변수를 설정합니다.
  2. 텍스트의 시작 위치를 하나씩 증가시키면서, 현재 위치에서부터 패턴과 비교합니다.
  3. 모든 문자 비교가 일치하면, 현재 시작 인덱스를 반환합니다.
  4. 모든 문자를 비교하고 패턴이 발견되지 않으면 -1을 반환합니다.

자바스크립트 코드 구현


function findFirstOccurrence(text, pattern) {
    const n = text.length;
    const m = pattern.length;

    for (let i = 0; i <= n - m; i++) {
        let j;
        for (j = 0; j < m; j++) {
            if (text[i + j] !== pattern[j]) {
                break;
            }
        }
        if (j === m) {
            return i; // 패턴이 발견된 경우
        }
    }
    return -1; // 패턴이 존재하지 않음
}

// 테스트 예시
console.log(findFirstOccurrence("hello world", "world")); // 6
console.log(findFirstOccurrence("hello world", "abc")); // -1
    

KMP 알고리즘

브루트 포스 방법은 간단하지만 비효율적일 수 있습니다. KMP 알고리즘은
검색 중에 불필요한 재검사를 방지하여 성능을 개선합니다. KMP 알고리즘의 기본 개념은
‘패턴의 일부가 매칭될 때, 나머지 부분을 재사용’하는 것입니다.

KMP 알고리즘 원리

KMP 알고리즘은 ‘부분 일치 테이블(또는 실패 함수)’을 활용하여 문자열 검색을
최적화합니다. 이 테이블은 검색 중 캐시할 수 있는 정보를 제공합니다.
KMP 알고리즘은 시간 복잡도가 O(n + m)으로, 이렇게 향상된 성능을 보여주므로
큰 데이터셋에 대해서 유용합니다.

알고리즘 단계

  1. 패턴에 대한 부분 일치 테이블을 생성합니다.
  2. 텍스트와 패턴을 비교하면서 일치하지 않을 경우, 테이블을 참조하여
    패턴의 위치를 지정합니다.
  3. 일치할 때까지 반복하며, 최종적으로 인덱스를 반환합니다.

부분 일치 테이블 생성

부분 일치 테이블을 생성하는 알고리즘은 다음과 같습니다. 이 테이블은 이전까지
검사했던 문자열에서 동일한 접두사와 접미사를 기반으로 하여 다음 비교 시
인덱스를 조정하는 데 사용됩니다.


function buildKMPTable(pattern) {
    const m = pattern.length;
    const lps = new Array(m).fill(0);
    let len = 0; 
    let i = 1;

    while (i < m) {
        if (pattern[i] === pattern[len]) {
            len++;
            lps[i] = len;
            i++;
        } else {
            if (len !== 0) {
                len = lps[len - 1];
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }
    return lps;
}
    

KMP 알고리즘 코드 구현


function KMPSearch(text, pattern) {
    const n = text.length;
    const m = pattern.length;
    const lps = buildKMPTable(pattern);
    let i = 0; // 텍스트 인덱스
    let j = 0; // 패턴 인덱스

    while (i < n) {
        if (pattern[j] === text[i]) {
            i++;
            j++;
        }
        if (j === m) {
            return i - j; // 패턴을 찾음
        } else if (i < n && pattern[j] !== text[i]) {
            if (j !== 0) {
                j = lps[j - 1];
            } else {
                i++;
            }
        }
    }
    return -1; // 패턴을 찾지 못함
}

// 테스트 예시
console.log(KMPSearch("hello world", "world")); // 6
console.log(KMPSearch("hello world", "abc")); // -1
    

결론

이 강좌에서는 문자열 찾기 문제를 해결하기 위한 두 가지 알고리즘,
브루트 포스 방법과 KMP 알고리즘의 구현 방법을 살펴보았습니다.
브루트 포스 방법은 직관적이고 간단하지만, 큰 문자열을 검색할 때는 비효율적일 수 있습니다.
반면 KMP 알고리즘은 더욱 효율적으로 패턴을 검색할 수 있는 방법을 제공합니다.
실제 코딩 테스트에서는 이러한 다양한 알고리즘을 이해하고 적절하게 활용하는 것이 중요합니다.

문자열 찾기와 관련된 문제는 코딩 테스트에서 자주 출제되므로, 다양한 예제를 풀어보며
경험을 쌓는 것이 필요합니다. 앞으로도 다양한 알고리즘 문제를 학습하여
역량을 더욱 향상시켜보시기 바랍니다.

자바스크립트 코딩테스트 강좌, 평균 구하기

문제 설명

주어진 숫자들의 배열이 있을 때, 이 숫자들의 평균을 계산하는 함수를 작성하세요.
평균은 모든 숫자의 합을 숫자의 개수로 나눈 값입니다.
배열이 비어있을 경우에는 예외 처리를 하여 적절한 메시지를 반환해야 합니다.

문제 예시

        입력: [1, 2, 3, 4, 5]
        출력: 3

        입력: []
        출력: "배열이 비어 있습니다."
    

알고리즘 접근 방법

이 문제를 해결하기 위해, 다음과 같은 단계를 거칩니다:

  1. 입력 배열이 비어 있는지를 확인합니다.
  2. 배열의 각 요소를 순회하며 합계를 계산합니다.
  3. 배열의 길이를 구하여, 평균을 계산합니다.
  4. 최종적으로 구한 평균 값을 반환합니다.

자바스크립트 코드 구현

이제 각 단계를 자바스크립트로 구현해보겠습니다.


function calculateAverage(numbers) {
    // 입력 배열이 비어 있는지 확인
    if (numbers.length === 0) {
        return "배열이 비어 있습니다.";
    }
    
    // 합계를 저장할 변수
    let sum = 0;
    
    // 배열의 각 요소를 순회하며 합계 계산
    for (let i = 0; i < numbers.length; i++) {
        sum += numbers[i];
    }
    
    // 평균 계산
    const average = sum / numbers.length;
    
    // 평균 반환
    return average;
}

// 예시 테스트
console.log(calculateAverage([1, 2, 3, 4, 5])); // 출력: 3
console.log(calculateAverage([])); // 출력: "배열이 비어 있습니다."
    

문제 해결 과정 상세 설명

1단계: 입력 배열 확인

첫 번째 단계에서는 주어진 배열이 비어 있는지를 확인합니다.
만약 배열의 길이가 0이라면, 함수는 즉시 “배열이 비어 있습니다.”라는 문자열을 반환합니다.
이는 사용자가 입력 배열을 잘못 지정했을 경우에 대한 예외 처리입니다.

2단계: 합계 계산

배열이 비어있지 않다면, 다음 단계로 넘어가 합계를 계산합니다.
여기서는 sum이라는 변수를 0으로 초기화한 다음, 배열의 각 요소를 순회하면서
그 값을 합산합니다. 배열의 길이는 numbers.length로 확인할 수 있습니다.

3단계: 평균 계산

합산이 완료되면, 배열의 길이로 합계를 나누어 평균 값을 계산합니다.
이 과정에서 const average = sum / numbers.length;와 같이 계산식을 작성할 수 있습니다.
평균은 소수 부분을 포함할 수 있으므로, 별도로 소수점 이하의 자리수를 조정할 필요가 없으면 이렇게 간단히 반환하면 됩니다.

4단계: 결과 반환

마지막 단계에서는 계산된 평균 값을 반환합니다.
이 값은 호출한 쪽에서 출력할 수 있도록 console.log나 다른 방법으로 활용할 수 있습니다.

결과 및 복습

이와 같이 평균을 구하는 알고리즘은 배열의 길이가 0인지 확인하는 예외 처리와
순회하여 합을 구하는 단순한 방법으로 구현됩니다.

복습해보면, 평균을 구하는 과정은 괄호 내 모든 숫자를 더한 후, 그 값을
숫자의 수로 나누어서 도출합니다.
이 과정에서 예외 상황을 처리하는 것은 실제 코딩 테스트에서 매우 중요하므로
항상 유의해야 합니다.

어려움 극복하기

이 문제를 해결하는 동안 고려해야 할 점은 아래와 같습니다.

  • 입력 배열이 항상 숫자로 이루어져 있는지 확인 필요
  • 예외 처리 시 반환할 메시지 또는 값을 일관성 있게 정의
  • 숫자가 아닌 요소가 포함된 경우 처리 방법 고려

코딩 테스트를 진행할 때, 위와 같은 예외 상황들을 항상 염두에 두어야
문제가 발생할 확률을 줄일 수 있습니다.

마무리

평균 구하기 문제는 간단하지만, 다양한 예외 상황과 조건을 고려해야
하므로 항상 주의 깊게 접근해야 합니다. 연습을 통해 알고리즘을 보다
효과적으로 구현할 수 있을 것입니다.

혹시 더 궁금한 사항이나 의문이 있다면 댓글로 남겨주세요!
다음 시간에는 다른 알고리즘 문제를 가지고 찾아뵙겠습니다.

자바스크립트 코딩테스트 강좌, 거짓말쟁이가 되긴 싫어

코딩테스트는 많은 개발자들이 겪는 어려운 과정입니다. 특히 자바스크립트로 문제를 해결해야 할 때는 언어의 특성을 잘 이해하고 있어야 합니다. 이번 강좌에서는 ‘거짓말쟁이가 되긴 싫어’라는 문제를 통해 자바스크립트의 특성과 알고리즘 접근 방식을 살펴보겠습니다.

문제 설명

다음과 같은 상황을 상상해보세요. 당신은 여러 친구와 함께 캠프를 가고 있습니다. 친구들 중 일부는 괴짜들이어서, 어떤 특정 사건에 대해서는 거짓말을 하기로 결심했습니다. 다음은 각 친구들이 이야기한 내용입니다.

주어진 입력은 배열로 표현되며, 각 배열의 값은 친구들이 주장하는 친구의 숫자입니다. 당신의 목표는 주장에 맞는 친구의 수가 다수일 때, 그 친구들이 거짓말을 하고 있다고 판단하는 것입니다.

예시 입력

    const statements = [
        [0, 1], // 친구 0은 친구 1을 주장
        [1, 2], // 친구 1은 친구 2를 주장
        [2, 0], // 친구 2는 친구 0을 주장
        [3, 2], // 친구 3은 친구 2를 주장 (여기서 친구 3은 거짓말쟁이다)
    ];
    

예시 출력

거짓말을 하는 친구의 수: 1

문제 해결 과정

이 문제에 접근하는 첫 번째 단계는 각 친구가 누군가를 주장하는 관계를 이해하는 것입니다. 여기서는 유향 그래프를 사용하여 각각의 친구를 노드로, 주장하는 관계를 간선으로 나타낼 수 있습니다.

1. 데이터 구조 설계

우선, 각 친구에 대한 주장을 저장할 수 있는 자료구조를 정의해야 합니다. 이를 위해 객체를 사용하여 친구의 ID와 그 친구가 주장하는 친구의 ID의 목록을 맵핑할 수 있습니다.

    const graph = {};
    statements.forEach(([speaker, listener]) => {
        if (!graph[speaker]) {
            graph[speaker] = [];
        }
        graph[speaker].push(listener);
    });
    

2. DFS/BFS를 통한 탐색

주장 간의 관계를 탐색하기 위해 DFS(깊이 우선 탐색) 혹은 BFS(너비 우선 탐색)를 사용할 수 있습니다. 이렇게 하면 각 친구의 주장이 유효한지를 확인할 수 있습니다.

    function hasContradictions(speaker) {
        const visited = new Set();
        const stack = [speaker];

        while (stack.length) {
            const curr = stack.pop();
            if (visited.has(curr)) {
                return true; // 이미 방문한 노드를 다시 방문할 경우 모순 발생
            }
            visited.add(curr);

            if (graph[curr]) {
                graph[curr].forEach(listener => {
                    stack.push(listener);
                });
            }
        }
        return false;
    }
    

3. 전체 친구에 대해 확인

모든 친구를 확인하여, 주장이 유효하지 않은 친구의 수를 셉니다. 전체 친구의 수중 몇 명이 모순을 만드는지를 파악하는 과정입니다.

    let liarsCount = 0;
    for (let i = 0; i < statements.length; i++) {
        if (hasContradictions(i)) {
            liarsCount++;
        }
    }
    return liarsCount;
    

최종 코드

    function findLiars(statements) {
        const graph = {};
        statements.forEach(([speaker, listener]) => {
            if (!graph[speaker]) {
                graph[speaker] = [];
            }
            graph[speaker].push(listener);
        });

        function hasContradictions(speaker) {
            const visited = new Set();
            const stack = [speaker];

            while (stack.length) {
                const curr = stack.pop();
                if (visited.has(curr)) {
                    return true; 
                }
                visited.add(curr);

                if (graph[curr]) {
                    graph[curr].forEach(listener => {
                        stack.push(listener);
                    });
                }
            }
            return false;
        }

        let liarsCount = 0;
        for (let i = 0; i < statements.length; i++) {
            if (hasContradictions(i)) {
                liarsCount++;
            }
        }
        return liarsCount;
    }

    console.log(findLiars(statements)); // 출력: 1
    

결론

위와 같은 문제를 통해 자바스크립트의 기본 문법, 자료구조 활용, 그리고 DFS/BFS 알고리즘을 적용하는 법을 배웠습니다. 코딩테스트 준비 시 이러한 문제들을 연습하여 알고리즘적 사고를 키우는 것이 중요합니다.