JavaScript AVL 트리: 높이 불변식과 LL·RR·LR·RL 삽입 회전

2026.09.10·수정 2026.09.13·약 26분·작성: 해비·블로그 소개

AVL: 검색 순서는 지키면서 기울어진 가지를 바로잡습니다

작은 트리의 회전 전후를 비교하며 AVL을 배웁니다. 가운데 가지의 이동과 높이 갱신 순서를 먼저 익히고 네 회전과 전체 구현으로 연결합니다.

먼저 작은 예제로 원리를 익히고, 마지막에 전체 구현을 펼쳐 보세요. 앞쪽 예제는 각각 독립적으로 브라우저 개발자 도구 Console에서 실행합니다. 같은 이름을 다시 선언했다는 오류가 나오면 새로고침 후 해당 예제를 실행하세요. 출력 뒤에 콘솔이 별도로 보여주는 undefined는 마지막 명령의 반환값일 수 있습니다.

일곱 노드로 이루어진 균형 이진 트리 개념 표지
주제를 표현한 개념 표지입니다. 정확한 동작과 값의 변화는 아래 코드와 표에서 확인하세요.

1. 번호 세 개가 한쪽으로만 이어졌습니다

등록번호를 작은 것은 왼쪽, 큰 것은 오른쪽에 놓는 BST를 사용한다고 해 보겠습니다. 30,20,10 순서로 넣으면 30의 왼쪽에 20, 그 왼쪽에 10이 놓입니다. 10을 찾으려면 30 → 20 → 10을 따라갑니다. 이런 모양이 계속 길어지면 트리여도 한 줄 목록을 훑는 것과 비슷해집니다.

AVL은 값을 추가한 뒤 한쪽이 너무 길어졌는지 살펴보고 연결을 고치는 BST입니다. 먼저 키의 크기 관계를 유지하면서 세 노드의 연결을 바꿔 보겠습니다. 높이 공식이나 네 회전 이름을 한꺼번에 외우지는 않아도 됩니다.

노드 왼쪽 자식 오른쪽 자식
30: 현재 시작점 20 없음
20 10 없음
10 없음 없음

아래 짧은 코드는 블록마다 독립적인 실험입니다. 브라우저 개발자 도구 Console에 전체를 붙여 넣거나 파일로 저장해 Node.js로 실행하세요. 같은 변수의 재선언 오류가 나오면 새로고침 후 그 블록을 실행합니다. 바로 아래 상자는 console.log 출력입니다.

const ten = { key: 10, left: null, right: null };
const twenty = { key: 20, left: ten, right: null };
const thirty = { key: 30, left: twenty, right: null };
console.log(thirty.key);
console.log(thirty.left.key);
console.log(thirty.left.left.key);
30
20
10

ten, twenty, thirty는 서로 다른 객체입니다. twenty.left가 ten을, thirty.left가 twenty를 가리킵니다. 마지막 세 줄은 시작점의 키, 한 번 왼쪽으로 간 키, 두 번 왼쪽으로 간 키를 출력합니다. 노드의 값을 바꾼 것이 아니라 객체끼리 연결한 구조입니다.

2. 가운데 20을 올리면 세 값의 순서는 그대로입니다

30을 시작점으로 유지하는 대신 20을 시작점으로 올려 보세요. 10은 20의 왼쪽에 그대로 두고, 30을 20의 오른쪽으로 연결합니다. 결과는 20의 양쪽에 10과 30이 있는 모양입니다. 작은 값이 왼쪽, 큰 값이 오른쪽이라는 약속은 그대로입니다.

단계 시작점 20의 왼쪽 / 오른쪽 30의 왼쪽
30 10 / 없음 20
20의 오른쪽에 30 연결 아직 연결 변경 중 10 / 30 아직 20
30의 왼쪽에서 옛 20 연결을 끊음 20으로 교체 10 / 30 없음

