핵심 알고리즘 가이드

코딩 면접을 준비하면서 “어떤 알고리즘을 공부해야 하지?”라는 고민을 해본 적 있나요?

저도 처음에는 수많은 알고리즘 중 무엇을 먼저 배워야 할지 막막했습니다. 하지만 실제로 프로젝트를 진행하고 면접을 경험하면서, 자주 사용되는 핵심 알고리즘 패턴이 존재한다는 것을 알게 되었습니다.

이 가이드는 실무와 면접에서 가장 많이 마주치는 알고리즘들을 정리했습니다.

왜 이 알고리즘들을 배워야 할까요?

1. 코딩 면접의 80%를 커버

실제 LeetCode, HackerRank의 데이터 분석에 따르면:

  • 정렬/탐색: 면접 문제의 30%
  • 동적 프로그래밍: 면접 문제의 25%
  • 그래프/트리: 면접 문제의 20%
  • 문자열: 면접 문제의 15%
  • 기타: 10%

이 핵심 알고리즘들만 제대로 이해해도 대부분의 면접 문제를 해결할 수 있습니다.

면접 문제 유형 분포
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
정렬/탐색      ████████████████████████████████ 30%
동적 프로그래밍 ████████████████████████ 25%
그래프/트리     ████████████████████ 20%
문자열         ███████████████ 15%
기타           ██████████ 10%

2. 실무에서 실제로 사용

“알고리즘은 면접용 아니야?”라고 생각할 수 있지만, 실무에서도 많이 사용됩니다.

저도 처음에는 “이런 걸 실무에서 언제 써?”라고 생각했는데, 막상 프로젝트를 진행하다 보니:

  • 정렬: 데이터 테이블 정렬, 우선순위 큐 구현
  • 탐색: 검색 기능, 자동완성, 실시간 필터링
  • 동적 프로그래밍: 캐싱 전략, 최적화 문제
  • 그래프: 소셜 네트워크, 추천 시스템, 의존성 관리
  • 문자열: 텍스트 처리, 파싱, 유효성 검사

실제로 이커머스 프로젝트에서 “사용자가 좋아할만한 상품 추천” 기능을 만들 때, 그래프 알고리즘(BFS)을 사용했었습니다. 알고리즘을 몰랐다면 비효율적인 방식으로 구현했을 거예요.

3. 문제 해결 능력 향상

알고리즘을 배우면 다음과 같은 능력이 향상됩니다.

Before (알고리즘 공부 전):

// 😰 비효율적인 접근
function findDuplicates(arr) {
  const duplicates = [];
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j] && !duplicates.includes(arr[i])) {
        duplicates.push(arr[i]);
      }
    }
  }
  return duplicates; // O(n³) 시간 복잡도!
}

After (알고리즘 공부 후):

// ✅ 효율적인 접근
function findDuplicates(arr) {
  const seen = new Set();
  const duplicates = new Set();

  for (const item of arr) {
    if (seen.has(item)) {
      duplicates.add(item);
    } else {
      seen.add(item);
    }
  }

  return Array.from(duplicates); // O(n) 시간 복잡도!
}

알고리즘을 배우면:

  • ✅ 복잡한 문제를 작은 문제로 분해하는 능력
  • ✅ 효율적인 해결책을 찾는 능력
  • ✅ 시간/공간 복잡도를 분석하는 능력
  • ✅ 코드 리뷰에서 성능 이슈를 발견하는 능력

카테고리별 핵심 알고리즘

1. 정렬 알고리즘 (Sorting Algorithms)

“데이터를 정렬한다”는 것은 프로그래밍에서 가장 기본적이면서도 중요한 작업입니다.

예를 들어, 전자상거래 사이트에서 “가격 낮은 순”으로 상품을 보여주는 기능을 생각해보세요. 이게 바로 정렬 알고리즘입니다!

정렬 알고리즘을 선택할 때는 데이터 크기, 데이터 상태, 메모리 제약 을 고려해야 합니다.

정렬 알고리즘 선택 가이드
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

데이터 크기가 작다 (< 10개)
    → 버블 정렬 (구현 간단)

데이터가 거의 정렬되어 있다
    → 삽입 정렬 또는 버블 정렬

일반적인 경우 (평균적으로 빠르게)
    → 퀵 정렬

