C# 코딩테스트 강좌, 트라이

1. 서론

프로그래밍 언어의 성장이 함께하면서, 다양한 자료구조와 알고리즘이 필요하게 되었습니다. 그 중에서 트라이는 문자열을 효율적으로 처리하는데 특화된 자료구조로, 주로 문자열 검색, 자동완성 기능, 접두어 집합 저장 등에 사용됩니다. 이 글에서는 트라이 자료구조를 활용한 알고리즘 문제를 소개하고, C#으로 문제를 풀어보겠습니다.

2. 트라이(Trie) 자료구조의 이해

트라이는 다형적인 속성을 가지며, 노드들은 각 문자를 나타냅니다. 트라이는 다음과 같은 특성을 가지고 있습니다:

  • 모든 단어를 저장할 수 있는 능력.
  • 단어의 접두어를 쉽게 검색할 수 있는 능력.
  • 접두어가 같은 단어를 그룹화할 수 있는 구조.

트라이는 일반적으로 다음과 같은 방식으로 구성됩니다:


class TrieNode {
    Dictionary Children;
    bool IsEndOfWord;

    public TrieNode() {
        Children = new Dictionary();
        IsEndOfWord = false;
    }
}
    
class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    public void Insert(string word) {
        TrieNode current = root;
        foreach (char c in word) {
            if (!current.Children.ContainsKey(c)) {
                current.Children[c] = new TrieNode();
            }
            current = current.Children[c];
        }
        current.IsEndOfWord = true;
    }

    public bool Search(string word) {
        TrieNode current = root;
        foreach (char c in word) {
            if (!current.Children.ContainsKey(c)) {
                return false;
            }
            current = current.Children[c];
        }
        return current.IsEndOfWord;
    }

    public bool StartsWith(string prefix) {
        TrieNode current = root;
        foreach (char c in prefix) {
            if (!current.Children.ContainsKey(c)) {
                return false;
            }
            current = current.Children[c];
        }
        return true;
    }
}
    

3. 문제 소개

이번 강좌에서 다룰 문제는 다음과 같습니다:

문제: 특정 접두사를 가진 단어의 개수 구하기

주어진 단어 목록과 특정 접두사 prefix가 있을 때, 이 접두사로 시작하는 단어의 개수를 반환하는 함수를 작성하세요.

입력:

  • 단어 목록: [“apple”, “app”, “application”, “apricot”]
  • 접두사: “app”

출력: 3 (단어 목록에서 “app”, “apple”, “application”이 있다.)

4. 문제 해결 과정

문제를 해결하기 위해 트라이 자료구조를 사용하여 단어를 삽입하고, 특정 접두사로 시작하는 단어의 개수를 세는 함수를 구현할 것입니다.

1. **단어 삽입**: 먼저 모든 단어를 트라이에 삽입해야 합니다. 이를 통해 각 단어의 문자별로 자식 노드가 생성됩니다.

2. **접두사 검색**: 특정 접두사로 깊이 탐색하면서, 해당 접두사로 시작하는 모든 단어의 개수를 세는 로직을 구현합니다.

3. **결과 반환**: 접두사로 시작하는 단어의 개수를 반환합니다.

4.1 코드 구현

아래는 C#으로 구현한 전체 코드입니다:


class Solution {
    public int CountWordsWithPrefix(string[] words, string prefix) {
        Trie trie = new Trie();
        
        // 모든 단어를 트라이에 삽입
        foreach (string word in words) {
            trie.Insert(word);
        }
        
        // 접두사로 시작하는 단어의 개수를 세기
        return CountWordsStartingWithPrefix(trie, prefix);
    }

    private int CountWordsStartingWithPrefix(Trie trie, string prefix) {
        TrieNode current = trie.Root;
        foreach (char c in prefix) {
            if (!current.Children.ContainsKey(c)) {
                return 0; // 접두사에 해당하는 단어가 없으면 0 반환
            }
            current = current.Children[c];
        }
        return CountAllWords(current); // 접두사 아래의 모든 단어 세기
    }