두 번째 단계는 잠시 서로를 가리키는 중간 상태입니다. 두 대입이 끝나기 전에 트리를 순회하지 않습니다. 최종적으로 30.left까지 정리해야 20 → 30 → 20으로 계속 돌아가는 연결이 남지 않습니다. 코드는 완성된 연결 상태를 한 묶음으로 만드는 작업입니다.

이 동작을 30에서의 오른쪽 회전이라고 부릅니다. 아래 값 10을 찾아 떠돌게 하거나 키를 정렬해서 새로 만드는 것이 아닙니다. 기존 객체 사이의 연결만 바꿉니다. 30에서 왼쪽, 다시 왼쪽으로 길어진 첫 상황을 LL이라고 부르지만, 우선 “중간 20이 올라오고 30이 오른쪽으로 내려간다”를 기억하세요.

3. 20의 오른쪽에 25가 있었다면 어디로 옮길까요?

이번에는 30의 왼쪽에 20, 20의 왼쪽에는 10→5, 오른쪽에는 25가 있는 모양입니다. 20을 올릴 때 그 오른쪽 자리를 30이 차지합니다. 원래 그 자리에 있던 25를 잃어버리면 안 됩니다. 25는 20보다 크고 30보다 작으므로 30의 왼쪽으로 옮기면 두 관계를 모두 지킵니다.

연결 회전 전 회전 후
전체 시작점 30 20
20.left 10, 그 왼쪽에 5 그대로 10
20.right 25 30
30.left 20 25
25의 자리 의미 20보다 큰 쪽 30보다 작은 쪽이며 여전히 20보다 큼
const x = { key: 20, left: { key: 10, left: { key: 5 } }, right: { key: 25 } };
const y = { key: 30, left: x, right: null };
const middle = x.right;
x.right = y;
y.left = middle;
const root = x;
console.log(root.key);
console.log(root.right.key);
console.log(root.right.left.key);
20
30
25

첫 두 줄은 이 모양의 객체를 만듭니다. 짧게 보기 위해 끝 노드의 사용하지 않는 자식 필드는 생략했습니다. const middle = x.right는 연결을 덮어쓰기 전에 25 객체를 기억하는 줄입니다. x가 20, y가 30, middle이 25라고 바꿔 읽어 보세요.

x.right = y로 20의 오른쪽을 30으로 바꾸고, y.left = middle로 30의 왼쪽에 25를 붙입니다. 마지막 root = x는 새 시작점을 20으로 정합니다. 출력 20,30,25는 새 시작점, 그 오른쪽, 다시 그 왼쪽 순서입니다. middle을 먼저 보관하지 않으면 x.right를 바꾼 뒤에는 그 표현으로 원래 25를 찾을 수 없습니다.

10과 5는 이번 연결 변경에 직접 등장하지 않습니다. 20.left를 그대로 두었으므로 그 아래 가지 전체가 보존됩니다. 회전은 트리 모든 곳을 다시 잇는 작업이 아니라 몇 개의 연결만 바꾸는 작업입니다.

4. 높이는 내려간 30부터 다시 적습니다

모양을 바꾼 뒤에는 각 노드가 기억한 높이도 고쳐야 합니다. 여기서는 빈 가지 높이를 0, 노드 하나만 있는 가지를 1로 셉니다. 높이 2인 가지 아래에 부모를 붙이면 부모의 높이는 3입니다. 양쪽 길이가 다르면 긴 쪽에 1을 더합니다.

위 예제의 가지 회전 전 높이 회전 후 높이
10→5 가지 2 2: 연결 그대로
25 하나 1 1: 노드 그대로
30을 시작으로 하는 가지 4 2: 왼쪽에 25 하나
20을 시작으로 하는 가지 3 3: 왼쪽 높이 2, 오른쪽 30의 높이 2

30이 이제 아래로 내려갔으므로 30의 새 높이 2를 먼저 계산합니다. 다음에 20은 그 값을 읽어 자신의 높이를 계산합니다. 20부터 계산하면 아직 남아 있는 30의 옛 높이 4를 읽어 5라고 잘못 기록합니다.