최악의 경우도 보장되어야 함
    → 병합 정렬

메모리가 제한적이다
    → 퀵 정렬 (제자리 정렬)

버블 정렬 (Bubble Sort)

시간 복잡도: O(n²) 공간 복잡도: O(1)

왜 ‘버블’일까요?

버블 정렬은 마치 물속의 거품이 위로 올라가듯이, 큰 값이 배열의 끝으로 “떠오르는” 모습에서 이름이 유래되었습니다.

동작 원리 시각화:
[5, 2, 8, 1, 9]
 ↓  ↓ (비교 & 교환)
[2, 5, 8, 1, 9]
    ↓  ↓ (비교, 교환 안함)
[2, 5, 8, 1, 9]
       ↓  ↓ (비교 & 교환)
[2, 5, 1, 8, 9]
          ↓  ↓ (비교, 교환 안함)
[2, 5, 1, 8, 9] → 9가 맨 끝으로!

언제 사용할까?

  • 데이터가 거의 정렬되어 있을 때 (최선의 경우 O(n))
  • 구현이 간단해야 할 때
  • 데이터 크기가 매우 작을 때 (< 10개)
  • 교육 목적으로 정렬의 기본 개념을 이해할 때

실전 활용:

// ❌ 비효율적 (최적화 없음)
const bubbleSortBasic = (arr) => {
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
};

// ✅ 최적화된 버전
const bubbleSort = (arr) => {
  let swapped;

  for (let i = 0; i < arr.length; i++) {
    swapped = false;

    // 이미 정렬된 부분은 제외
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        swapped = true;
      }
    }

    // 교환이 없었다면 이미 정렬됨
    if (!swapped) break;
  }

  return arr;
};

// 실사용 예시: 소규모 우선순위 목록 정렬
const priorityTasks = [
  { name: 'Task A', priority: 3 },
  { name: 'Task B', priority: 1 },
  { name: 'Task C', priority: 2 }
];

bubbleSort(priorityTasks, (a, b) => a.priority - b.priority);

⚠️ 함정 주의!

// ❌ 큰 배열에는 사용하지 마세요!
const largeArray = Array.from({ length: 10000 }, () => Math.random());
bubbleSort(largeArray); // 너무 느림!

// ✅ 큰 배열은 퀵소트나 내장 sort() 사용
largeArray.sort((a, b) => a - b);

퀵 정렬 (Quick Sort)

시간 복잡도: 평균 O(n log n), 최악 O(n²) 공간 복잡도: O(log n)

왜 ‘퀵(Quick)’일까요?

퀵 정렬은 실제로 가장 빠른 정렬 알고리즘 중 하나입니다. “분할 정복(Divide and Conquer)” 전략을 사용하는데, 마치 책을 정리할 때 “무거운 책은 왼쪽, 가벼운 책은 오른쪽”으로 나누는 것과 비슷합니다.

퀵 정렬 동작 원리 (피벗 = 5):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[3, 7, 8, 5, 2, 1, 9, 5, 4]
              ↑ 피벗

1단계: 피벗(5)을 기준으로 분할
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
작은 값들 | 피벗 | 큰 값들
[3,2,1,4] | [5,5] | [7,8,9]

2단계: 각 부분을 재귀적으로 정렬
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[1,2,3,4] | [5,5] | [7,8,9]

최종 결과:
[1, 2, 3, 4, 5, 5, 7, 8, 9]

언제 사용할까?

  • 대부분의 일반적인 정렬 상황 (가장 추천!)
  • 평균적으로 가장 빠른 정렬이 필요할 때
  • 메모리가 제한적일 때 (제자리 정렬 가능)
  • 안정성(stability)이 중요하지 않을 때

💡 알고 계셨나요? JavaScript의 Array.sort(), Python의 sorted(), Java의 Arrays.sort() 모두 퀵소트 또는 퀵소트 변형을 사용합니다!

실전 활용:

// ✅ 간단한 버전 (교육용)
const quickSort = <T>(arr: T[], compare?: (a: T, b: T) => number): T[] => {
  if (arr.length <= 1) return arr;

  const pivot = arr[Math.floor(arr.length / 2)];
  const compareFunc = compare || ((a, b) => Number(a) - Number(b));

  const left = arr.filter(x => compareFunc(x, pivot) < 0);
  const middle = arr.filter(x => compareFunc(x, pivot) === 0);
  const right = arr.filter(x => compareFunc(x, pivot) > 0);

  return [...quickSort(left, compare), ...middle, ...quickSort(right, compare)];
};

