2.2프로그래머스 Lv.1 #21~40
프로그래머스 Lv.1 21~40번
개요
프로그래머스에 자바스크립트로 풀 수 있는 Lv.1 문제는 총 82문제가 있다. 정답률 높은 문제 순, 21 ~ 40번 문제 풀이 중 배울 점이 있는 풀이들에서 배운 개념들을 내 것으로 만들고자 정리해본다.
27. 문자열 다루기 기본
문제 이름이 쉬워보이고, 언뜻 굉장히 간단한 문제 같지만 의외로 함정이 있는 문제다. 문자열 s의 길이가 4 또는 6이고, 숫자로만 구성되어 있는지 확인하는 함수이다. 나는 이렇게 풀었지만, 이 답은 틀린 답이다.
javascriptfunction 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
한다.
javascriptfunction solution(s) { return (s.length === 4 || s.length === 6) && /^[0-9]+$/.test(s); }
28. 행렬의 덧셈
행렬의 덧셈은 행과 열의 크기가 같은 두 행렬을 더하는 함수를 작성하는 문제다. 나는 2중 for
문으로 풀었다.
javascriptfunction 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중으로 쓰는 것이 더 가독성이 좋다.
javascriptfunction 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
문으로 인덱스들 간에 비교하며 이렇게 풀었다:
javascriptfunction 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에 해당 요소를 할당한다.
javascriptfunction 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
로 한 줄에 풀 수 있다.
javascriptfunction solution(arr){ return arr.filter((val, idx) => val !== arr[idx -1]); }
31. 최대공약수와 최소공배수
제목이 말하는 그대로를 반환하는 문제다. 풀면서 정수에 대한 지식이 있으면 좀 좋을것 같다는 생각이 들었던 문제로, 그런게 특별히 없는 나는 여러 테스트 케이스들을 넣어가며 풀었다.
최대공약수를 구하려면 최소공배수를 구하고, 이걸로 두 수를 나눈 몫을 각자 곱한 후 최소공배수를 곱하면 된다고 생각해서 그렇게 풀었다. 이후에 알게되었지만 최대공약수는 LCM(Least Common Multiple), 최소공배수는 GCF(Greatest Common Factor)라고 불린다고 한다.
javascriptfunction 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이 될때까지 이 작업을 반복해서 최종적으로 나온 두 인수 중 더 큰 쪽이 최대공약수가 된다. 이를 코드로 표현하면 아래와 같다.
javascriptfunction gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
이것을 활용하면 아래와 같이 최종 답을 쓸 수 있다:
javascriptfunction 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. 가독성 대신 아름다움을 택한 버전
아래와 같이 쓴 풀이가 극찬을 받고 있어서 가져왔다. 사실 가독성은 아주 떨어진다고 생각하지만, 어쨌든 멋있으면 그 나름대로의 가치가 있지 않겠는가?
javascriptfunction gcdlcm(a, b){ let r; for(let ab = a * b; r = a % b; a = b, b = r){} return [b, ab / b]; }
33. 예산
예산은 주어진 배열에서 budget을 초과하지 않게 조합을 짰을때 최대 몇개의 요소를 포함한 조합을 짤 수 있는지를 묻는 문제다.
적은 금액부터 순서대로 세운 후, 임시값에 더하다가 값이 초과하면 반환하도록 답을 작성했다:
javascriptfunction 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
는 여기서도 쓸 수 있다.
javascriptfunction 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
를 사용해서 풀 수도 있다.
javascriptfunction 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
문을 사용했다.
javascriptfunction 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진법으로 파싱할 수 있다.
javascriptfunction solution = (n) => { return parseInt([...n.toString(3)].reverse().join(""), 3); }
34-2. 내장함수를 배제한 풀이
진법 문제를 toString()
이나 parseInt
로 풀지 않고 정공법(?)으로 풀 수도 있다. 아래 풀이는 while
문으로, 3으로 계속 수를 나누며 해당 수의 나머지를 앞에 추가하는 식으로 3진법으로 변환하였다.
javascriptfunction 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
문으로 풀었다.
javascriptfunction 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
문 속에 녹이는것이 좋다. 아래와 같은 방식으로 초기값을 선언할때 만들 수 있다:
javascriptfunction 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
문 보다는 보기 좋다.
javascriptfunction 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의 숫자가 생각나지 않아서 계속 로깅하면서 풀었다.
javascriptfunction 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; }
풀이를 정리할 수 있는 포인트들이 있는데,
- 공백인 경우 유지하는 로직을 위로 올린다. 지금은 맨 밑에 삼항 연산자로 있어 불필요한 연산을 해야할 수도 있다.
- 대소문자 분기를
isUpperCase
로 관리하기보다 그냥 아스키코드에 해당하는 숫자로 범위를 표시하는게 나을 수도 있다.
javascriptfunction 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
사실, 좀 노가다성이 있긴 해도 그냥 사전을 만들고 거기서 쓰는 것이 나을 수도 있다. 보는 사람 입장에서 좀 더 짧은 코드로 이해할 수 있다. 만드는 입장에서도 아스키코드 숫자 찾아보는것보다 더 편하게 쓸 수 있다.
javascriptfunction 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(''); }