function height(node) {
  return node === null ? 0 : node.height;
}
const a = { height: 2 };
const middle = { height: 1 };
const y = { left: middle, right: null, height: 4 };
const x = { left: a, right: y, height: 3 };
y.height = 1 + Math.max(height(y.left), height(y.right));
x.height = 1 + Math.max(height(x.left), height(x.right));
console.log(y.height);
console.log(x.height);
2
3

이 실험은 높이 계산에 필요한 기록만 따로 만든 것입니다. a의 height:2는 10→5 가지를, middle의 height:1은 25 가지를 나타냅니다. 키와 아래쪽 연결을 모두 다시 만들지 않고 이미 계산된 자식 높이를 읽는 과정만 봅니다.

height 함수는 빈 가지 null이면 0, 노드가 있으면 저장한 height를 반환합니다. y.height는 max(1,0)+1=2가 되고, 그다음 x.height는 max(2,2)+1=3이 됩니다. 전체 코드의 update(y), update(x)가 이 순서를 사용합니다. 회전의 방향만 맞아도 높이 기록이 틀리면 다음 삽입에서 잘못 판단할 수 있습니다.

5. 꺾인 모양은 먼저 펴고, 그다음 올립니다

30,10,20 순서로 넣으면 30의 왼쪽에 10, 10의 오른쪽에 20이 생깁니다. 왼쪽으로 갔다 오른쪽으로 꺾이므로 LR입니다. 이 모양은 먼저 10에서 왼쪽 회전해 20을 올리고, 이어 30에서 오른쪽 회전합니다. 낯선 새 동작 두 개가 아니라 같은 연결 변경을 두 번 하는 것입니다.

단계 30의 왼쪽 20의 왼쪽 / 오른쪽 전체 시작점
시작: 30의 왼쪽 10의 오른쪽 20 10 없음 / 없음 30
첫 회전: 10 대신 20을 그 자리로 20 10 / 없음 30
두 번째 회전: 30 대신 20을 위로 없음 10 / 30 20
const twenty = { key: 20, left: null, right: null };
const ten = { key: 10, left: null, right: twenty };
const thirty = { key: 30, left: ten, right: null };
ten.right = twenty.left;
twenty.left = ten;
thirty.left = twenty;
thirty.left = twenty.right;
twenty.right = thirty;
const root = twenty;
console.log(root.key);
console.log(root.left.key);
console.log(root.right.key);
20
10
30

ten.right를 twenty.left로 바꾸고 twenty.left를 ten으로 정하는 두 줄이 첫 회전입니다. 그 결과를 thirty.left에 연결해야 30에서 새 가지로 내려갈 수 있습니다. 이 예제의 옮길 중간 가지는 null이지만, 전체 회전 함수는 가지가 있는 경우도 같은 규칙으로 보존합니다.

다음 두 줄은 30.left를 20의 옛 오른쪽으로 정리하고 20.right를 30으로 바꾸는 두 번째 회전입니다. 마지막에 root가 20이 되어 좌우에 10과 30이 놓입니다. 이 짧은 예제는 연결 변화만 관찰하므로 높이 계산은 생략했습니다. 실제 AVL 삽입에서는 각 회전 뒤 앞 절의 순서로 높이도 갱신합니다.

30,10,20을 넣은 LR 상황에서 첫 회전만 마쳤습니다. 전체 루트도 이미 20일까요?

답과 이유 확인하기

아닙니다. 첫 회전은 30의 왼쪽 가지 안에서만 일어납니다. 전체 루트는 아직 30이고, 30.left가 20이 됩니다. 두 번째로 30을 오른쪽 회전한 뒤 전체 루트가 20으로 바뀝니다.

6. 이제 전체 삽입 코드를 읽을 준비가 되었습니다

전체 코드의 insert는 먼저 일반 BST처럼 내려가 새 노드를 붙입니다. 돌아오는 길에 양쪽 가지 높이를 비교합니다. 차이가 1 이하면 그대로 두고, 한쪽이 2만큼 길어지면 알맞은 회전을 합니다. 이것이 “값의 순서에 맞게 삽입한 다음 너무 기울어진 연결을 고친다”는 흐름입니다.

