sungyup's.

algorithm_programmers / Lv.1 / 2.2 프로그래머스 Lv.1 #21~40

2.2프로그래머스 Lv.1 #21~40

프로그래머스 Lv.1 21~40번

개요

프로그래머스에 자바스크립트로 풀 수 있는 Lv.1 문제는 총 82문제가 있다. 정답률 높은 문제 순, 21 ~ 40번 문제 풀이 중 배울 점이 있는 풀이들에서 배운 개념들을 내 것으로 만들고자 정리해본다.

27. 문자열 다루기 기본

문제 이름이 쉬워보이고, 언뜻 굉장히 간단한 문제 같지만 의외로 함정이 있는 문제다. 문자열 s의 길이가 4 또는 6이고, 숫자로만 구성되어 있는지 확인하는 함수이다. 나는 이렇게 풀었지만, 이 답은 틀린 답이다.

javascript
function solution(s) { if(s.length !== 4 && s.length !== 6) return false; return Number.isInteger(+s); }

이 답이 틀린 이유는 +s가 예상치 못한 결과를 가져올 수 있기 때문이다. 예를 들어 1e22, 10e1 같은 경우, 지수 형식으로 파싱이 되어 실제로는 false를 반환해야하지만 true를 반환해버리는 문제가 있다.

27-1. 정규표현식

따라서 문제에 충실하게, 각 자릿수가 모두 숫자인지 확인하는 것이 좋다. 이 경우 가장 좋은 방식은 정규표현식이다.

문자열의 시작(^)과 끝($) 사이에 [0-9] 중 해당하는 문자가 1회 이상 반복(+)하는지 test한다.

javascript
function solution(s) { return (s.length === 4 || s.length === 6) && /^[0-9]+$/.test(s); }

28. 행렬의 덧셈

행렬의 덧셈은 행과 열의 크기가 같은 두 행렬을 더하는 함수를 작성하는 문제다. 나는 2중 for문으로 풀었다.

javascript
function solution(arr1, arr2) { const answer = []; for(let i = 0; i < arr1.length; i++){ const row = []; for(let j = 0; j < arr1[0].length; j++){ row.push(arr1[i][j] + arr2[i][j]) } answer.push(row) } return answer; }

28-1. 2중 map

map을 2중으로 쓰는 것이 더 가독성이 좋다.

javascript
function sumMatrix(A, B){ return A.map((arr1, idx1) => arr1.map((val, idx2) => val + arr2[idx1][idx2])); }

30. 같은 숫자는 싫어

같은 숫자는 싫어는 배열에서 연속된 숫자는 하나만 남기는 함수를 만드는 문제다. Set을 사용하면 되지 않을까 할수도 있지만, [1, 1, 3, 3, 0, 1, 1] 같은 예시는 [1, 3, 0, 1]을 반환해야하기 때문에 Set은 쓸 수 없다.

나는 for문으로 인덱스들 간에 비교하며 이렇게 풀었다:

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

이 풀이는 사실 아래의 풀이보다 덜 직관적으로 읽히는 풀이로, 아래의 풀이처럼 푸는게 이 방향에서의 풀이의 가장 좋은 방식이라고 생각한다. 아래의 풀이에선 prev라는 변수를 만들고, 처음엔 null을 할당한다. 이후, for문에선 해당 인덱스에 있는 요소가 prev와 다르면 요소를 push하고 prev에 해당 요소를 할당한다.

javascript
function solution(arr) { const answer = []; let prev = null; for (let i = 0; i < arr.length; i++) { if (arr[i] !== prev) { answer.push(arr[i]); prev = arr[i]; } } return answer; }

30-1. filter 활용하기

사실 이 문제는 filter로 한 줄에 풀 수 있다.

javascript
function solution(arr){ return arr.filter((val, idx) => val !== arr[idx -1]); }

31. 최대공약수와 최소공배수

제목이 말하는 그대로를 반환하는 문제다. 풀면서 정수에 대한 지식이 있으면 좀 좋을것 같다는 생각이 들었던 문제로, 그런게 특별히 없는 나는 여러 테스트 케이스들을 넣어가며 풀었다.

최대공약수를 구하려면 최소공배수를 구하고, 이걸로 두 수를 나눈 몫을 각자 곱한 후 최소공배수를 곱하면 된다고 생각해서 그렇게 풀었다. 이후에 알게되었지만 최대공약수는 LCM(Least Common Multiple), 최소공배수는 GCF(Greatest Common Factor)라고 불린다고 한다.

