안녕하세요. 이번 시간에는 프로그래머스의 알고리즘 문제 풀이를 하겠습니다. 사용 언어는 자바스크립트입니다. 혹시나 다른 알고리즘 문제 사이트 중 자바스크립트를 지원하고, 문제 내용이 알찬(백준처럼 너무 많지는 않은) 사이트가 있다면 알려주세요~ 시간될 때마다 풀어보겠습니다.
Level 3까지는 제가 쉽게 푸는데 4랑 5는 좀 고민을 해야겠더군요 ㅠㅠ. 알고리즘 수업을 들은 지 오래 돼서요. 못 푸는 문제가 있다면 일단 그건 뒤로 보류하고 푼 문제부터 알려드리겠습니다. 여기 풀이는 모두 제가 직접 푼 풀이입니다. 혹시나 더 나은 풀이가 있다면 댓글로 알려주세요.
프로그래머스 알고리즘을 풀 생각이 있으신 분들은 아래 풀이를 보지 마세요. 고민하지 않고 답 먼저 본다면 전혀 실력이 늘지 않습니다. 고민을 하고 다양한 시도를 해야 실력이 느는 것 같습니다. 저도 뭐 알고리즘 대회 입상한 적도 없고, 참가할 생각도 없는 그냥 평범한 사람입니다. 유일한 업적은 대학교 수업 알고리즘 A+받은 것뿐이랄까요 ㅋㅋ. 그냥 고민하면서 한 문제씩 풀어나가고 있습니다.
문제들을 보니까 if문과 for문만 있으면 간단하게 풀 수 있어 보입니다. 하지만 저는 for문을 쓰지 않는 코딩을 하기(map, forEach 등의 함수형 메서드를 더 선호합니다) 때문에 여러분이 푼 답과 조금 다를 수도 있습니다. 특히 한 줄로 문제를 풀어버리는 것을 좋아해서 짧고 간결하게 풀어보겠습니다. 그리고 몇몇 문제는 정규표현식을 쓰면 엄청 간단하게 풀 수 있습니다. 그래서 어떠한 문제는 정규표현식과 일반 풀이 두 개를 모두 제공해드리겠습니다. 단순한 알고리즘 풀이 외에도 자바스크립트의 메서드나 여러 줄임 표현식같은 것을 알려드리고 있기에 한 번 씩 봐보셔도 좋습니다.
Level 1 1번~10번
2016
2016년 a월 b일은 무슨 요일일까요?라는 문제입니다.
new Date에서 month가 0부터 시작(1월이 0이고, 12월이 11)한다는 것을 알고, getDay가 요일을 가져오는 메서드라는 것을 이용하면 쉽게 풀 수 있습니다.
function solution(a, b) {
return ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'][new Date(2016, a - 1, b).getDay()];
}
짧은 리스트는 위와 같이 배열로 그냥 하드코딩하는 게 편하기도 합니다.
가운데 글자 가져오기
abcde에서는 c를 가져오고 qwer에서는 we 두 글자를 가져오는 문제입니다.
보통 이런 문제는 글자를 자르면 됩니다. 인덱스를 구하면 되죠.
function solution(s) {
return s.substr(Math.ceil(s.length / 2) - 1, s.length % 2 === 0 ? 2 : 1);
}
substr로 문자열을 자를 수 있고, 시작 인덱스를 적절히 찾으면 됩니다. 중간점을 찾으려면 보통 s.length / 2를 내림하거나 올림하면 됩니다.
같은 숫자는 싫어
[1,1,3,3,0,1,1]이면 [1,3,0,1]로 연속되는 중복을 제거하는 문제죠.
이런 문제는 필연적으로 반복문이 최소 한 번은 들어가게 됩니다. 사람이라면 어떻게 중복을 제거할 지를 생각해봐도 좋을 것 같습니다. 저라면 지금 숫자랑 다음 숫자랑 같으면 그 숫자는 없애는 방식을 택하겠습니다. 1, 1, 3, 3, 0, 1, 1
function solution(arr) {
return arr.filter((v, i) => v !== arr[i + 1]);
}
반복문을 돌면서 숫자를 없앨 수 있는 메서드가 있습니다. 바로 filter죠. 그것을 사용하면 쉽게 다음 숫자랑 같은 숫자를 제거할 수 있습니다.
나누어 떨어지는 숫자 배열
[5,9,7,10]과 5가 주어지면 [5, 10]을 리턴해야 합니다. 아무것도 없으면 [-1]을 리턴합니다.
역시나 filter로 쉽게 필터링할 수 있습니다. 근데 [-1]을 리턴해야하는 조항 때문에 한 줄로 못 끝내서 짜증이 나네요(한 줄로 하면 코드가 너무 지저분해집니다)
function solution(arr, divisor) {
const answer = arr.filter(el => el % divisor === 0);
return answer.length ? answer.sort((p, c) => p - c) : [-1];
}
마지막에 정답 배열의 개수로 오름차순 정렬을 할지, [-1]을 리턴할지 결정하고 있습니다.
두 정수 사이의 합
3과 5가 주어지면 3+4+5를 해서 리턴하면 됩니다.
function solution(a, b) {
a > b && ([a, b] = [b, a]);
return Array(b - a + 1).fill().map((v, i) => v + i).reduce((a, c) => a + c);
}
5, 3과 같이 역순으로 주어지는 경우만 추가적으로 고려하면 됩니다. [a, b] = [b, a]
는 두 숫자를 바꾸는 편리한 문법이므로 알아두시면 좋습니다. Array부터 시작해서 fill, map까지 이어지는 과정도 [1,2,3,4,5]이런 순차적인 숫자 배열을 만드는 방식이므로 알아두시면 좋습니다.
이렇게 풀고 짧게 끝냈다며 좋아하고 있었는데, 다른 사람의 풀이를 보는 순간 벙 쪘습니다. 가우스 방식(가우스가 1부터 100까지를 순식간에 더했던 일화)을 사용하신 분들이 있었던 거죠. ㅋㅋ 저는 너무 창의력이 떨어지는 것 같습니다.
function solution(a, b) {
return (a + b) * ((a > b ? a - b : b - a) + 1) / 2;
}
양쪽 두 수를 더한 것에, 두 수 사이의 숫자 수 + 1을 곱하고 나누기 2를 하는 게 가우스 방식입니다. a와 b 대소관계 때문에 좀 지저분하네요.
문자열 내 마음대로 정렬하기
['abce', 'abcd', 'cdx']와 2가 주어지면 2번째 인덱스 글자(c, c, x)를 기준으로 정렬합니다. 만약 2번째 인덱스 글자가 서로 같다면(c, c처럼) 사전순으로 정렬합니다. abce와 abcd를 사전순으로 정렬하는 것이죠.
따라서 같을 때와 다를 때 정렬 방법이 달라집니다. 정렬이기 때문에 당연히 sort 메서드가 들어가고요. sort 시 -1이면 순서가 유지되고, 1이면 순서가 서로 바뀐다는 것 이용하면 됩니다.
function solution(strings, n) {
return strings.sort((p, c) => p[n] === c[n] ? p.localeCompare(c) : p[n].localeCompare(c[n]))
}
여기서 꿀팁! localeCompare로 사전순으로 정렬할 수 있습니다! sort 메서드 안에서 return a.localeCompare(b)
하면 됩니다. localeCompare 없이도 사전순으로 정렬하는 것을 직접 구현하셔도 되겠죠. 키워드도 하나 알려드립니다. lexicographic order가 사전순 정렬입니다.
문자열 내 p와 y의 개수
문자열 내의 p와 y의 개수가 같으면(대소문자 구분 없음) true 아니면 false를 리턴하면 됩니다. Ppayay는 true입니다.
해답은 간단하죠. 개수를 세서 하면 됩니다.
function solution(s) {
const p = s.split('').filter(v => ['p', 'P'].includes(v));
const y = s.split('').filter(v => ['y', 'Y'].includes(v));
return p.length === y.length;
}
문자열을 배열로 만들어서 각 단어가 p나 P인지, 또는 y나 Y인지를 찾아서 필터링 후, 개수를 비교합니다.
이 문제도 제 함수형에 대한 집착(고정관념)을 반성하게 해준 문제입니다. 정규표현식보다 함수형 메서드를 우선 시하여 생각하는 문제가 있는 거죠. 정규표현식으로 하면 엄청 간단한 문제였습니다.
function solution(s) {
return s.match(/p/gi).length === s.match(/y/gi).length;
}
정규표현식에 걸리는 것들의 개수를 세면 됐습니다. g는 모두 찾는 거고, i는 대소문자 구분 안 한다는 뜻입니다. 그런데 이게 에러가 납니다. (p와 y가 없는 경우 문제가 됩니다)
function solution(s){
return s.replace(/p/gi, '').length == s.replace(/y/gi, '').length;
}
이렇게 replace에도 정규식을 써서 풀 수 있습니다. 위에 match는 왜 안 되는지 잘 모르겠네요.
문자열 내림차순으로 배치하기
문자열을 역순으로 정렬합니다. 대문자는 소문자보다 뒤에 위치해야 합니다. 예를 들어 Zbcdefg는 gfedcbZ가 됩니다.
당연히 정렬(sort)을 하게 되고요. localeCompare는 여기서 못 씁니다. 대문자가 소문자보다 뒤에 위치해야 해서요.
function solution(s) {
return s.split('').sort((prev, cur) => cur.charCodeAt() - prev.charCodeAt()).join('');
}
제가 좋아하는 split, sort(또는 map) join(또는 reduce)으로 이어지는 메서드입니다. split-apply-combine 기법이라고도 불리는 3단 콤보입니다. 문자열을 split으로 배열로 바꿔서 원하는 처리를 하고 다시 join으로 문자열로 되돌립니다. charCodeAt은 문자의 숫자코드를 알려주는 메서드입니다. 이걸 사용해서 정렬하면 됩니다. 대소문자 정렬에서 localeCompare과 다른 결과를 보여줍니다.
문자열 다루기 기본
문자열의 길이가 4 또는 6이고 숫자로만 구성되어 있는지 확인합니다.
숫자로만 되어있는지와(AND) 길이가 4 또는(OR) 6인지를 확인하면 되겠네요.
function solution(s) {
return /^[0-9]+$/.test(s) && [4,6].includes(s.length);
}
프로그래머스에서 \d 정규표현식을 지원하지 않아 저렇게 숫자로 했습니다. OR 조건에서 (s.length === 4 || s.length === 6)
대신 includes로 짧게 줄일 수 있다는 것 알아두세요.
서울에서 김서방 찾기
속담으로 이름을 지은 문제인 것 같네요. ['Jane', 'Kim']이란 배열이 있으면 Kim의 위치를 찾으면 됩니다.
간단하게 indexOf로 끝내버립시다.
function solution(seoul) {
return '김서방은 ' + seoul.indexOf('Kim') + '에 있다';
}
Level 1 문제는 30개가 있더군요. 앞으로 두 편 더 Level 1 문제 풀이를 보여드리겠습니다. 여러분의 답과 비교해보시고 얼마나 더 짧게 줄일 수 있는지 테스트해보세요 ㅎㅎ. 사실 너무 쉬워서 블로그 포스팅하는 시간이 몇 배는 더 걸렸네요 ㅠㅠ. Level 2부터 좀 재밌어집니다.