// ✅ 최적화된 제자리 정렬 버전
const quickSortInPlace = (arr: number[], low = 0, high = arr.length - 1): void => {
  if (low < high) {
    const pivotIndex = partition(arr, low, high);
    quickSortInPlace(arr, low, pivotIndex - 1);
    quickSortInPlace(arr, pivotIndex + 1, high);
  }
};

const partition = (arr: number[], low: number, high: number): number => {
  const pivot = arr[high];
  let i = low - 1;

  for (let j = low; j < high; j++) {
    if (arr[j] < pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }

  [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
  return i + 1;
};

// 실사용 예시: 대용량 상품 데이터 정렬
interface Product {
  id: number;
  name: string;
  price: number;
  rating: number;
}

const products: Product[] = [
  { id: 1, name: 'Laptop', price: 1200, rating: 4.5 },
  { id: 2, name: 'Mouse', price: 25, rating: 4.2 },
  { id: 3, name: 'Keyboard', price: 80, rating: 4.7 }
];

// 가격순 정렬
quickSort(products, (a, b) => a.price - b.price);

// 평점순 정렬
quickSort(products, (a, b) => b.rating - a.rating);

⚠️ 함정 주의!

// ❌ 최악의 경우: 이미 정렬된 배열 + 나쁜 피벗 선택
const sortedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// 항상 첫 번째 요소를 피벗으로 선택하면 O(n²)!

// ✅ 해결책 1: 랜덤 피벗 선택
const pivot = arr[Math.floor(Math.random() * arr.length)];

// ✅ 해결책 2: 중간값 피벗 선택 (median-of-three)
const mid = Math.floor(arr.length / 2);
const pivot = [arr[0], arr[mid], arr[arr.length - 1]]
  .sort((a, b) => a - b)[1];

// ✅ 해결책 3: 최악의 경우도 보장이 필요하면 병합 정렬 사용

병합 정렬 (Merge Sort)

시간 복잡도: O(n log n) 공간 복잡도: O(n)

언제 사용할까?

  • 안정적인 정렬이 필요할 때
  • 최악의 경우에도 O(n log n)을 보장해야 할 때
  • 연결 리스트를 정렬할 때

실전 활용:

// 외부 정렬 (대용량 파일 정렬)
const mergeSort = (arr) => {
  if (arr.length <= 1) return arr;

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  return merge(left, right);
};

const merge = (left, right) => {
  const result = [];
  let i = 0, j = 0;

  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }

  return [...result, ...left.slice(i), ...right.slice(j)];
};

2. 탐색 알고리즘 (Searching Algorithms)

“바늘을 건초더미에서 찾는다”는 말, 들어보셨나요?

탐색 알고리즘은 바로 이 바늘을 얼마나 빠르게 찾을 수 있는지에 대한 문제입니다. 구글 검색, 파일 찾기, 사용자 검색 등 우리가 매일 사용하는 기능들이 모두 탐색 알고리즘을 기반으로 합니다.

탐색 알고리즘 비교
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
알고리즘      | 시간 복잡도 | 전제 조건
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
선형 탐색     | O(n)       | 없음
이진 탐색     | O(log n)   | 정렬된 배열
해시 테이블   | O(1)       | 해시 함수
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

예시: 1000개 데이터에서 검색
- 선형 탐색: 평균 500번 비교
- 이진 탐색: 최대 10번 비교  ← 50배 빠름!
- 해시 테이블: 1번 비교      ← 500배 빠름!
  • 시간 복잡도: O(n)
  • 공간 복잡도: O(1)

  • 언제 사용할까?
    • 데이터가 정렬되어 있지 않을 때
    • 데이터 크기가 작을 때
    • 한 번만 검색할 때

시간 복잡도: O(log n) 공간 복잡도: O(1)

이진 탐색의 마법 🎩

이진 탐색은 마치 “스무고개” 게임과 같습니다!

예시: 1~100 사이의 숫자 맞추기

선형 탐색 방식:
"1이야?" → "아니"
"2야?" → "아니"
"3이야?" → "아니"
...최대 100번 질문

이진 탐색 방식:
"50보다 크니?" → "아니"
"25보다 크니?" → "응"
"37보다 크니?" → "아니"
...최대 7번 질문으로 끝!

동작 원리 시각화:

[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]에서 7 찾기
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1단계:
[1, 3, 5, 7, 9, | 11, 13, 15, 17, 19]
           ↑ mid = 9
7 < 9 → 왼쪽으로

2단계:
[1, 3, | 5, 7, 9]
    ↑ mid = 5
7 > 5 → 오른쪽으로

3단계:
[7, | 9]
 ↑ mid = 7
7 == 7 → 찾았다! ✅

언제 사용할까?

  • 정렬된 데이터에서 검색할 때 (필수 조건!)
  • 빠른 검색이 필요할 때
  • 데이터가 자주 변하지 않을 때
  • 대량의 데이터를 다룰 때

실전 활용:

// ✅ 기본 이진 탐색
const binarySearch = <T>(
  arr: T[],
  target: T,
  compare: (a: T, b: T) => number = (a, b) => Number(a) - Number(b)
): number => {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    const comparison = compare(arr[mid], target);

    if (comparison === 0) {
      return mid; // 찾았다!
    } else if (comparison < 0) {
      left = mid + 1; // 오른쪽 절반에서 찾기
    } else {
      right = mid - 1; // 왼쪽 절반에서 찾기
    }
  }

  return -1; // 찾지 못함
};

