[숏코딩] JS 기본문법(백준)
목차
사실, JavaScript는 숏코딩과는 거리가 먼 언어이다.
하지만 그럼에도 불구하고 필자처럼 JS로 숏코딩을 하고 싶은 사람은 있을 거라고 생각한다.
아무리 찾아도 JS에 숏코딩에 관한 팁은 없었기에 이 글에서 정리해보고자 한다.
(숏코딩을 위해 세미콜론을 생략해도 아무 문제가 없기에, 이 글에서는 기본적으로 줄의 끝에 세미콜론을 사용하지 않았습니다.)
1. 입력#
백준에서 JS로 입력을 받는 방법은 크게 두 가지가 있다. 바로 fs와 readline인데, 백준에서 JS를 조금이라도 해봤다면 fs가 몇 배는 더 짧다는 걸 알 수 있다.
기본적인 fs 문법은 아래와 같다.
const input = require('fs').readFileSync("/dev/stdin").toString().trim()가장 짧게 줄이면 아래와 같다.
i=0+require('fs').readFileSync(0)무슨 일이 일어난 걸까?
우선 trim 함수는 입력값 끝에 공백이나 줄바꿈을 제거하기 위해 있는 거지만, JS는 관대하기에 없어도 괜찮은 경우가 많다.
또한 /dev/stdin 경로는 그냥 0으로 놓아도 알아서 잘 파일을 찾아준다.
그렇다면 앞에 붙은 0+는 무엇일까? 바로 toString 함수의 역할을 대신해 문자열로 바꿔주는 역할이다.
이렇게 한다면 입력 코드에서도 절반을 줄일 수 있다.
사실 입력 부분은 입력 형식에 따라서, 그리고 문제에서 필요한 것에 따라서도 바뀐다.
예를 들어 숫자 단 한 개를 입력으로 준다면 앞에 0을 생략한
i=+require('fs').readFileSync(0)도 가능하다. 이 역시 JS의 관대함 덕분이다.
추가로, 입력값을 단 한 번만 활용해도 된다면 굳이 변수 선언을 하지 말고 바로 때려 박아도 된다.
+2024-11-09 추가
일부 상황의 경우 더 짧은 문법을 사용할 수 있다.
process.stdin.on('data',i=>console.log(+i?'A':'B'))주로 입력값이 숫자 1개이거나 입력값을 적게 사용하는 경우에 좋을 것 같다.
require 문법과 글자 수 비교
i=+require('fs').readFileSync(0)기본 32byte, 사용 1byte (i)
process.stdin.on('data',i=>)기본 28byte, 사용 2byte (+i)
(require 문법과 달리 여러 줄의 코드를 실행하려면 => 뒤에 {}를 추가해야 한다. 2byte가 추가로 사용되며, 이는 입력값을 단 1번 사용했을 때만 더 짧은 코드를 작성할 수 있다는 의미이다.)
2. 변수, 함수 선언과 return#
JS는 매우 관대한 언어이기 때문에, const나 let 같은 예약어 없이도 변수를 바로 선언하고 쓸 수 있다.
함수도 마찬가지로, 화살표 함수 문법을 이용하면 획기적으로 줄일 수 있다. 또한 return 예약어도 너무 길기 때문에 화살표 함수에서 if문은 삼항연산자로 처리하고, for문은 재귀를 이용함으로써 한 줄에 바로 return하도록 한다.
function A(x,y) {
if(x > y) {
return 1
}else {
return 0
}
}A=(x,y)=>x>y?1:0위 두 코드는 똑같이 동작한다.
3. 2차원 배열과 split, join, fill 함수#
dp나 dfs 방문 체크를 위해 2차원 배열을 만드는 건 흔한 일이다.
기본적으로 크기에 로 채워진 2차원 배열을 만드는 짧은 코드는 다음과 같다.
p=Array.from(Array(n),e=>Array(m).fill(k))
p[a][b] //사용시
그러나 정작 만들어놓고 2차원 배열을 호출하는 횟수는 그리 많지 않다면, 1차원 배열을 2차원 배열처럼 활용할 수도 있다.
t=1e5,p=Array(n*m*t).fill(k)
p[a*t+b] //사용시
생성 코드는 42B → 28B로 14B를 줄였다.
사용 코드는 7B → 8B로 고작 1B 늘었다.
즉, 배열을 13번 이하로 사용하는 코드라면 1차원 배열로 바꾸는 게 이득이다.
+배열을 여러 개 만든다면 t 값을 그대로 가져다 사용할 수도 있어서 이득이 배가 된다.
split, join, fill은 JS로 PS를 하면서 꼭 쓰게 되는 함수들이다. 이 함수들도 줄일 게 뭐가 있나 싶겠지만, 놀라운 트릭이 존재한다.
let s1 = 'Hello, World!'.split(" ")
let s2 = [5,1].join(" ")
let s3 = [1,2,3].fill(0)
/******************/
let s1 = 'Hello, World!'.split` `
let s2 = [5,1].join` `
let s3 = [1,2,3].fill`` //output = [[""],[""],[""]]
위에 분리된 두 코드들은 거의 똑같이 동작한다. (fill은 조금 다르다.)
어쩌면 함수 뒤에 괄호가 없는 이상한 모습에 충격을 받았을지도 모르겠다.
이 모든 게 가능한 원리는 바로 백틱 ` 때문이다. 백틱은 JS에서 템플릿 리터럴로 쓰이는데, 간단히 말해 백틱으로 감싼 부분이 문자열뿐만 아니라 내장된 표현식을 모두 허용하기 때문에 함수들의 매개변수가 되어 괄호 없이도 동작하는 것이다.
fill은 백틱을 사용하면 무려 공백이 들어 있는 배열로 채워준다! (왜 그런지는 잘 모르겠다.. 아시는 분은 댓글 남겨주세요) 이 점을 이용하면 앞서 얘기한 배열 생성 방법과 결합해 극단적으로 짧은 2차원 배열 생성 코드를 만들 수도 있다.
4. 숫자와 비트연산자#
보통 문제에서 고정값으로 주는 숫자들은 대부분 MOD 연산에 활용되는 큰 소수인 경우가 많다. (1000000007이라든가..)
이런 걸 그대로 가져다 쓴다면 가뜩이나 힘든 숏코딩에서 소중한 바이트를 낭비하게 되는 것이다.
비단 JS뿐만 아니라, 여러 언어에서 지수 표기법을 지원한다.
if(1000000007 === 1e9+7) //true
1e9 === 1 * 10**9
2e9 === 2 * 10**9
3.1e5 === 3.1 * 10**5소수도 문제없이 잘 작동하니 지수 표기법으로 큰 숫자의 길이를 줄일 수 있다.
계산할 때는 비트연산자라는 걸 활용해 글자 수를 줄일 수 있다. 가장 대표적인 예시는 아래와 같다.
let a1 = Math.floor(5.16171819) //5
let a2 = ~~5.16171819 //5
a1과 a2는 양수 범위 내에서 완전히 똑같다. 비트연산을 할 때 소수점 부분은 잘려 없어져버리기 때문에, 소수점을 내림하는 Math.floor 함수와 정확히 같은 역할을 하는 것이다. (~은 비트 NOT 연산자로, NOT을 두 번 사용해 원래 값은 변함이 없고, 비트연산을 했기 때문에 소수점 부분만 없어지는 원리이다.)
하지만 음수에서는 서로 다르게 작동하니 유의해야 한다.
let a1 = Math.floor(-0.5) //-1
let a2 = ~~-0.5 //0
Math.floor는 항상 작은 정수로 내림 처리하지만, ~~은 단순히 소수점 부분을 제거하기 때문에 차이가 생기는 것이다.
5. true, false와 연산자 우선순위#
JS는 정말 멋지게도 0은 false이고 1은 true이다. 즉,
if(1 && !0) {
console.log('wow')
}위 코드는 잘 작동한다.
그래서 dfs 등에서 방문 처리를 할 때, 0과 1로 표현하면 크게 코드를 단축할 수 있다.
+앞서 살펴본 fill 함수의 백틱 트릭 같은 경우, 배열을 [""]라는 다소 이상한 값으로 채우는데..
여기서 JS의 골때리는 점을 알 수 있다.
if([""]) {
console.log(1)
}
if(0) {
console.log(2)
}
if([""] == 0) {
console.log(3)
}
if([""] === 0) {
console.log(4)
}위 코드를 실행하면 1과 3이 출력된다. 와우!
[""]은 기본적으로 true 값을 가지고 있지만, ==이나 부등호로 비교를 할 때는 0과 같은 취급을 받는다. (그래서 [""] < 1은 true이다..)
부등호는 하나만 써도 정상적으로 작동하지만, 등호는 반드시 두 개 이상을 써야 하므로 부등호로 판별하는 게 더 짧은 코드를 만드는 데 효과적이다.
연산자 우선순위도 숏코딩에서 중요한 내용 중 하나이다. 우선순위가 높다면 굳이 괄호를 치지 않고 2바이트를 아낄 수 있기 때문이다.
| 19 | 그룹 | 없음 | ( … ) |
| 18 | 멤버 접근 | 좌결합성 | … . … |
| 계산된 멤버 접근 | 좌결합성 | … [ … ] | |
| new (인자 리스트 제공) | 없음 | new … ( … ) | |
| 함수 호출 | 좌결합성 | … ( … ) | |
| 옵셔널 체이닝 | 좌결합성 | ?. | |
| 17 | new (인자 리스트 생략) | 우결합성 | new … |
| 16 | 후위 증가 | 없음 | … ++ |
| 후위 감소 | … -- | ||
| 15 | 논리 NOT | 우결합성 | ! … |
| 비트 NOT | ~ … | ||
| 단항 양부호 | + … | ||
| 단항 부정 | - … | ||
| 전위 증가 | ++ … | ||
| 전위 감소 | -- … | ||
| typeof | typeof … | ||
| void | void … | ||
| delete | delete … | ||
| await | await … | ||
| 14 | 거듭제곱 | 우결합성 | … ** … |
| 13 | 곱하기 | 좌결합성 | … * … |
| 나누기 | … / … | ||
| 나머지 | … % … | ||
| 12 | 더하기 | 좌결합성 | … + … |
| 빼기 | … - … | ||
| 11 | 비트 왼쪽 시프트 | 좌결합성 | … << … |
| 비트 오른쪽 시프트 | … >> … | ||
| 비트 부호 없는 오른쪽 시프트 | … >>> … | ||
| 10 | 미만 | 좌결합성 | … < … |
| 이하 | … <= … | ||
| 초과 | … > … | ||
| 이상 | … >= … | ||
| in | … in … | ||
| instanceof | … instanceof … | ||
| 9 | 동등 | 좌결합성 | … == … |
| 부등 | … != … | ||
| 일치 | … === … | ||
| 불일치 | … !== … | ||
| 8 | 비트 AND | 좌결합성 | … & … |
| 7 | 비트 XOR | 좌결합성 | … ^ … |
| 6 | 비트 OR | 좌결합성 | … | … |
| 5 | 논리 AND | 좌결합성 | … && … |
| 4 | 논리 OR | 좌결합성 | … || … |
| 널 병합 연산자 | 좌결합성 | … ?? … | |
| 3 | 조건 (삼항) | 우결합성 | … ? … : … |
| 2 | 할당 | 우결합성 | … = … |
| … += … | |||
| … -= … | |||
| … **= … | |||
| … *= … | |||
| … /= … | |||
| … %= … | |||
| … <<= … | |||
| … >>= … | |||
| … >>>= … | |||
| … &= … | |||
| … ^= … | |||
| … |= … | |||
| … &&= … | |||
| … ||= … | |||
| … ??= … | |||
| yield | 우결합성 | yield … | |
| yield* | yield* … | ||
| 1 | 쉼표 / 시퀀스 | 좌결합성 | … , … |
(출처: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Operator_precedence)
6. 마치며#
위에서 언급한 내용 이외에도 >= 사용 시 <로 바꾸고 참과 거짓의 코드를 바꾸는 등 숏코딩의 세계는 무궁무진하다.
JS는 관대함이 장점이자 단점인 언어이다. 이 관대함을 잘 활용해 멋지게 숏코딩을 성공해낸다면 그 성취감을 이루 말할 수 없을 것이다.
글을 쓰는 지금을 기준으로 숏코딩 1위를 하고 있는, 위의 내용이 모두 적용된 필자의 코드를 공유하겠다. 여러분도 JS로 숏코딩에 꼭 도전해보길 바란다.
http://boj.kr/593f8ca7afe74ed792a9ce2da2143eaa
+2024/12/19 기준으로 3위까지 내려갔다. 2위는 Node.js로 기존 내 코드에 무려 46글자나 줄였는데, 눈여겨볼 테크닉 몇 가지가 있다.
추후 더 깊고 고급진 숏코딩 기술로 돌아오겠다.
댓글