javascript
function solution(n, m) { const min = Math.min(n, m); const max = Math.max(n, m); let a = 1; let b = max; for(let i = 1; i <= min; i++){ if(min % i === 0 && max % i === 0) a = i } const minDivA = min / a; const maxDivA = max / a; b = a * minDivA * maxDivA return [a, b]; }

31-1. 유클리드 호제법

최대공약수는 유클리드 호제법으로 구할 수 있다. 간단한 예시로 2484와 4212의 최대공약수를 구한다고 하면,

4212 = 2484 * 1 + 1728일때, 2484와 4212의 최대공약수는 1728과 2484의 최대공약수와도 같다는 원리다. 한 번 더 이를 진행하면, 2484 = 1728 * 1 + 756이므로 1728과 756의 최대공약수 또한 우리가 원하는 2484와 4212의 최대공약수와 같다.

즉, 나머지가 0이 될때까지 이 작업을 반복해서 최종적으로 나온 두 인수 중 더 큰 쪽이 최대공약수가 된다. 이를 코드로 표현하면 아래와 같다.

javascript
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }

이것을 활용하면 아래와 같이 최종 답을 쓸 수 있다:

javascript
function solution(n, m) { const gcd = (a, b) => b === 0 ? a : gcd(b, a % b); const g = gcd(n, m); const lcm = (n * m) / g; return [g, lcm]; }

31-1-1. 가독성 대신 아름다움을 택한 버전

아래와 같이 쓴 풀이가 극찬을 받고 있어서 가져왔다. 사실 가독성은 아주 떨어진다고 생각하지만, 어쨌든 멋있으면 그 나름대로의 가치가 있지 않겠는가?

javascript
function gcdlcm(a, b){ let r; for(let ab = a * b; r = a % b; a = b, b = r){} return [b, ab / b]; }

33. 예산

예산은 주어진 배열에서 budget을 초과하지 않게 조합을 짰을때 최대 몇개의 요소를 포함한 조합을 짤 수 있는지를 묻는 문제다.

적은 금액부터 순서대로 세운 후, 임시값에 더하다가 값이 초과하면 반환하도록 답을 작성했다:

javascript
function solution(d, budget) { const sorted = d.sort((a, b) => a - b); let temp = 0, count = 0; for(let i = 0; i < sorted.length; i++){ temp += sorted[i] if(temp > budget) return count; count++; } return count; }

33-1. reduce

reduce는 여기서도 쓸 수 있다.

javascript
function solution(d, budget){ return d.sort((a, b) => a - b).reduce((count, price) => { return count + (budget -= price) >= 0; }, 0); }

33-2. for...of

sort까진 필수고, for...of를 사용해서 풀 수도 있다.

javascript
function solution(d, budget) { d.sort((a, b) => a - b); let sum = 0, count = 0; for (let cost of d) { if ((sum += cost) > budget) break; count++; } return count; }

34. 3진법 뒤집기

3진법 뒤집기는 분명 무슨 메소드가 있을것 같았지만 우선 3진법으로 바꾸고(toString(3)), 3진법 수를 10진법으로 바꾸는데에는 자리별로 for문을 사용했다.

javascript
function solution(n) { const base3 = ([...n.toString(3)].reverse()); let answer = 0; for(let i = base3.length; i > 0; i--){ answer += base3[i - 1] * Math.pow(3, base3.length - i) } return answer; }

34-1. parseInt

parseInt로 3진법 수를 10진법으로 파싱할 수 있다.

javascript
function solution = (n) => { return parseInt([...n.toString(3)].reverse().join(""), 3); }

34-2. 내장함수를 배제한 풀이

진법 문제를 toString()이나 parseInt로 풀지 않고 정공법(?)으로 풀 수도 있다. 아래 풀이는 while문으로, 3으로 계속 수를 나누며 해당 수의 나머지를 앞에 추가하는 식으로 3진법으로 변환하였다.

javascript
function solution(n) { const answer = []; while(n !== 0) { answer.unshift(n % 3); n = Math.floor(n/3); } return answer.reduce((acc,v,i) => acc + (v * Math.pow(3, i)),0); }

예를 들어 n이 45인 경우, 45 % 3 = 0이 먼저 unshift되고, 다음으론 15 % 3 = 0, 그 다음은 5 % 3 = 2, 1 % 3 = 1이 들어가 answer가 [1, 2, 0, 0]이 된다.