// ✅ 재귀 버전 (더 직관적)
const binarySearchRecursive = <T>(
  arr: T[],
  target: T,
  left = 0,
  right = arr.length - 1,
  compare: (a: T, b: T) => number = (a, b) => Number(a) - Number(b)
): number => {
  if (left > right) return -1;

  const mid = Math.floor((left + right) / 2);
  const comparison = compare(arr[mid], target);

  if (comparison === 0) return mid;
  if (comparison < 0) return binarySearchRecursive(arr, target, mid + 1, right, compare);
  return binarySearchRecursive(arr, target, left, mid - 1, compare);
};

// 실사용 예시 1: 사용자 목록에서 특정 사용자 찾기
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: 'Alice' },
  { id: 3, name: 'Bob' },
  { id: 5, name: 'Charlie' },
  { id: 7, name: 'David' }
]; // ID로 정렬되어 있음!

const findUserById = (id: number): User | null => {
  const index = binarySearch(users, { id } as User, (a, b) => a.id - b.id);
  return index >= 0 ? users[index] : null;
};

// 실사용 예시 2: 가격대 검색
const products = [
  { name: 'Mouse', price: 25 },
  { name: 'Keyboard', price: 80 },
  { name: 'Monitor', price: 300 },
  { name: 'Laptop', price: 1200 }
]; // 가격순 정렬됨!

// 특정 가격 이하 상품 찾기
const findMaxPriceIndex = (maxPrice: number): number => {
  let left = 0;
  let right = products.length - 1;
  let result = -1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (products[mid].price <= maxPrice) {
      result = mid;
      left = mid + 1; // 더 큰 값도 확인
    } else {
      right = mid - 1;
    }
  }

  return result;
};

console.log(findMaxPriceIndex(100)); // 1 (Keyboard까지)

⚠️ 함정 주의!

// ❌ 정렬되지 않은 배열에서 이진 탐색
const unsorted = [5, 2, 8, 1, 9];
binarySearch(unsorted, 2); // 잘못된 결과!

// ✅ 먼저 정렬하거나 선형 탐색 사용
unsorted.sort((a, b) => a - b);
binarySearch(unsorted, 2); // 올바른 결과

// ❌ 중간값 계산 시 오버플로우 (매우 큰 배열)
const mid = (left + right) / 2; // left + right가 오버플로우 가능

// ✅ 안전한 중간값 계산
const mid = left + Math.floor((right - left) / 2);
// 또는
const mid = Math.floor((left + right) / 2); // JavaScript는 안전

💡 이진 탐색의 변형들

// 1. 첫 번째 발견 위치 찾기
const findFirst = (arr, target) => {
  let left = 0, right = arr.length - 1, result = -1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) {
      result = mid;
      right = mid - 1; // 더 왼쪽도 확인
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return result;
};

