Skip to content

[Bona1122] 25.01.23#36

Merged
JooKangsan merged 6 commits intocodingTestStd:mainfrom
bona1122:bona1122
Feb 6, 2025
Merged

[Bona1122] 25.01.23#36
JooKangsan merged 6 commits intocodingTestStd:mainfrom
bona1122:bona1122

Conversation

@bona1122
Copy link
Collaborator

[해시]

해시 문제를 풀며 리마인드했던 내용들과 얻은 인사이트를 정리했습니다.

해시를 활용해야 하는 상황들을 정리해봤습니다:

  1. 데이터의 빠른 검색이 필요한 경우
    배열에서의 검색은 O(n)이 걸리지만, 해시는 O(1)로 접근 가능
    ex)[오픈채팅방]문제에서 유저아이디로 닉네임 조회
  2. 데이터의 개수를 세어야 하는 경우
    출현 빈도나, 중복된 요소를 찾을 때 사용
  3. 데이터를 그룹화/매핑해야 하는 경우
    연관된 데이터를 매핑할 때 (예: 이름과 점수 매핑, 키패드 문제 좌표 매핑)

그 외...

  • 맵으로 요소 개수 카운팅 할 때, (map[key] || 0) + 1 패턴이 유용했습니다.
// Before
if (map[key] === undefined) {
  map[key] = 1
} else {
  map[key] += 1
}

// After
map[key] = (map[key] || 0) + 1
  • '할인 행사'문제를 풀며 슬라이딩 윈도우 알고리즘에 대해 배웠습니다.

슬라이딩 윈도우

📌 알고리즘 개요

슬라이딩 윈도우는 배열이나 리스트의 일부 연속된 구간을 윈도우로 지정하고, 이 윈도우를 밀어가며 문제를 해결하는 알고리즘입니다. 투 포인터 알고리즘의 특수한 형태로, 구간의 크기가 고정되어 있다는 특징이 있습니다.

기본 구현 패턴

function optimizedSlidingWindow(arr, k) {
    // 1. 초기값 계산
    let sum = 0;
    for (let i = 0; i < k; i++) {
        sum += arr[i];
    }
    
    let maxSum = sum;
    
    // 2. 슬라이딩하며 값 갱신
    for (let i = k; i < arr.length; i++) {
        sum = sum + arr[i] - arr[i-k];  // O(1) 연산
        maxSum = Math.max(maxSum, sum);
    }
    
    return maxSum;
}

📌 푼 문제


📝 간단한 풀이 과정

완주하지 못한 선수

  • 완주자 목록으로 해시맵을 생성하고, 참가자 목록을 순회하며 완주하지 못한 선수를 찾는 방식으로 구현했습니다.
  • 그리고 동명이인이 가능해서 카운트를 기록했습니다.
// 완주자 목록으로 해시맵 생성
const solution = (participant, completion) => {
  const completionMap = completion.reduce((map, person) => {
    map[person] = (map[person] || 0) + 1
    return map
  }, {})

  return participant.find((person) => {
    if (completionMap[person]) {
      completionMap[person]--
      return false
    }
    return true
  })
}

폰켓몬

  • 중복을 제거한, "종류"를 카운트 하기위해, 해시맵을 활용했습니다.
  • Set을 이용해서도 중복을 제거할 수 있어서, Set을 활용하는 방법도 추가했습니다.
// 1. 해시맵을 이용한 풀이
const solution1 = (nums) => {
  let max = 0
  const can = nums.length / 2
  const map = nums.reduce((map, num) => {
    if (map[num] === undefined) {
      max++
      map[num] = 1
    }
    return map
  }, {})
  return can < max ? can : max
}

// 2. Set을 이용한 풀이
const solution2 = (nums) => {
  const can = nums.length / 2
  const max = [...new Set(nums)].length

  return can < max ? can : max
}

추억 점수

  • 해시맵을 이용해 이름과 그리움 점수를 매핑하고, 이를 기반으로 각 사진의 점수를 계산했습니다.
const solution = (name, yearning, photo) => {
  const map = name.reduce((map, name, idx) => {
    map[name] = yearning[idx]
    return map
  }, {})

  return photo.map((p) => p.reduce((acc, cur) => (acc += map[cur] || 0), 0))
}

할인 행사

  • (방법1) 매번 slice로 확인할 구간을 배열로 만들고 객체를 만들어서 카운트를 진행했습니다.
  • (방법2) "연속된 10일간의 상품 구매 가능 여부를 체크" 하는 것이라 10일간 이라는 윈도우 크기가 고정된 것으로 보고, 슬라이딩 윈도우 알고리즘을 활용하여 시간효율성을 높일 수 있었습니다. 윈도우를 밀면서,
    • 새로 들어오는 상품은 해시맵에 추가
    • 제거되는 상품은 해시맵에서 감소
// 방법 1: 매번 slice로 새 배열과 객체를 생성
function solution1(want, number, discount) {
  let answer = 0
  const map = want.reduce((map, name, idx) => {
    map[name] = number[idx]
    return map
  }, {})

  const check = (sale) => {
    for (let item in map) {
      if (map[item] !== sale[item]) return false
    }
    return true
  }

  for (let i = 0; i <= discount.length - 10; i++) {
    const sliced = discount.slice(i, i + 10)
    const sale = sliced.reduce((map, name) => {
      map[name] = (map[name] || 0) + 1
      return map
    }, {})

    if (check(sale)) answer++
  }

  return answer
}

