Algorithm|Javascript Algorithm

실전 코드를 빅오 표현법으로 표현하기

0
실전 코드를 빅오 표현법으로 표현하기

크리스가 동료 개발자가 작성한 코드를 리뷰하고 있다.

변수 이름도 깔끔하고 주석도 잘 달려있다. 그런데 반복문 구조가 어딘가 묘하다.

"이 함수, 데이터가 100개일 땐 괜찮은데 10,000개가 넘어가면 서버가 멈출 것 같은데요?"

"어? 반복문 하나밖에 안 썼는데 왜요?"

동료의 반문에 크리스는 정확한 근거를 들어 설명해야 했다.

"이건 반복문이 하나처럼 보이지만, 사실 숨겨진 반복문이 있어서 O(N^2)이에요!"

오늘은 막연했던 빅오(Big-O) 개념을 실제 코드에 대입해 계산해 보는 실전 훈련 시간이다. 덧셈과 곱셈만 할 줄 알면 누구나 할 수 있다

1. 빅오 계산의 4가지 법칙

복잡한 수학 공식은 필요 없다. 코드를 눈으로 훑으며 아래 4가지 규칙만 적용하면 된다.

규칙 1: 최악을 생각하라 (Worst Case)

"운 좋게 바로 찾았다"는 없다. 항상 "찾는 데이터가 배열의 맨 마지막에 있다"고 가정한다.

규칙 2: 상수는 버려라 (Drop Constants)

  • 반복문을 2번 돌리든(2N), 100번 돌리든(100N), 빅오에서는 그냥 O(N)이다.
  • N이 무한대로 커지면 2배나 100배 차이는 그래프의 기울기(추세)에 영향을 주지 않기 때문이다.
  • 규칙 3: 덜 중요한 항은 무시하라 (Drop Non-Dominants)

  • O(N^2 + N + 10) -> O(N^2)
  • N^2이 커지는 속도에 비하면 N이나 10은 티끌 수준이다. 가장 강력한 놈 하나만 남긴다.
  • 규칙 4: 단계별 연산 합산

  • 코드 한 줄 한 줄의 비용을 더한다.
  • 단순 할당(=), 산술 연산(+), 비교(>)는 O(1)이다.
  • 2. 실전 예제 분석

    예제 1: 덧셈 (상수 무시)

    typescript
    function printCount(n) {
      for (let i = 0; i < n; i++) { // n번 반복
        console.log(i);
      }
    
      for (let j = 0; j < n; j++) { // n번 반복
        console.log(j);
      }
    }
  • 첫 번째 반복문: N번
  • 두 번째 반복문: N번
  • 합계: N + N = 2N
  • 빅오: 상수를 버리고 O(N)이 된다.
  • 예제 2: 곱셈 (중첩 반복)

    typescript
    function printPairs(n) {
      for (let i = 0; i < n; i++) {       // n번 반복
        for (let j = 0; j < n; j++) {   // 안에서 또 n번 반복
          console.log(i, j);
        }
      }
    }
  • 바깥쪽 루프가 한 번 돌 때마다, 안쪽 루프는 N번 전체를 돈다.
  • 합계: N times N = N^2
  • 빅오: O(N^2) (이차 시간)
  • 예제 3: 입력이 다를 때 (N vs M)

    typescript
    function processItems(arr1, arr2) {
      arr1.forEach(item => console.log(item)); // a번 반복
      arr2.forEach(item => console.log(item)); // b번 반복
    }
  • 입력받는 배열이 다르므로, 무작정 N이라고 퉁치면 안 된다.
  • 빅오: O(A + B) (두 배열 크기의 합)
  • 만약 중첩 반복문이었다면? O(A times B)가 된다.
  • 3. 크리스의 지적: 숨겨진 복잡도 찾기

    이제 크리스가 지적했던 동료의 코드를 보자. 반복문은 분명 하나다.

    typescript
    // ⚠️ 함정 카드 발동
    function findCommon(arr1, arr2) {
      // arr1을 순회 (N번)
      arr1.forEach(item1 => {
        // arr2에 item1이 있는지 확인 (includes)
        if (arr2.includes(item1)) { 
          console.log("찾았다:", item1);
        }
      });
    }

    왜 이 코드가 O(N^2)일까?

    범인은 includes() 메서드다.

    우리가 1편에서 배웠듯, 자바스크립트의 includes, indexOf, slice 등은 내부적으로 배열을 처음부터 끝까지 뒤지는 반복문(O(N))이다.

    즉, 이 코드는 for 문 안에 for 문이 들어있는 것과 똑같다.

  • 바깥쪽: arr1 순회 (N)
  • 안쪽: includes 실행 (M)
  • 빅오: O(N times M). 만약 두 배열 크기가 같다면 O(N^2)이다.
  • 해결책:

    arr2를 Set(해시 테이블)이나 객체로 바꾸면 검색을 O(1)로 줄일 수 있다. 그러면 전체 복잡도는 O(N)으로 내려간다. 이것이 알고리즘 최적화의 기본 패턴이다.

    4. 1장을 마치며: 알고리즘적 사고 장착 완료

    지금까지 우리는 알고리즘의 기초 체력을 다졌다.

  • 알고리즘과 자바스크립트: 왜 싱글 스레드 환경에서 효율성이 생명인지 깨달았다.
  • 프로토타입 메서드 비용: shiftincludes 같은 편리한 함수 뒤에 숨겨진 비용(O(N))을 알게 되었다.
  • 빅오와 시간/공간 복잡도: O(1)부터 O(N^2)까지, 코드의 효율성을 객관적으로 측정하는 눈을 길렀다.
  • 빅오 계산: 코드를 보고 "이건 위험해!"라고 판단할 수 있게 되었다.
  • 이제 크리스는 코드를 짤 때 단순히 "돌아가게" 만드는 것에 만족하지 않는다.

    "이 데이터가 100만 개라면?"이라는 질문을 던지고, 시간과 공간 효율성을 따져가며 더 나은 로직을 고민하는 엔지니어가 되었다.

    다음 장부터는 본격적인 무기들을 수집할 차례다.

    데이터를 순서대로 나열하는 것만으로도 검색 속도를 획기적으로 높일 수 있다. 개발자 면접의 단골 손님, 정렬(Sorting) 알고리즘의 세계로 떠나보자.

    정렬 알고리즘이란 편에서 계속된다.


    🔗 참고 링크

  • Big-O Cheat Sheet (Data Structures)
  • time complexity and Big-O notation
  • 댓글 (0)

    0/1000
    댓글을 불러오는 중...