이후, reduce로 이것을 3의 power와 곱한 것들끼리 더하면 0021을 10진수로 바꾸게 된다. 즉, i = 0일때 acc = 0 + 1 * 3^ 0 = 1, i =1일때 acc = 1 + 2 * 3^1 = 7, ...식인 것이다.

35. 삼총사

삼총사는 배열 안의 3개 정수를 더해 0으로 만들 수 있는 조합이 몇갠지 구하는 문제다. 나는 단순하게 3중 for문으로 풀었다.

javascript
function solution(number) { let counter = 0; for(let i = 0; i < number.length - 2; i++){ for(let j = 1; j < number.length - 1; j++){ for(let k = 2; k < number.length; k++){ if(i < j && j < k && number[i] + number[j] + number[k] === 0) { counter++; } } } } return counter; }

단, 이렇게 풀면 i < j && j < k 조건을 넣더라도 불필요한 반복이 발생한다. i < j < k조건을 자연스럽게 for문 속에 녹이는것이 좋다. 아래와 같은 방식으로 초기값을 선언할때 만들 수 있다:

javascript
function solution(number) { let count = 0; const n = number.length; for (let i = 0; i < n - 2; i++) { for (let j = i + 1; j < n - 1; j++) { for (let k = j + 1; k < n; k++) { if (number[i] + number[j] + number[k] === 0) { count++; } } } } return count; }

35-1. 재귀 백트래킹

3개 숫자를 조합하는 함수를 만들고 필터링하는 식으로도 풀 수 있다. 재귀 형식이라 약간 난이도는 있지만 3중 for문 보다는 보기 좋다.

javascript
function solution(number) { let result = 0; const combination = (current, start) => { if (current.length === 3) { result += current.reduce((acc, cur) => acc + cur, 0) === 0 ? 1 : 0; return; } for (let i = start; i < number.length; i++) { combination([...current, number[i]], i + 1); } } combination([], 0); return result; }

39. 시저 암호

시저 암호는 유명한 암호의 예시로, n만큼 각 문자의 거리를 밀어 새 문자열을 만드는 문제다.

나는 charCodeAt()String.fromCharCode()는 생각났지만 알파벳 A~Z, a~z의 숫자가 생각나지 않아서 계속 로깅하면서 풀었다.

javascript
function solution(s, n) { let answer = ""; for(let i = 0; i < s.length; i++){ let isUpperCase = s[i].toUpperCase() === s[i]; let crypted = s[i].charCodeAt() + n if(isUpperCase && crypted > 90){ crypted -= 26; } if(!isUpperCase && crypted > 122){ crypted -= 26; } answer += s[i] === " " ? " " : String.fromCharCode(crypted) } return answer; }

풀이를 정리할 수 있는 포인트들이 있는데,

  1. 공백인 경우 유지하는 로직을 위로 올린다. 지금은 맨 밑에 삼항 연산자로 있어 불필요한 연산을 해야할 수도 있다.
  2. 대소문자 분기를 isUpperCase로 관리하기보다 그냥 아스키코드에 해당하는 숫자로 범위를 표시하는게 나을 수도 있다.
javascript
function solution(s, n) { let answer = ""; for (let i = 0; i < s.length; i++) { const char = s[i]; // 공백은 그대로 유지 if (char === " ") { answer += " "; continue; } const code = char.charCodeAt(); // 대문자 A-Z (65~90) if (code >= 65 && code <= 90) { answer += String.fromCharCode(((code - 65 + n) % 26) + 65); } // 소문자 a-z (97~122) else if (code >= 97 && code <= 122) { answer += String.fromCharCode(((code - 97 + n) % 26) + 97); } } return answer; }

39-1. simple is best

사실, 좀 노가다성이 있긴 해도 그냥 사전을 만들고 거기서 쓰는 것이 나을 수도 있다. 보는 사람 입장에서 좀 더 짧은 코드로 이해할 수 있다. 만드는 입장에서도 아스키코드 숫자 찾아보는것보다 더 편하게 쓸 수 있다.

javascript
function solution(s, n) { const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const lower = "abcdefghijklmnopqrstuvwxyz"; return [...s].map(c => { if (c === ' ') return ' '; const base = upper.includes(c) ? upper : lower; const index = (base.indexOf(c) + n) % 26; return base[index]; }).join(''); }