// 2. 마지막 발견 위치 찾기
const findLast = (arr, target) => {
  let left = 0, right = arr.length - 1, result = -1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) {
      result = mid;
      left = mid + 1; // 더 오른쪽도 확인
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return result;
};

// 예시: [1, 2, 2, 2, 3, 4]에서 2 찾기
findFirst([1, 2, 2, 2, 3, 4], 2);  // 1
findLast([1, 2, 2, 2, 3, 4], 2);   // 3
  • 시간 복잡도: O(V + E) - V: 정점, E: 간선
  • 공간 복잡도: O(V)

  • 언제 사용할까?
    • 모든 경로를 탐색해야 할 때
    • 백트래킹 문제
    • 사이클 감지

실전 활용:

// 파일 시스템 탐색
class FileSystem {
  constructor(name, isDirectory = false) {
    this.name = name;
    this.isDirectory = isDirectory;
    this.children = [];
  }

  addChild(node) {
    this.children.push(node);
  }
}

const dfsTraversal = (node, callback, depth = 0) => {
  callback(node, depth);

  if (node.isDirectory) {
    for (const child of node.children) {
      dfsTraversal(child, callback, depth + 1);
    }
  }
};

// 사용 예시
const root = new FileSystem('/', true);
const documents = new FileSystem('Documents', true);
const file1 = new FileSystem('file1.txt', false);

root.addChild(documents);
documents.addChild(file1);

dfsTraversal(root, (node, depth) => {
  console.log('  '.repeat(depth) + node.name);
});
  • 시간 복잡도: O(V + E)
  • 공간 복잡도: O(V)

  • 언제 사용할까?
    • 최단 경로를 찾을 때
    • 레벨 순회가 필요할 때
    • 가까운 노드부터 탐색해야 할 때

실전 활용:

// 소셜 네트워크에서 친구 추천 (2촌 친구 찾기)
interface Person {
  name: string;
  friends: Person[];
}

const findFriendsOfFriends = (person: Person, maxDegree: number = 2): Set<Person> => {
  const visited = new Set<Person>();
  const result = new Set<Person>();
  const queue: [Person, number][] = [[person, 0]];

  visited.add(person);

  while (queue.length > 0) {
    const [current, degree] = queue.shift()!;

    if (degree > 0 && degree <= maxDegree) {
      result.add(current);
    }

    if (degree < maxDegree) {
      for (const friend of current.friends) {
        if (!visited.has(friend)) {
          visited.add(friend);
          queue.push([friend, degree + 1]);
        }
      }
    }
  }

  return result;
};

3. 동적 프로그래밍 (Dynamic Programming)

복잡한 문제를 작은 부분 문제로 나누어 해결하는 기법입니다.

피보나치 수열 (Fibonacci Sequence)

  • 시간 복잡도: O(n) - DP 사용 시
  • 공간 복잡도: O(n) 또는 O(1)

  • 언제 사용할까?
    • DP의 기본 개념 학습
    • 재귀를 최적화해야 할 때

실전 활용:

// ❌ 비효율적인 재귀 (O(2^n))
const fibRecursive = (n) => {
  if (n <= 1) return n;
  return fibRecursive(n - 1) + fibRecursive(n - 2);
};

// ✅ 메모이제이션 (O(n))
const fibMemo = (() => {
  const cache = {};
  return function(n) {
    if (n in cache) return cache[n];
    if (n <= 1) return n;
    return cache[n] = fibMemo(n - 1) + fibMemo(n - 2);
  };
})();

// ✅ Tabulation (O(n), 공간 O(1))
const fibOptimal = (n) => {
  if (n <= 1) return n;
  let prev = 0, curr = 1;

  for (let i = 2; i <= n; i++) {
    [prev, curr] = [curr, prev + curr];
  }

  return curr;
};

배낭 문제 (Knapsack Problem)

  • 시간 복잡도: O(n × W) - n: 아이템 수, W: 배낭 용량
  • 공간 복잡도: O(n × W)

  • 언제 사용할까?
    • 리소스 제약이 있는 최적화 문제
    • 예산 내에서 최대 가치 선택
    • 캐시 크기 제한이 있을 때

실전 활용:

// 광고 선택 최적화 (예산 내에서 최대 수익)
interface Ad {
  cost: number;
  revenue: number;
  name: string;
}