    private int CountAllWords(TrieNode node) {
        int count = 0;
        
        if (node.IsEndOfWord) {
            count++; // 현재 노드가 단어의 끝이라면 단어 카운트
        }
        
        foreach (var child in node.Children) {
            count += CountAllWords(child.Value); // 모든 자식 노드에 대해 재귀 탐색
        }
        
        return count;
    }
}
    

5. 시간 복잡도 분석

트라이에 단어를 삽입하는 데 걸리는 시간 복잡도는 O(m)입니다. 여기서 m은 단어의 길이입니다.
접두사를 검색하는 데에는 O(k) (k는 접두사의 길이) 시간이 소요됩니다.
따라서 전체 시간 복잡도는 O(n * m + k), 여기서 n은 단어의 개수입니다.

6. 테스트 케이스

다음은 몇 가지 테스트 케이스입니다:


string[] words = {"apple", "app", "application", "apricot"};
string prefix = "app";
Solution solution = new Solution();
Console.WriteLine(solution.CountWordsWithPrefix(words, prefix)); // 출력: 3

words = new string[] {"banana", "bandana", "band"};
prefix = "ban";
Console.WriteLine(solution.CountWordsWithPrefix(words, prefix)); // 출력: 3

words = new string[] {"car", "cart", "cat"};
prefix = "ca";
Console.WriteLine(solution.CountWordsWithPrefix(words, prefix)); // 출력: 3

words = new string[] {"dog", "deer", "dough"};
prefix = "do";
Console.WriteLine(solution.CountWordsWithPrefix(words, prefix)); // 출력: 3
    

7. 결론

이번 글에서는 트라이 자료구조를 이용해 문자열 처리 문제를 해결하는 방법에 대해 알아보았습니다. 트라이는 많은 분야에서 다양한 용도로 사용되며, 문자열 검색과 같은 문제를 해결하기에 아주 유용한 데이터 구조입니다. 트라이의 핵심 아이디어와 함께 C#을 통한 구현을 살펴보았으며, 다양한 테스트 케이스를 통하여 문제 해결 능력을 향상시킬 수 있기를 바랍니다.

C# 코딩테스트 강좌, 리프 노드의 개수 구하기

안녕하세요, 여러분! 오늘은 C#을 이용하여 알고리즘 문제를 해결하는 방법을 배워보겠습니다. 이번 주제는 이진 트리에서 리프 노드의 개수를 구하는 문제입니다. 이 문제를 풀면서 자료구조와 재귀 호출에 대한 이해도를 높일 수 있습니다.

1. 문제 정의

문제는 다음과 같습니다. 주어진 이진 트리가 있을 때, 리프 노드(자식 노드가 없는 노드)의 개수를 구하는 함수를 작성하시오.

예시

  • 입력: 다음과 같은 이진 트리
  •        1
          / \
         2   3
        / \
       4   5
        
  • 출력: 3 (리프 노드: 4, 5, 3)

2. 이진 트리 소개

이진 트리는 각 노드가 최대 두 개의 자식을 가지는 트리 데이터 구조입니다. 이진 트리는 다음과 같은 특성을 가지고 있습니다:

  • 각 노드는 자식 노드(왼쪽 및 오른쪽)를 가질 수 있습니다.
  • 리프 노드는 자식 노드가 없는 노드를 의미합니다.
  • 이진 트리는 재귀적 구조를 가집니다.

3. 접근 방법

리프 노드의 개수를 구하기 위해 재귀 함수를 사용하는 방법을 고려해보겠습니다. 기본적인 접근 방식은 다음과 같습니다:

  1. 현재 노드를 판단합니다. 만약 현재 노드가 null이라면, 0을 반환합니다.
  2. 현재 노드가 리프 노드(즉, 왼쪽 자식과 오른쪽 자식 모두 null)인 경우, 1을 반환합니다.
  3. 현재 노드가 리프 노드가 아니라면, 왼쪽과 오른쪽 서브트리에서 각각 리프 노드의 개수를 재귀적으로 구한 후 합산하여 반환합니다.