rotateRight는 20·30·25 실험의 연결 변경에 높이 갱신을 붙인 함수입니다. rotateLeft는 좌우를 바꾼 동작입니다. LR은 왼쪽 자식에서 rotateLeft를 한 뒤 현재 노드에서 rotateRight를 하는 순서이고, RL은 그 반대입니다. LL·RR·LR·RL 표는 이 두 기본 동작을 이해한 뒤 정리표로 사용하세요.

함수가 돌려준 새 루트를 부모의 left 또는 right에 다시 넣는 줄을 놓치지 마세요. 가장 위에서는 this.root도 교체합니다. 앞의 작은 예제는 이미 만들어진 특정 모양을 수동으로 고쳤지만, 전체 버전은 어떤 삽입에서 어느 가지를 고칠지 선택하고 높이를 유지합니다. 회전 예제 세 줄만으로 범용 AVL이 완성되는 것은 아닙니다.

이 글의 전체 구현은 안전한 정수의 집합입니다. 중복 등록은 무시하며 삭제는 지원하지 않습니다. has는 값의 존재, ceiling은 기준 이상 중 가장 작은 값, toArray는 정렬된 전체 목록을 돌려줍니다. 우선 삽입과 회전만 읽고, 순서 질의와 높이의 시간 복잡도 증명은 아래에서 차례로 확장하세요.

관련 코딩테스트 문제로 이어서 연습합니다

이 자료구조의 블로그 연습 문제와 해설에서 배운 동작을 적용해 보세요. 먼저 작은 예시를 직접 처리한 뒤 입력 전체를 다루는 코드로 확장하면 됩니다. 아래 전체 구현은 연결된 문제의 기존 메서드와 반환 형식을 유지합니다.

전체 구현 · 상세 설명 · 예외와 성능 분석 펼치기

여기부터는 필요한 기능을 골라 읽는 참고 영역입니다. 새로운 파일로 전체 코드를 실행할 때는 아래 Node.js 실행 안내를 따르세요. 앞쪽의 작은 브라우저 실습과 전체 파일을 한 콘솔에 이어 붙이지 마세요. 기능별 입력 조건과 반환 형식은 아래 설명을 기준으로 합니다.

문제 상황과 API 계약

일반 BST에 10,20,30,40을 순서대로 넣으면 오른쪽으로만 이어져 검색이 선형 시간이 될 수 있습니다. AVL은 순서 불변식에 높이 조건을 추가해 이런 편향을 막습니다. 키가 순서대로 들어올 수 있고 매번 특정 값 이상인 가장 작은 키를 찾아야 한다면 정렬되지 않은 Map만으로는 답하기 어렵습니다.

이 구현은 안전한 정수 키를 저장하는 집합입니다. 중복 키는 추가하지 않고 add는 새 키가 들어갔을 때만 true를 반환합니다. has는 포함 여부, ceiling(x)는 x 이상인 최소 키 또는 null, toArray는 오름차순 배열을 반환합니다. 삭제·키별 값 저장·동일 키의 빈도 카운터는 구현하지 않습니다.

높이는 빈 자식 0, 잎 1로 정의합니다. 다른 자료의 빈 트리 높이 −1 규칙과 섞으면 회전 기준이 어긋납니다. 노드와 root는 설명을 위해 외부에서 볼 수 있지만 직접 수정하지 않아야 하며, 내부 insert와 회전 메서드는 공개 입력 검증용 API가 아닙니다.

정확성을 지키는 불변식

모든 노드에서 왼쪽 키는 node.key보다 작고 오른쪽 키는 큽니다. 중복은 새 노드로 만들지 않으므로 엄격한 부등호를 쓸 수 있습니다. 동시에 node.height는 1+max(왼쪽 높이,오른쪽 높이)이고, 균형 인수 balance=왼쪽 높이−오른쪽 높이는 −1,0,1이어야 합니다.