const knapsack = (ads: Ad[], budget: number): { maxRevenue: number; selectedAds: Ad[] } => {
  const n = ads.length;
  const dp: number[][] = Array(n + 1).fill(0).map(() => Array(budget + 1).fill(0));

  // DP 테이블 채우기
  for (let i = 1; i <= n; i++) {
    for (let w = 0; w <= budget; w++) {
      if (ads[i - 1].cost <= w) {
        dp[i][w] = Math.max(
          dp[i - 1][w],
          dp[i - 1][w - ads[i - 1].cost] + ads[i - 1].revenue
        );
      } else {
        dp[i][w] = dp[i - 1][w];
      }
    }
  }

  // 선택된 광고 역추적
  const selectedAds: Ad[] = [];
  let w = budget;
  for (let i = n; i > 0 && w > 0; i--) {
    if (dp[i][w] !== dp[i - 1][w]) {
      selectedAds.push(ads[i - 1]);
      w -= ads[i - 1].cost;
    }
  }

  return { maxRevenue: dp[n][budget], selectedAds };
};

최장 공통 부분 수열 (LCS - Longest Common Subsequence)

  • 시간 복잡도: O(m × n)
  • 공간 복잡도: O(m × n)

  • 언제 사용할까?
    • 텍스트 유사도 측정
    • Git diff 구현
    • DNA 서열 비교

실전 활용:

// Git diff 유사 기능
const longestCommonSubsequence = (text1, text2) => {
  const m = text1.length;
  const n = text2.length;
  const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (text1[i - 1] === text2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }

  return dp[m][n];
};

// 텍스트 유사도 계산
const textSimilarity = (text1, text2) => {
  const lcsLength = longestCommonSubsequence(text1, text2);
  const maxLength = Math.max(text1.length, text2.length);
  return (lcsLength / maxLength) * 100;
};

console.log(textSimilarity('ABCDGH', 'AEDFHR')); // 약 50% 유사

4. 그래프 알고리즘 (Graph Algorithms)

다익스트라 (Dijkstra’s Algorithm)

  • 시간 복잡도: O((V + E) log V)
  • 공간 복잡도: O(V)

  • 언제 사용할까?
    • 최단 경로 찾기 (음수 가중치 없을 때)
    • 네비게이션 앱
    • 네트워크 라우팅

실전 활용:

// 배달 경로 최적화
interface Edge {
  to: number;
  weight: number;
}

class Graph {
  private adjacencyList: Map<number, Edge[]> = new Map();

  addEdge(from: number, to: number, weight: number): void {
    if (!this.adjacencyList.has(from)) {
      this.adjacencyList.set(from, []);
    }
    this.adjacencyList.get(from)!.push({ to, weight });
  }

  dijkstra(start: number): Map<number, number> {
    const distances = new Map<number, number>();
    const visited = new Set<number>();
    const pq: [number, number][] = [[start, 0]]; // [node, distance]

    distances.set(start, 0);

    while (pq.length > 0) {
      pq.sort((a, b) => a[1] - b[1]);
      const [current, currentDist] = pq.shift()!;

      if (visited.has(current)) continue;
      visited.add(current);

      const neighbors = this.adjacencyList.get(current) || [];
      for (const { to, weight } of neighbors) {
        const distance = currentDist + weight;

        if (!distances.has(to) || distance < distances.get(to)!) {
          distances.set(to, distance);
          pq.push([to, distance]);
        }
      }
    }

    return distances;
  }
}

5. 문자열 알고리즘 (String Algorithms)

KMP 알고리즘 (Knuth-Morris-Pratt)

  • 시간 복잡도: O(n + m)
  • 공간 복잡도: O(m)

  • 언제 사용할까?
    • 패턴 매칭
    • 텍스트 검색
    • 플레이저리즘 검사

라빈-카프 (Rabin-Karp)

  • 시간 복잡도: 평균 O(n + m)
  • 공간 복잡도: O(1)

  • 언제 사용할까?
    • 여러 패턴을 동시에 검색
    • 중복 문자열 찾기

실전 활용:

