DFS (깊이 우선 탐색)

// 재귀 방식
function dfs(graph, node, visited = new Set()) {
  visited.add(node)
  console.log(node)
  graph[node].forEach((neighbor) => {
    if (!visited.has(neighbor)) dfs(graph, neighbor, visited)
  })
}

// 스택 방식
function dfsStack(graph, startNode) {
  const stack = [startNode]
  const visited = new Set()

  while (stack.length > 0) {
    const node = stack.pop()

    if (!visited.has(node)) {
      console.log(node)
      visited.add(node)
      graph[node]
        .slice()
        .reverse()
        .forEach((neighbor) => {
          if (!visited.has(neighbor)) stack.push(neighbor)
        })
    }
  }
}

const graph = {
  0: [1, 2],
  1: [0, 3, 4],
  2: [0, 5],
  3: [1],
  4: [1],
  5: [2],
}
dfs(graph, 0) // 0, 1, 3, 4, 2, 5
#344

캘린더 이벤트 겹침 처리 알고리즘

겹치는 이벤트들을 나란히 배치하기 위한 레인 할당: 시작 순으로 보며 비어 있는 가장 낮은 레인에 넣으면, 레인 수가 동시에 겹치는 최대 개수와 같아진다(구간 그래프의 탐욕 색칠).1

interface Event {
  id: string
  start: number
  end: number
}
interface PositionedEvent extends Event {
  lane: number
  totalLanes: number
}

function calculatePositions(events: Event[]): PositionedEvent[] {
  // 1. 시작 시간 순 정렬
  const sorted = [...events].sort((a, b) => a.start - b.start || a.end - b.end)

  // 2. 겹침 그룹 찾기 + 레인 할당
  const result: PositionedEvent[] = []
  let group: Event[] = []
  let groupEnd = 0

  sorted.forEach((event) => {
    if (group.length === 0 || event.start < groupEnd) {
      group.push(event)
      groupEnd = Math.max(groupEnd, event.end)
    } else {
      processGroup(group, result)
      group = [event]
      groupEnd = event.end
    }
  })
  if (group.length) processGroup(group, result)

  return result
}

function processGroup(group: Event[], result: PositionedEvent[]) {
  const activeLanes: number[] = []

  group.forEach((event) => {
    // 사용 가능한 가장 낮은 레인 찾기
    let lane = activeLanes.findIndex((end) => end <= event.start)
    if (lane === -1) lane = activeLanes.length

    activeLanes[lane] = event.end
    result.push({ ...event, lane, totalLanes: 0 })
  })

  // totalLanes 업데이트
  const maxLane =
    Math.max(...result.slice(-group.length).map((e) => e.lane)) + 1
  result.slice(-group.length).forEach((e) => (e.totalLanes = maxLane))
}

// CSS 적용: left = lane/totalLanes * 100%, width = 100%/totalLanes

Footnotes

  1. Interval graph - Wikipedia: “An optimal graph coloring of the interval graph represents an assignment of resources that covers all of the requests with as few resources as possible; it can be found in polynomial time by a greedy coloring algorithm that colors the intervals in sorted order by their left endpoints.” ↩

#510

Fiber 가 렌더링을 쪼갤 수 있는 이유 — 콜 스택 대신 세 포인터

노드마다 child(첫 자식), sibling(다음 형제), return(부모) 세 링크가 걸려 있다. App 의 child 는 Header, Header 의 sibling 은 Main, Main 의 child 는 List, 각자의 return 은 부모다. 규칙은 넷: child 가 있으면 내려가고, root 에 도달하면 끝내고, sibling 이 없으면 return 으로 올라가며 반복하고, sibling 이 있으면 옆으로 간다. 내려가는 길이 beginWork, 올라오는 길이 completeWork 라 한 노드를 두 번 지난다. 순서: begin App → begin Header → complete Header → begin Main → begin List → complete List → complete Main → complete App.

재귀로 순회하면 어디까지 했는지가 JS 콜 스택의 프레임(App → Main → List)에 쌓여 있어, 도중에 양보하려고 함수를 빠져나오면 그 프레임이 풀려나간다. 세 포인터로 순회하면 진행 상태가 workInProgress 포인터 하나로 줄어든다 — 노드마다 걸린 링크를 직접 따라가므로 호출 스택이 필요 없고, 멈추면 그 포인터만 남아 거기서 다시 시작한다.


Footnotes

  1. “Nothing in Fiber uses the normal JS stack.” / 스택을 다시 쌓는 비용보다 중단이 문제다 — “…we would spend some time unwinding and rebuilding the stack each time. It is not too bad. However, everything along the stack would have to be aware of how to “unwind” when we abort in the middle of the work flow.“ 다만 원문도 Fiber 사정이라고 못박는다 — ”…a few unique design patterns that apply to Fiber, but not necessarily anything else.“ ↩

#560