삽입은 먼저 일반 BST와 같은 한 경로를 내려갑니다. 재귀가 돌아올 때 그 경로의 높이를 다시 계산하고 균형을 고칩니다. 회전은 중위 순서를 바꾸지 않고 부모·자식 관계만 재배치합니다. 오른쪽 회전에서 x.right였던 middle은 y.left로 옮겨야 모든 키가 x< middle <y 순서를 보존합니다.

높이 h의 AVL이 가질 수 있는 최소 노드 수는 N(h)=1+N(h−1)+N(h−2)입니다. 높이 차이를 최대 1까지 허용하는 가장 작은 트리를 세는 식입니다. 이 수는 높이에 대해 지수적으로 증가하므로 n개 노드의 높이는 O(log n)입니다. 따라서 최악 입력 순서에서도 삽입 경로와 조회 경로가 로그 길이를 유지합니다.

전체 구현과 실행

Node.js 24의 CommonJS 환경을 기준으로 합니다. 아래 전체 코드를 avl.cjs로 저장하고 파일이 있는 폴더에서 node avl.cjs를 실행하세요. 외부 패키지는 필요하지 않습니다. module.exports는 다른 파일에서 가져올 때 사용하며 require.main 조건 안의 호출은 직접 실행할 때만 작동합니다.

class AVLSet {
  constructor() {
    this.root = null;
    this.size = 0;
  }
  check(key) {
    if (!Number.isSafeInteger(key)) throw new TypeError('키는 안전한 정수여야 합니다.');
  }
  height(node) { return node?.height ?? 0; }
  update(node) {
    node.height = 1 + Math.max(this.height(node.left), this.height(node.right));
  }
  rotateRight(y) {
    const x = y.left, middle = x.right;
    x.right = y;
    y.left = middle;
    this.update(y);
    this.update(x);
    return x;
  }
  rotateLeft(x) {
    const y = x.right, middle = y.left;
    y.left = x;
    x.right = middle;
    this.update(x);
    this.update(y);
    return y;
  }
  add(key) {
    this.check(key);
    const before = this.size;
    this.root = this.insert(this.root, key);
    return this.size !== before;
  }
  insert(node, key) {
    if (!node) {
      this.size++;
      return { key, left: null, right: null, height: 1 };
    }
    if (key < node.key) node.left = this.insert(node.left, key);
    else if (key > node.key) node.right = this.insert(node.right, key);
    else return node;
    this.update(node);
    const balance = this.height(node.left) - this.height(node.right);
    if (balance > 1) {
      if (key > node.left.key) node.left = this.rotateLeft(node.left);
      return this.rotateRight(node);
    }
    if (balance < -1) {
      if (key < node.right.key) node.right = this.rotateRight(node.right);
      return this.rotateLeft(node);
    }
    return node;
  }
  has(key) {
    this.check(key);
    let node = this.root;
    while (node) {
      if (key === node.key) return true;
      node = key < node.key ? node.left : node.right;
    }
    return false;
  }
  ceiling(key) {
    this.check(key);
    let node = this.root, candidate = null;
    while (node) {
      if (node.key >= key) {
        candidate = node.key;
        node = node.left;
      } else node = node.right;
    }
    return candidate;
  }
  toArray() {
    const result = [];
    const visit = (node) => {
      if (!node) return;
      visit(node.left);
      result.push(node.key);
      visit(node.right);
    };
    visit(this.root);
    return result;
  }
}

module.exports = { AVLSet };

if (require.main === module) {
  const t = new AVLSet();
  [30, 10, 20, 20].forEach(key => t.add(key));
  console.log(JSON.stringify({ root: t.root.key, sorted: t.toArray(), ceiling: t.ceiling(15) }));
}

직접 실행 출력:

{"root":20,"sorted":[10,20,30],"ceiling":20}

rotateRight(y)는 y.left를 새 루트 x로 올립니다. 링크를 바꾼 뒤 아래로 내려간 y의 높이를 먼저 고치고 새 루트 x를 고칩니다. 순서를 반대로 하면 x가 아직 낡은 y.height를 읽습니다. rotateLeft도 같은 이유로 아래 노드부터 갱신합니다.