// 중복 파일 감지
const rabinKarp = (text, pattern) => {
  const d = 256; // 문자 집합 크기
  const q = 101; // 소수
  const m = pattern.length;
  const n = text.length;
  let patternHash = 0;
  let textHash = 0;
  let h = 1;
  const matches = [];

  // h = d^(m-1) % q
  for (let i = 0; i < m - 1; i++) {
    h = (h * d) % q;
  }

  // 패턴과 텍스트 첫 윈도우의 해시 계산
  for (let i = 0; i < m; i++) {
    patternHash = (d * patternHash + pattern.charCodeAt(i)) % q;
    textHash = (d * textHash + text.charCodeAt(i)) % q;
  }

  // 슬라이딩 윈도우
  for (let i = 0; i <= n - m; i++) {
    if (patternHash === textHash) {
      // 해시가 같으면 실제 문자열 비교
      if (text.substring(i, i + m) === pattern) {
        matches.push(i);
      }
    }

    if (i < n - m) {
      textHash = (d * (textHash - text.charCodeAt(i) * h) + text.charCodeAt(i + m)) % q;
      if (textHash < 0) textHash += q;
    }
  }

  return matches;
};

학습 로드맵

초급 (1-2주)

  1. ✅ 선형 탐색, 버블 정렬
  2. ✅ 이진 탐색
  3. ✅ 재귀 기초 (피보나치)

중급 (3-4주)

  1. ✅ 퀵소트, 병합 정렬
  2. ✅ DFS, BFS
  3. ✅ 기본 DP (피보나치, LCS)

고급 (5-8주)

  1. ✅ 다익스트라, A*
  2. ✅ 배낭 문제
  3. ✅ KMP, 라빈-카프

실전 팁

1. 문제 해결 프로세스

// 1단계: 문제 이해 및 예시 작성
// 입력: [3, 1, 4, 1, 5, 9, 2, 6]
// 출력: [1, 1, 2, 3, 4, 5, 6, 9]

// 2단계: 시간/공간 복잡도 목표 설정
// 목표: O(n log n) 시간, O(1) 공간

// 3단계: 알고리즘 선택
// 퀵소트 선택 (평균 O(n log n), 제자리 정렬)

// 4단계: 구현
const solve = (arr) => {
  return quickSort(arr);
};

// 5단계: 테스트 및 최적화
console.log(solve([3, 1, 4, 1, 5, 9, 2, 6]));

2. 복잡도 분석 치트시트

복잡도 예시 10개 100개 1000개
O(1) 배열 접근 1 1 1
O(log n) 이진 탐색 3 7 10
O(n) 선형 탐색 10 100 1000
O(n log n) 퀵소트 30 700 10000
O(n²) 버블 정렬 100 10000 1000000

3. 디버깅 전략

// 디버깅용 래퍼 함수
const debugAlgorithm = <T extends (...args: any[]) => any>(
  fn: T,
  name: string
): T => {
  return ((...args: Parameters<T>): ReturnType<T> => {
    console.log(`[${name}] 입력:`, args);
    const startTime = performance.now();

    const result = fn(...args);

    const endTime = performance.now();
    console.log(`[${name}] 출력:`, result);
    console.log(`[${name}] 실행 시간: ${(endTime - startTime).toFixed(2)}ms`);

    return result;
  }) as T;
};

// 사용 예시
const binarySearchDebug = debugAlgorithm(binarySearch, 'BinarySearch');
binarySearchDebug([1, 2, 3, 4, 5], 3);

참고 자료

온라인 저지

  • Introduction to Algorithms (CLRS) - 알고리즘 바이블
  • Grokking Algorithms - 그림으로 배우는 알고리즘
  • Cracking the Coding Interview - 면접 대비

시각화 도구

유튜브 채널

  • Abdul Bari - 알고리즘 이론
  • William Fiset - 그래프 알고리즘
  • Back To Back SWE - 면접 문제 풀이

마치며

알고리즘 학습은 마라톤과 같습니다. 단기간에 모든 것을 배우려 하지 말고, 하나씩 차근차근 이해하며 나아가세요.

저도 처음에는 DP가 너무 어려워서 포기하고 싶었지만, 매일 한 문제씩 풀다 보니 어느새 자연스럽게 패턴이 보이기 시작했습니다.

가장 중요한 것은 일관성입니다. 하루 30분이라도 꾸준히 연습하면, 3개월 후에는 놀라운 성장을 경험하게 될 것입니다.

화이팅! 🚀

댓글