// 방법 2: 슬라이딩 윈도우
function solution2(want, number, discount) {
  const need = want.reduce((obj, name, idx) => {
    obj[name] = number[idx]
    return obj
  }, {})

  // 초기 윈도우(첫 10일) 설정
  const sale = {}
  for (let i = 0; i < 10; i++) {
    sale[discount[i]] = (sale[discount[i]] || 0) + 1
  }

  const check = () => {
    for (let item in need) {
      if (need[item] !== sale[item]) return false
    }
    return true
  }

  let answer = check() ? 1 : 0

  // 슬라이딩 윈도우 이동
  for (let i = 10; i < discount.length; i++) {
    sale[discount[i]] = (sale[discount[i]] || 0) + 1 // 새로운 것 추가
    sale[discount[i - 10]]-- // 이전 것 제거

    if (check()) answer++
  }

  return answer
}

오픈채팅방

  • 이름이 변경되면 이전의 채팅 기록에도 영향을 주어서, id별로 이름을 매핑해두는 것을 먼저 떠올렸습니다.
  • Enter,Leave일때만 채팅 기록에 메시지가 남기에 해당 경우에만 최종 기록 메시지 목록에 id와 메시지를 기록했습니다. 마찬가지로 이름은 계속 바뀔 수 있으니, id로 기록해두고 나중에 결과 배열에서 id를 map을 통해 최종 이름을 찾아갈 수 있게 하였습니다.
function solution(record) {
  const userInfo = {}
  const result = []
  const MESSAGES = {
    Enter: "님이 들어왔습니다.",
    Leave: "님이 나갔습니다.",
  }

  record.forEach((r) => {
    const [action, id, name] = r.split(" ")
    if (name) userInfo[id] = name
    if (action !== "Change") {
      result.push([id, MESSAGES[action]])
    }
  })

  return result.map(([id, message]) => userInfo[id] + message)
}

키패드 누르기

  • 키패드의 각 숫자를 좌표로 생각하고, 현재 왼손/오른손의 위치에서 목표 숫자까지의 거리를 계산하는 방식으로 접근했습니다.
  • 처음에는 각 숫자의 좌표를 하드코딩하는 방식으로 구현했는데, 2번째 방식처럼 2차원 배열로 실제 키패드 모양을 표현하고 좌표를 매핑한 해시맵을 만드는 방법이 더 직관적인 것 같습니다.
// 방법 1: 객체기반 접근(각 숫자의 좌표 직접 매핑 - 하드코딩)
function solution1(numbers, hand) {
  const keypad = {
    1: [0, 0],
    2: [0, 1],
    3: [0, 2],
    4: [1, 0],
    5: [1, 1],
    6: [1, 2],
    7: [2, 0],
    8: [2, 1],
    9: [2, 2],
    "*": [3, 0],
    0: [3, 1],
    "#": [3, 2],
  }

  let left = keypad["*"]
  let right = keypad["#"]

  const getDist = ([y1, x1], [y2, x2]) => Math.abs(y1 - y2) + Math.abs(x1 - x2)

  return numbers
    .map((num) => {
      const target = keypad[num]

      // 왼쪽 열이면, 왼손
      if (target[1] === 0) {
        left = target
        return "L"
      }
      // 오른쪽 열이면, 오른손
      if (target[1] === 2) {
        right = target
        return "R"
      }

      // 가운데열의 경우, 거리 비교
      const leftDist = getDist(left, target)
      const rightDist = getDist(right, target)

      if (leftDist === rightDist) {
        if (hand === "right") {
          right = target
          return "R"
        }
        left = target
        return "L"
      }

      if (leftDist < rightDist) {
        left = target
        return "L"
      } else {
        right = target
        return "R"
      }
    })
    .join("")
}

// 방법 2: 2차원 배열로 실제 키패드 모양 표현 후, 좌표 매핑
function solution2(numbers, hand) {
  // 키패드를 2차원 배열로 정의
  const keypad = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
    ["*", 0, "#"],
  ]

  // 키패드를 좌표로 매핑
  const getPos = new Map(
    keypad.flatMap((row, y) => row.map((num, x) => [num, [y, x]]))
  )

  let left = getPos.get("*")
  let right = getPos.get("#")

  const getDist = ([y1, x1], [y2, x2]) => Math.abs(y1 - y2) + Math.abs(x1 - x2)

  return numbers
    .map((num) => {
      const target = getPos.get(num)

      // 왼쪽 열이면, 왼손
      if (target[1] === 0) {
        left = target
        return "L"
      }
      // 오른쪽 열이면, 오른손
      if (target[1] === 2) {
        right = target
        return "R"
      }

      // 가운데 열
      const leftDist = getDist(left, target)
      const rightDist = getDist(right, target)

      if (leftDist === rightDist) {
        if (hand === "right") {
          right = target
          return "R"
        }
        left = target
        return "L"
      }

      if (leftDist < rightDist) {
        left = target
        return "L"
      } else {
        right = target
        return "R"
      }
    })
    .join("")
}

@bona1122 bona1122 self-assigned this Jan 27, 2025
@bona1122 bona1122 changed the title Bona1122 [Bona1122] 25.01.23 Jan 27, 2025
Copy link
Collaborator

@JooKangsan JooKangsan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨어요!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

슬라이딩 윈도우 이번에 찾아보면서 알았는데,
매번 새로운 배열을 생성하지 않고 기존 데이터를 업데이트 하는 방식이 확실히 효율적인거같네요...!
알고갑니다!

}
return map
}, {})
return can < max ? can : max
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Math.min(can, max) 이 방법도 좋을거 같아요!

@JooKangsan JooKangsan merged commit 11e5265 into codingTestStd:main Feb 6, 2025
0 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants