sungyup's.

Web_Miscellaneous / Algorithm(JS) / 4.4 프로그래머스 Lv.0 #31~40

4.4프로그래머스 Lv.0 #31~40

프로그래머스 Lv.0 31~40번

개요

기초 트레이닝 문제(Lv.0) 31 ~ 40번 문제 풀이 중 배울 점이 있는 풀이들에서 배운 개념들을 내 것으로 만들고자 정리해본다.

31. 수열과 구간 쿼리 4

하나의 배열과, 쿼리들이 담긴 배열이 주어지면 쿼리들이 담긴 배열을 이용해 배열의 몇번째에서 몇번째까지에 쿼리에 입력한 k의 배수번째 인덱스들에 1을 더한 배열을 반환하라는 문제다. 나는 이렇게 풀었다:

javascript
function solution(arr, queries) { queries.forEach(([s, e, k])=> { for(let i = s; i < e + 1; i++){ i % k === 0 && arr[i]++; } }) return arr; }

31-1. for...of를 활용한 풀이

javascript
function solution(arr, queries) { for(let [s, e, k] of queries) { for(let i = s; i <= e; i++) { if(i % k === 0) arr[i]++; } } return arr; }

forEach만 쓰던 내게 for... of가 신선하게 보였다. 또, 새삼스럽지만 for문에 i < e + 1보다 i <= e가 더 나아보이기도 했고.

31-2. 또또또 reduce

reduce를 정형화된 패턴으로만(그놈의 (acc, cur) => {...}) 쓰는 내게 이런 식의 풀이를 보는 것은 사고의 확장을 가져다 준다.

javascript
function solution(arr, queries) { return queries.reduce((bucket, [s, e, k]) => { for (let i = s; i <= e; i += 1) { if (i % k === 0) bucket[i] += 1 } return bucket }, [...arr]) }

31-3. 신기한 for

이 답은 처음에 보고 "이게 된다고?"했던 답이다.

javascript
function solution(arr, queries) { queries.forEach(([s, e, k]) => { for (; s <= e; s++) { !(s % k) && arr[s]++; } }); return arr; }

!(s % k)로 k의 배수일때라는 조건을 나타낸 것도 흥미롭지만(k로 나누어떨어지면 0이되니 !를 붙여 !0 = true로 만들고, 나머지는 false로 만드는게 흥미롭다. 물론 가독성은 떨어진다고 생각한다.), 더 재밌다고 생각한 부분은 역시 for 조건문.