insert의 balance>1 분기에서 새 key가 node.left.key보다 크면 LR입니다. 먼저 왼쪽 자식을 왼쪽으로 회전해 LL 모양으로 바꾸고 node를 오른쪽으로 회전합니다. balance<−1의 대칭 조건은 RL을 처리합니다. 삽입된 키를 이용한 이 판별은 삽입용이며 삭제에 그대로 재사용하면 안 됩니다.

add가 this.root=this.insert(…)를 수행하는 것은 회전 후 루트가 바뀔 수 있기 때문입니다. 재귀 호출 결과를 node.left 또는 node.right에 다시 대입하는 것도 같은 이유입니다. 회전을 계산만 하고 반환 루트를 연결하지 않으면 새로운 구조가 트리에 반영되지 않습니다.

ceiling은 node.key가 기준 이상이면 후보를 기록하고 더 작은 답이 있을 수 있는 왼쪽으로 갑니다. 기준 미만이면 왼쪽 전체도 작으므로 오른쪽으로 갑니다. 현재 후보보다 더 좋은 가능성이 남은 경로 하나만 따라가므로 정렬 배열을 전부 만들 필요가 없습니다. toArray는 학습용 확인과 결과 나열에만 사용합니다.

상태 변화 따라가기

입력 순서 불균형 형태 처리 최종 루트 / 중위 순회
30,20,10 LL: 왼쪽의 왼쪽 30에서 오른쪽 회전 20 / [10,20,30]
10,20,30 RR: 오른쪽의 오른쪽 10에서 왼쪽 회전 20 / [10,20,30]
30,10,20 LR: 왼쪽의 오른쪽 10 왼쪽 회전 → 30 오른쪽 회전 20 / [10,20,30]
10,30,20 RL: 오른쪽의 왼쪽 30 오른쪽 회전 → 10 왼쪽 회전 20 / [10,20,30]
20 재삽입 중복 기존 노드 반환 크기 3, 높이 그대로

복잡도와 적용하지 말아야 할 경우

작업 최악 시간 공간 / 설명
add / has / ceiling O(log(n+1)) 균형 불변식으로 최악 보장, 평균·상환 가정 아님
삽입 재균형 단일 회전 최대 2번 높이 갱신과 탐색은 여전히 O(log n)
toArray O(n) 출력 O(n), 재귀 스택 O(log(n+1))
전체 저장 / add 스택 O(n) / O(log(n+1)) 노드마다 키·두 자식·높이

삭제는 없습니다. 삭제 후에는 높이가 줄어 여러 조상에서 다시 균형이 깨질 수 있고, 삽입 키 방향으로 회전을 분류할 수도 없습니다. add 코드의 부등호만 바꿔 delete를 만들지 마세요. 삭제가 필수이면 그 연산까지 검증된 균형 트리 구현을 사용해야 합니다.

숫자가 아닌 키를 지원하려면 일관된 전순서 비교자를 도입해야 합니다. NaN처럼 비교에서 양쪽 모두 false가 되는 값을 허용하면 중복 판정과 검색이 깨집니다. 이 코드는 Number.isSafeInteger로 그런 값을 거부합니다.

모든 키가 한 번에 주어지고 이후 삽입이 없다면 정렬 배열과 이진 탐색이 더 간단합니다. 정확 포함 여부만 필요하면 Set도 대안입니다. AVL은 지속적인 삽입과 순서 질의를 함께 요구할 때 비용을 지불할 가치가 있습니다. 문자열 비교가 긴 경우처럼 비교 자체가 O(1)이 아니면 표의 시간에 비교 비용도 반영해야 합니다.

연습으로 확인하기

번호를 온라인으로 등록하면서 기준 이상의 가장 작은 등록번호를 찾아 보세요. 단순 has와 달리 순서 질의가 균형 BST를 선택하는 이유를 보여 줍니다.

[창작 문제] 기준 이상 등록번호 안내

LeetCode 1382 – Balance a Binary Search Tree — 기존 BST를 균형 트리로 바꾸는 공식 문제입니다. 매 삽입마다 균형을 유지하는 이 AVL 구현과 동일한 과제가 아니므로, 정렬 순회와 재구성 대안을 비교하는 연습으로 사용하세요.

공식 자료

공식 자료 확인일: 2026년 9월 10일. 구현은 이 글의 JavaScript 계약에 맞춰 독립적으로 작성했습니다. 다른 언어 라이브러리의 API와 숫자 범위가 그대로 적용되는 것은 아닙니다.

직접 실습: 연산 비용으로 구조를 설명합니다

실습 주제: JavaScript AVL 트리: 높이 불변식과 LL·RR·LR·RL 삽입 회전

  1. 본문 구현에서 저장되는 값과 연결 관계를 그림으로 적습니다.
  2. 조회·삽입·삭제 중 이 구조가 가장 자주 수행할 연산을 고릅니다.
  3. 연산 전후에도 유지되어야 하는 규칙을 한 문장으로 적습니다.
  4. 배열이나 Map 같은 다른 구조로 바꿨을 때 시간·공간 비용을 비교합니다.
풀이 기준과 확인 결과

메서드 이름만 외우지 말고 한 번의 연산에서 어떤 값과 연결이 바뀌는지 추적하세요. 빈 구조, 원소 한 개, 중복값, 연속 삽입·삭제를 실행했을 때 본문이 설명한 불변식이 유지되면 성공입니다.

테스트 체크리스트

  • 빈 구조에 대한 조회·삭제 처리
  • 첫 원소와 마지막 원소 변경
  • 중복값 또는 동일 우선순위 처리
  • 입력 크기가 커졌을 때 예상 복잡도 유지

이 글이 도움이 되었나요?

조회 중

자료구조 학습 순서

필수 18개 · 전체 18개

읽음 기록 관리

전체 과정 목차 (18개)
  1. 필수 학습 · 자료구조 선택 가이드: 연산 비용으로 배열·스택·큐·Set 고르기
  2. 필수 학습 · JavaScript 배열: 인덱스 조회와 삽입·삭제 비용
  3. 필수 학습 · JavaScript Map·Set: 값 조회와 중복 제거 실습
  4. 필수 학습 · 자료구조 스택 쉽게 이해하기: push pop으로 문제 풀이 감 잡기
  5. 필수 학습 · 큐와 FIFO: head 인덱스로 JavaScript 대기열 만들기
  6. 필수 학습 · 단방향 연결 리스트: head·tail 삽입과 삭제
  7. 필수 학습 · JavaScript 원형 덱 구현: 양끝 삽입·삭제와 고정 용량 버퍼
  8. 필수 학습 · JavaScript 문자열 해시 테이블 구현: 충돌 처리와 리사이즈, NFC 정규화
  9. 필수 학습 · 트리 자료구조 차이: 이진 트리 BST MST 구분하기
  10. 필수 학습 · JavaScript 이진 탐색 트리 구현: 중복 키와 세 가지 삭제 처리
  11. 필수 학습 · JavaScript 최소 힙 구현: 우선순위 큐의 push·pop과 비교 함수
  12. 필수 학습 · JavaScript 그래프 구현: 인접 리스트·인접 행렬 비교와 BFS
  13. 필수 학습 · JavaScript Union-Find: 경로 압축과 크기 합치기로 연결 상태 관리하기
  14. 필수 학습 · JavaScript Trie: Unicode 접두사 검색과 안전한 삭제 구현
  15. 필수 학습 · JavaScript Fenwick Tree: lowbit로 구간 합과 단일 증가 갱신 구현
  16. 필수 학습 · JavaScript 반복형 세그먼트 트리: 구간 합·단일 대입·결합 순서
  17. 필수 학습 · JavaScript LRU 캐시: Map과 이중 연결 리스트의 불변식
  18. 필수 학습 · JavaScript AVL 트리: 높이 불변식과 LL·RR·LR·RL 삽입 회전 현재 글

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기