4. C# 코드 구현

이제 위에서 설명한 접근 방법에 따라 C#으로 코드를 구현해보겠습니다.

using System;

class TreeNode
{
    public int Val;
    public TreeNode Left;
    public TreeNode Right;

    public TreeNode(int val)
    {
        Val = val;
        Left = null;
        Right = null;
    }
}

class Program
{
    public static int CountLeafNodes(TreeNode root)
    {
        // 1. 현재 노드가 null인 경우
        if (root == null)
            return 0;

        // 2. 현재 노드가 리프 노드인 경우
        if (root.Left == null && root.Right == null)
            return 1;

        // 3. 왼쪽과 오른쪽 서브트리에서 리프 노드의 개수를 구함
        return CountLeafNodes(root.Left) + CountLeafNodes(root.Right);
    }

    static void Main(string[] args)
    {
        // 트리 생성
        TreeNode root = new TreeNode(1);
        root.Left = new TreeNode(2);
        root.Right = new TreeNode(3);
        root.Left.Left = new TreeNode(4);
        root.Left.Right = new TreeNode(5);

        // 리프 노드 개수 출력
        int leafCount = CountLeafNodes(root);
        Console.WriteLine($"리프 노드의 개수: {leafCount}");
    }
}

5. 코드 설명

코드에서 제공된 TreeNode 클래스는 이진 트리의 노드를 정의합니다. 각 노드는 값(Val)과 왼쪽 자식 노드(Left), 오른쪽 자식 노드(Right)를 가집니다. CountLeafNodes 함수는 주어진 노드에 대한 리프 노드의 개수를 재귀적으로 계산합니다.

  • 첫 번째 조건문에서 현재 노드가 null인 경우 0을 반환하여 종료합니다.
  • 두 번째 조건문에서 현재 노드가 리프 노드인 경우 1을 반환합니다.
  • 마지막으로, 왼쪽과 오른쪽 서브트리를 각각 방문하여 결과를 합산하여 반환합니다.

6. 시간 복잡도 분석

이 알고리즘의 시간 복잡도는 O(N)입니다. 여기서 N은 트리의 노드 개수를 나타냅니다. 각 노드를 한 번씩 방문하게 되므로, 선형 시간 복잡도를 가집니다.

7. 추가적인 고려사항

  • 트리가 빈 경우, 즉 루트 노드가 null인 경우에는 리프 노드가 없습니다.
  • 균형 이진 트리와 편향 이진 트리의 경우 모두 문제를 동일하게 처리할 수 있습니다.

8. 결론

이진 트리에서 리프 노드의 개수를 구하는 문제를 해결하는 방법을 배웠습니다. 재귀 함수를 사용하여 간단하게 해결할 수 있으며, 이 과정에서 트리 자료구조에 대한 이해도를 높일 수 있습니다.

다음 시간에는 다른 알고리즘 문제를 다루며 더 나아가겠습니다. 감사합니다!

C# 코딩테스트 강좌, K번째 수 구하기

코딩 테스트에서 자주 출제되는 문제 유형인 ‘K번째 수 구하기’에 대한 자세한 설명과 문제 해결 방법을 소개합니다.

문제 설명

주어진 배열 arr와 정수 K가 있을 때, K번째로 작은 수를 찾아 반환하는 문제입니다. 배열은 0부터 시작하는 인덱스를 가지며, 같은 수가 여러 개 있을 경우, 여러 개의 K번째 수 중 하나를 반환할 수 있습니다.

예를 들어, 배열이 [7, 10, 4, 3, 20, 15]이고 K=3일 경우, 3번째로 작은 수는 7입니다.

입력 형식

입력으로는 다음과 같은 형식이 주어집니다:

  • 첫 번째 줄: 배열의 크기 N (1 <= N <= 10^6)
  • 두 번째 줄: N개의 정수 (배열의 원소는 -10^9 이상 10^9 이하)
  • 세 번째 줄: 정수 K (1 <= K <= N)

입력 예시


6
7 10 4 3 20 15
3
        

출력 형식

배열 내에서 K번째로 작은 수를 출력합니다.

출력 예시


7
        

문제 풀이 과정

1단계: 문제 이해

우리는 그러면 주어진 배열에서 K번째로 작은 수를 찾아야 합니다. 배열이 정렬되어 있지 않기 때문에, 정렬된 상태에서 K번째 원소의 값을 찾아야 합니다.

2단계: 알고리즘 선택

가장 직관적인 방법은 배열을 정렬한 후 K번째 원소를 반환하는 것입니다. 그러나 이 방법은 O(N log N) 시간 복잡도를 가집니다. 이를 최적화하기 위해 다양한 방법이 존재합니다:

  • Quickselect 알고리즘 (평균 O(N) 시간 복잡도)
  • Heap 자료구조를 이용한 방법

이번 강좌에서는 Quickselect 방법을 사용하겠습니다.

3단계: Quickselect 설명

Quickselect는 Quick Sort 알고리즘과 유사하며, 피벗(pivot)을 선택하여 왼쪽 부분과 오른쪽 부분으로 나뉘게 됩니다. 이때 K의 위치에 따라 결과를 도출합니다.

Quickselect의 과정

  1. 중간 피벗값을 선택합니다.
  2. 피벗보다 작거나 같은 값은 왼쪽으로, 큰 값은 오른쪽으로 이동시킵니다.
  3. K가 피벗 인덱스와 동등하면 피벗을 반환합니다.
  4. K가 피벗보다 작으면 왼쪽 부분에 대해 Quickselect를 재귀적으로 호출합니다.
  5. K가 피벗보다 크면 오른쪽 부분에 대해 Quickselect를 재귀적으로 호출합니다.

C# 코드 구현


using System;
using System.Linq;

class KthSmallest
{
    public static int QuickSelect(int[] arr, int left, int right, int k)
    {
        if (left == right) // 배열에 원소가 하나만 있을 경우
            return arr[left];

        Random random = new Random();
        int pivotIndex = left + random.Next(right - left);
        int pivot = arr[pivotIndex];

        // 피벗을 최우측으로 이동
        Swap(arr, pivotIndex, right);
        int storeIndex = left;

        for (int i = left; i < right; i++)
        {
            if (arr[i] < pivot)
            {
                Swap(arr, storeIndex, i);
                storeIndex++;
            }
        }

        Swap(arr, storeIndex, right); // 피벗을 제자리에 놓음

        if (k == storeIndex) return arr[k];
        else if (k < storeIndex) return QuickSelect(arr, left, storeIndex - 1, k);
        else return QuickSelect(arr, storeIndex + 1, right, k);
    }

    private static void Swap(int[] arr, int i, int j)
    {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    static void Main()
    {
        int N = int.Parse(Console.ReadLine());
        int[] arr = Console.ReadLine().Split().Select(int.Parse).ToArray();
        int K = int.Parse(Console.ReadLine()) - 1; // K를 0-based index로 변환

        int result = QuickSelect(arr, 0, N - 1, K);
        Console.WriteLine(result);
    }
}
        

코드 설명

위의 C# 코드는 Quickselect 알고리즘을 사용하여 K번째 수를 찾습니다:

  • QuickSelect 메소드: 배열에서 K번째 작은 수를 찾아 반환합니다.
  • Swap 메소드: 두 원소의 위치를 바꿉니다.
  • Main: 입력을 받고 실행합니다.

이 알고리즘은 평균적으로 O(N) 시간 복잡도를 가지며, 이는 매우 효율적입니다.

마무리

이번 강좌에서는 C#을 사용하여 ‘K번째 수 구하기’ 문제를 해결하는 방법을 배웠습니다. Quickselect 알고리즘을 통해 효율적으로 문제를 해결하는 방법을 실습함으로써, 코딩 테스트에서 유용한 기법을 익힐 수 있었습니다. 추가적으로 다양한 알고리즘과 문제를 경험하여 더욱 능력을 키우길 바랍니다.

C# 코딩테스트 강좌, 사전 찾기

문제 설명

주어진 단어 목록과 검색할 단어가 있을 때, 이 검색할 단어가 단어 목록에 포함되어 있는지를 체크하는 문제입니다. 이 문제는
사전(dictionary) 자료구조를 활용하여 해결할 수 있습니다. 이는 C#에서 Dictionary 클래스를 사용하여 쉽게 구현할 수 있습니다.

알고리즘 문제

문제


        주어진 단어 목록 words가 주어질 때, 다음과 같은 checkWords 메소드를 작성하세요. 
        
checkWords 메소드는 문자열 목록과 문자열을 인자로 받아, 해당 문자열이 목록에 있으면 true를 리턴하고, 없으면 false를 리턴해야 합니다.
예시
입력: words = ["apple", "banana", "cherry"], searchWord = "banana"
출력: true

문제 풀이

이 문제를 해결하기 위한 가장 효율적인 방법 중 하나는 해시 테이블을 사용하는 것입니다. 해시 테이블을 사용하여 단어 목록을
미리 저장하고, 검색할 때 O(1) 시간 복잡도로 단어를 찾아낼 수 있습니다. C#의 Dictionary 클래스를 활용하면
이러한 구현이 가능하므로, 아래의 단계로 문제를 해결하겠습니다.

단계 1: 단어 목록을 Dictionary로 변환하기

먼저, 주어진 단어 목록을 Dictionary 형태로 변환합니다. 각 단어를 키(key)로 사용하고, 값(value)은
불필요하게 빈 문자열을 사용하겠습니다.


        // C# 코드
        using System;
        using System.Collections.Generic;

        public class DictionarySearch
        {
            private Dictionary wordDictionary;

            public DictionarySearch(List words)
            {
                wordDictionary = new Dictionary();
                foreach (var word in words)
                {
                    wordDictionary[word] = true; // Value는 필요 없으므로 true로 설정
                }
            }
        }
        

단계 2: 단어 검색 메소드 작성하기

이제 checkWords 메소드를 작성하겠습니다. 이 메소드는 사전에서 검색할 단어가 존재하는지 여부를 확인할 것입니다.


        public bool CheckWords(string searchWord)
        {
            return wordDictionary.ContainsKey(searchWord);
        }
        

단계 3: 최종 클래스 및 메소드 구현

위의 단계들을 결합하여 최종적인 솔루션 형태로 클래스를 완성하겠습니다.


        using System;
        using System.Collections.Generic;

        public class DictionarySearch
        {
            private Dictionary wordDictionary;

            public DictionarySearch(List words)
            {
                wordDictionary = new Dictionary();
                foreach (var word in words)
                {
                    wordDictionary[word] = true;
                }
            }

            public bool CheckWords(string searchWord)
            {
                return wordDictionary.ContainsKey(searchWord);
            }
        }