javascript
for (초기식; 조건식; 증감식) { // 실행할 코드 }

여기서 초기식, 조건식, 증감식 세 부분 모두 생략 가능하다고 한다. 다만, 세미콜론(;) 두 개는 항상 있어야 한다고 한다. 따라서 아래 코드 둘은 완전히 같다.

javascript
for(; s <= e; s++){ // ... } for(let i = s; i <= e; i++){ // ... }

굳이 변수를 안 만드는 선택을 한 건 물론 이해되지만, 개인적으로 생략하는 방식은 가독성이 좋지는 않다고 생각한다.(그냥 내가 안 익숙해서 그렇기도 하겠지만) 하지만 알아둬서 나쁠건 없다.

32. 배열 만들기 2

배열 만들기 2는 문제를 풀어내는 것 자체는 단순해서 남들의 답이 더 기대되었다.

javascript
function solution(l, r) { const answer = []; for(let i= l; i<=r ;i++){ if(`${i}`.split("").every(value => value === "5" || value === "0")){ answer.push(i); } } if(!answer.length) return [-1]; return answer; }

32-1. RegExp

RegExp.test가 떠오를만한 문제다.

아래 답의 /^[05]+$/는, 각각 이런 의미다:

  • ^ : 문자열 시작
  • [05] : 문자 클래스. 0 또는 5 중 하나.
  • + : 앞의 [05]가 하나 이상 반복
  • $ : 문자열 끝

문자열이 0 또는 5로만 이루어져있고, 하나 이상 포함되어 있는 경우에만 true

javascript
function solution(l, r) { const answer = []; for (let i = l; i <= r; i++) { if (/^[05]+$/.test(i)) { answer.push(i); } } return answer.length ? answer : [-1]; }

32-2. 제너레이터 함수(generator function)

특이한 구문을 봐서 공부하고자 적는다. 참고로, toString(2)는 숫자를 2진수 문자열로 변환한다.

javascript
function* gen50() { let i = 1; while(true) { yield Number(Number(i).toString(2)) * 5; i++; } } function solution(l, r) { const n = gen50(); let a = 0; const arr = []; while(a < l) { a = n.next().value; } while(a <= r) { arr.push(a); a = n.next().value; } return arr.length ? arr : [-1]; }

function*는 제너레이터 함수, 즉 중간에 실행을 멈췄다가 다시 시작할 수 있는 함수를 정의할 때 쓰는 표현이다. 여기서 yield는 일반함수와 달리 값을 즉시 반환하지 않고, 값을 하나씩 내보내며 실행을 일시 중단시킨다.

제너레이터 함수는 제너레이터 객체를 반환하고, next()로 값을 하나씩 꺼낼 수 있다.

javascript
function* gen() { yield 1; yield 2; yield 3; } const g = gen(); console.log(g.next()); // { value: 1, done: false } console.log(g.next()); // { value: 2, done: false } console.log(g.next()); // { value: 3, done: false } console.log(g.next()); // { value: undefined, done: true }

뭐하려고 이런 문법이 존재하냐?라고 하면, 값을 순차적으로 하나씩 계산하고 싶을 때 메모리를 덜 사용할 수 있다고 한다. 즉, lazy evaluation(필요할 때만 계산)을 가능하게 해주는 것.

34. 콜라츠 수열

자연수 x에 대해 짝수이면 2로 나누고, 홀수면 3x + 1로 바꿔서 계산하면 언젠가는 x가 1이되고, 이 과정에서 거친 모든 수를 콜라츠 수열이라고 한다. x를 받아 콜라츠 수열을 반환하는 문제다. 나는 이렇게 풀었다:

javascript
function solution(n) { const answer = []; while(true){ if(n === 1) { answer.push(1); break; } if(n % 2 === 0){ answer.push(n) n = n / 2; } else { answer.push(n) n = 3 * n + 1; } } return answer; }

다른 풀이로 들어가기에 앞서, 한가지 중요한걸 짚고 넘어간다. 처음에 풀때는, n을 받아 이걸 일단 보존하기 위해 let k = n이라고 k라는 변수를 만들어 이걸 while문으로 실행하며 변경하고 배열에 추가했는데, 그럴 필요가 없다.

함수 파라미터로 받은 n값으로 전달(pass by value)된다. 자바스크립트의 원시 타입은 함수에 전달될때 값이 복사되어 함수 내부로 전달되기 때문에, 외부의 값을 변화시키지 않는다.

javascript
function solution(n) { n = 10; // 여기서 n을 바꿔도 } let x = 5; solution(x); console.log(x); // 여전히 5임

앞서 원시 타입이라고 제한한점에서 알 수 있껬지만, 객체참조(reference)로 전달되어서 함수 안에서 변경하면 원본도 바뀐다.

javascript
function mutate(arr) { arr.push(999); } const a = [1, 2, 3]; mutate(a); console.log(a); // [1, 2, 3, 999] → 원본도 변경됨

아무튼, 다른 사람들의 콜라츠 수열 풀이를 보자.

34-1. 재귀

하나의 함수로 완전히 재귀 로직을 구현하려면 solution함수가 전달받는 매개변수를 약간 변경해야한다. 즉, array를 추가적으로 받아 해당 array에 값을 추가하는 로직도 넣어야한다.

javascript
function solution(n, arr = []) { arr.push(n) if (n === 1) return arr if (n % 2 === 0) return solution(n / 2, arr) return solution(3 * n + 1, arr) }

34-2. 제너레이터 함수(generator function)

32번처럼, 이 문제도 제너레이터 함수로 푼 풀이가 있다.

javascript
function* collatz(num) { let value = num; yield value; while (value !== 1) { value = value % 2 ? 3 * value + 1 : value / 2; yield value; } } function solution(n) { return [...collatz(n)]; }

제너레이터 함수는 제너레이터 객체를 반환하고, next()로 값을 하나씩 꺼낼 수 있다. 따라서 위의 답에서 [...collatz(n)]을 반환한 것은, 모든 yield 값을 배열로 한꺼번에 펼쳐서 수집하기 위함이다.

35. 배열 만들기 4

문제 자체는 그냥 시킨대로 그대로 코드를 쓰면 풀리는 쉬운 문제다. 앞선 31번 문제에서 for문의 조건문 생략을 보고 왔기에, 습관적으로 쓰던 i++를 뒤로 미룰 수 있게 되어 그렇게 코드를 작성했다.

javascript
function solution(arr) { const stk = []; for(let i = 0; i < arr.length;){ if(!stk.length || stk[stk.length -1] < arr[i]){ stk.push(arr[i]); i++; } else { stk.pop(); } } return stk; }

35-1. i++를 표현식 안에서 사용하기

비슷하지만 이렇게 할 수도 있다. i++에 대한 개념을 확실히 이해하고 있어야 자신있게 아래와 같이 쓸 수 있을 것이다.

javascript
// 내 코드 stk.push(arr[i]); i++; // 위의 두 코드는 아래 한줄과 같다. stk.push(arr[i++]);

여기서 필요에 따라선 ++i를 쓸 줄도 알면 좋을것 같다. 전위 연산자로 쓰게 되면, 먼저 i를 증가시키고 그 증가시킨 i가 push된다. 지금처럼 후위 연산자로 쓰면 먼저 i가 push되고, 이후 i가 증가된다.

그냥 단독으로 i++라던가 ++i를 쓰면 큰 차이가 없을 수도 있지만, 표현식 안에서 계산된 결과를 사용할 때 차이가 생긴다.

javascript
// 후위 연산: 값을 쓰고 이후 증가시킴 let n = 5; let x = n++; // x = 5, n = 6 // 전위 연산: 먼저 증가시키고 값을 씀 let n = 5; let x = ++n; // x = 6, n = 6

35-2. continue로 루프 제어

for문을 제어할 때, continue 키워드를 활용하고 i++for 조건문에 적을 수도 있다. continue는 말그대로 아래 코드로 계속 넘어가라는 의미가 아니고, 다음 루프로 넘어가라, 즉 증감식으로 넘어가라는 의미다.

개인적으로 내 풀이가 더 낫다고 생각하지만, continue의 사용례를 되짚어볼만한 기회라고 생각했다.

javascript
function solution(arr) { const stk = []; for(let i=0; i<arr.length; i++) { if(stk.length === 0) { stk.push(arr[i]); continue; } if(stk[stk.length - 1] < arr[i]) { stk.push(arr[i]); continue; } stk.pop(); i--; } return stk; }

37. 주사위 게임 3

문제를 보고 바로 떠오른 것은 지난번에 봤던 Set이었다. Set.size()로 주사위의 숫자들이 어떻게 다르게 나왔는지 파악할 수 있었던 지난번 주사위 문제가 생각나서였다.

그런데 이번엔 주사위가 4개였고, 따라서 2/2나 3/1 같이, size는 2인데 서로 다른 케이스가 나오는 경우들이 있었다. 그래서 Set을 포기해야 한다면... 자연스럽게 떠오른 것이 Map이었다. 나는 이렇게 풀었다:

javascript
function solution(a, b, c, d){ const counts = new Map(); for (const num of [a, b, c, d]){ counts.set(num, (counts.get(num) || 0) + 1); } const entries = [...counts.entries()]; const length = entries.length; switch(length){ case 1: return entries[0][0] * 1111; case 2: const [[n1, c1], [n2, c2]] = entries; if(c1 === 1 || c2 === 1) { if(c1 === 1) return (10 * n2 + n1) ** 2 if(c2 === 1) return (10 * n1 + n2) ** 2 } else { return (n1 + n2) * Math.abs(n1 - n2); } case 3: const [[num1, cnt1], [num2, cnt2], [num3, cnt3]] = entries; if(cnt1 === 2) return num2 * num3; if(cnt2 === 2) return num1 * num3; if(cnt3 === 2) return num1 * num2; case 4: return Math.min(a, b, c, d) } }

37-1. Array를 이용한 풀이

javascript
function solution(a, b, c, d) { const dice = [a, b, c, d]; const counter = new Array(7).fill(0); for (let i = 0; i < 4; i++) counter[dice[i]]++; let result = 0; if (counter.includes(4)) { result = 1111 * counter.indexOf(4); } else if (counter.includes(3)) { const p = counter.indexOf(3); const q = counter.indexOf(1); result = (10 * p + q) ** 2; } else if (counter.includes(2) && counter.filter(val => val === 2).length === 2) { const p = counter.indexOf(2); const q = counter.lastIndexOf(2); result = (p + q) * Math.abs(p - q); } else if (counter.includes(2)) { const p = counter.indexOf(2); const q = dice.filter(num => num !== p)[0]; const r = dice.filter(num => num !== p)[1]; result = q * r; } else { result = Math.min(...dice); } return result; }

37-2. Map을 정렬해서 쓰기

javascript
function solution(a, b, c, d) { const map = new Map(); for (const data of [a, b, c, d]) { map.set(data, (map.get(data) || 0) + 1); } const sortedArr = [...map].sort((a, b) => {if (a[1] === b[1]) return b[0] - a[0]; else return b[1] - a[1]}); if (map.size === 1) return 1111 * sortedArr[0][0]; else if (map.size === 3) return sortedArr[1][0] * sortedArr[2][0]; else if (map.size === 4) return sortedArr[3][0]; else if (sortedArr[0][1] === 3) return (10 * sortedArr[0][0] + sortedArr[1][0]) ** 2; else return (sortedArr[0][0] + sortedArr[1][0]) * (sortedArr[0][0] - sortedArr[1][0]); }

38. 글자 이어 붙여 문자열 만들기

단순한 문제로, 얼마나 간결하고 가독성 좋게 쓰느냐가 관건인듯하다. 나는 이렇게 풀었다:

javascript
function solution(my_string, index_list) { const answer = []; const arr = my_string.split(''); for(let i of index_list){ answer.push(arr[i]) } return answer.join(''); }

38-1. Array.map으로 간단하게 풀이

javascript
function solution(my_string, index_list){ return index_list.map(i => my_string[i]).join(''); }

38-2. Array.reduce도 여전히 사용 가능

javascript
function solution(my_string, index_list){ return index_list.reduce((result, i) => result + my_string[i], '') }

39. 9로 나눈 나머지

사실 그냥 BigInt 수를 지정하고 9로 나눈 나머지를 구하면 될 것 같은 문제지만, 문제 설명을 따르기로 한다. 나는 일단 배열로 바꾼 다음 reduce를 썼다. 배열로 일단 바꾸면 문자가 되는 바람에 +cur로 숫자로 바꿨다.

javascript
function solution(number) { return number.split("").reduce((acc, cur)=> acc += +cur, 0) % 9; }

39-1. array로 바꾸는 다른 방법, Array.from

아래와 같이 number.split('')대신 Array.from(number)로도 배열로 바꿀 수 있다.

javascript
function solution(number) { return Array.from(number).reduce((acc, v) => acc + Number(v), 0) % 9 }

39-2. array로 바꾸는 또 다른 방법, [...string]

[...number]도 배열로 바꾸는 방법이다. 이번 기회에 문자열을 배열로 바꾸는 방법들을 정리해보았다.

javascript
function solution(number) { return [...number].reduce((total, num) => +total + (+num)) % 9; }

40. 문자열 여러 번 뒤집기

문자열 여러 번 뒤집기는 앞서 본 많은 문제들처럼 쿼리를 받아 문자열에 적용하는 문제다.

javascript
function reverse(string){ const arr = string.split(""); const newArr = []; for(let i = 0; i< arr.length;i++){ newArr.unshift(arr[i]) } return newArr.join('') } function solution(my_string, queries) { let string = my_string queries.forEach(([s, e])=> { string = string.slice(0, s) + reverse(string.slice(s, e + 1)) + string.slice(e + 1) }) return string; }

40-1. reverse는 이미 있습니다. splice를...

Array.prototype.reverse()는 이미 존재한다. 따라서, 직접 reverse를 구현할 필요 없이, 배열로 바꾸고 쓰면 된다.

javascript
function solution(my_string, queries) { let str = my_string.split(''); queries.forEach(([start, end]) => { const changeStr = str.slice(start, end + 1); str.splice(start, changeStr.length, ...changeStr.reverse()); }); return str.join(''); }