        class Program
        {
            static void Main(string[] args)
            {
                List words = new List { "apple", "banana", "cherry" };
                DictionarySearch dictionarySearch = new DictionarySearch(words);

                Console.WriteLine(dictionarySearch.CheckWords("banana")); // Output: true
                Console.WriteLine(dictionarySearch.CheckWords("grape")); // Output: false
            }
        }
        

복잡도 분석

– 시간 복잡도: O(1) – 해시 테이블을 사용하여 단어를 저장하고 검색하는 것이 최적으로 O(1) 시간에 수행됩니다.
– 공간 복잡도: O(n) – 주어진 단어 목록의 크기 n에 비례합니다.

중요 포인트

– 문제를 해결하기 위해 Dictionary 클래스를 사용하는 것이 핵심입니다. 이는 빠른 검색 속도를 보장합니다.
– 문자열 비교의 경우, 대소문자 구별 여부에 따라 결과가 달라질 수 있으므로 주의해야 합니다.
– 전체적인 코드를 수행해 본 후, 다양한 테스트 케이스를 통해 검증이 필요합니다.

결론

본 강좌에서는 C#을 활용하여 사전 찾기 문제를 해결하는 방법을 설명했습니다. 해시 테이블을 사용한 자료구조적 접근이
문제가 주어진 조건을 효율적으로 만족시킬 수 있음을 보였습니다. 실제 코딩 테스트에서도 이와 같은 자료구조의 사용이 빈번하게
나타나므로, 미리 연습하는 것이 중요합니다.

C# 코딩테스트 강좌, 게임 개발하기

문제 설명

가상의 게임에서는 적 캐릭터와 플레이어 캐릭터가 있습니다. 적 캐릭터는 (x, y) 좌표에서 출발하여 플레이어 캐릭터에게 접근해야 합니다. 여기서, 적 캐릭터는 매 턴마다 플레이어의 위치로 가장 가까이 이동할 수 있는 최적의 경로를 찾아야 합니다. 이 문제를 해결하기 위해, 우리는 적이 플레이어에게 다가가기 위한 최적 경로를 계산해야 합니다.

문제 정의

다음의 입력을 받습니다:

  • 적의 초기 위치 (X1, Y1)
  • 플레이어의 위치 (X2, Y2)

출력은 적이 플레이어에게 도달하는 최적의 경로를 (모든 중간 좌표 포함) 반환해야 합니다.

입력 예시

    0 0
    3 4
    

출력 예시

    (0,0) → (1,1) → (2,2) → (3,3) → (3,4)
    

해결 전략

이 문제는 기초적인 거리 계산과 경로 추적을 포함합니다. 우리는 각 턴에서 적의 위치를 업데이트하여 플레이어에게 접근할 수 있는 다음 위치를 계산해야 합니다. 이를 위해 유클리드 거리 공식을 사용하여 두 점 간의 거리와 방향을 계산하고, 매 턴마다 적이 취할 수 있는 최적의 경로를 찾습니다.

1. 거리 계산

    dist(X1, Y1, X2, Y2) = sqrt((X2 - X1)² + (Y2 - Y1)²)
    

2. 이동 로직 구현

우리는 적이 매 턴마다 플레이어에게 접근하도록 함으로써, X와 Y 좌표를 조정합니다.

3. 경로 저장

각 이동 후의 좌표를 리스트에 저장하여 최종적으로 경로를 출력합니다.

C# 구현 예시

    using System;
    using System.Collections.Generic;

    class Program
    {
        static void Main(string[] args)
        {
            // 적의 초기 위치
            var enemyPosition = (X: 0, Y: 0);
            // 플레이어의 위치
            var playerPosition = (X: 3, Y: 4);
            var path = new List<(int, int)>();

            while (enemyPosition != playerPosition)
            {
                path.Add(enemyPosition);
                enemyPosition = MoveTowardsPlayer(enemyPosition, playerPosition);
            }
            path.Add(playerPosition);

            Console.WriteLine("Path taken:");
            foreach (var pos in path)
                Console.WriteLine($"({pos.Item1}, {pos.Item2})");
        }

        static (int, int) MoveTowardsPlayer((int X, int Y) enemy, (int X, int Y) player)
        {
            int newX = enemy.X + (player.X > enemy.X ? 1 : (player.X < enemy.X ? -1 : 0));
            int newY = enemy.Y + (player.Y > enemy.Y ? 1 : (player.Y < enemy.Y ? -1 : 0));
            return (newX, newY);
        }
    }
    

결론

이 문제를 통해 게임 개발에 있어 경로finding과 객체 간의 상호작용을 이해할 수 있습니다. C#을 이용한 코딩 테스트 문제 해결은 향후 실제 게임 개발 프로젝트에 도움이 될 것입니다. 우리는 입력된 위치에 따라 적이 플레이어에게 도달하도록 최적의 이동 경로를 계산할 수 있음을 알 수 있습니다.