어지러운 js문법~

이종호·2021년 4월 3일
0

알고리즘

목록 보기
2/10
post-thumbnail

낡아버린 머리에 기름칠을 하기 위해 프로그래머스 level1부터 js로 풀어보기로 했다.

그러다 은근 시간초과로 고생하게된 문제가 있었는데
바로 완주하지못한 선수 이다.
https://programmers.co.kr/learn/courses/30/lessons/42576?language=javascript

처음에 객체로 만들어서 key는 이름, value로는 개수로 만들어 비교하면 빠르지 않을까 막연히 생각하고 돌렸더니 거침없이 시간초과가 났다.

고민고민하다가 두 배열 모두 sort하고 비교하는게 단순히 더 빠를것 같다는 생각이 들어 해봤더니 통과했다.

그래서 다른 분들은 어떻게 했는지 궁금해서 봤더니 어지러운 코드가 보였다.

문제의 코드

var solution=(_,$)=>_.find(_=>!$[_]--,$.map(_=>$[_]=($[_]|0)+1))

효율성 차이

한 줄이 끝이였는데, 가독성은 당연 떨어지지만 너무 탐스러워 보여 해설을 여기 정리해 적어본다.

새님의 해설

var solution=(participant,completion)=>participant.find(name=>!completion[name]--,completion.map(name=>completion[name]=(completion[name]|0)+1))

완주자 배열을 {이름: 완주자 배열에 등장하는 횟수}로 매핑하고, 그 맵에 참가자 이름 하나씩 넣어서 찾아볼 때 마다 횟수 떨어뜨려서 회수 0 나오는 놈 찾아 뱉는 함수

패터쓴님 해설

연구해보니 새님의 설명하신 것들이 정확
일단 _하고 $는 미니파이이자 고수들의 겉멋(?) 인것 같고,

participant.find(콜백 애로우 함수, 맵 펑션) 이렇게 두개의 argument가 주어진거고, 뒤의 맵 펑션이 콜백 전에 실행이 됩니다.
mdn문서 참조하면 thisArg가 있는 부분인데, 이 두번째 argument 부분이 첫 번째 보다 먼저 실행됩니다.
그러므로 함수가 시작되면 콜백 애로우 함수보다 두번째 맵 함수가 먼저 실행이 됩니다.

그러면 맵 함수에서 하는 일은 무엇인가, 맵 함수는 기볹거으로 새로운 배열을 리턴하는데, 여기서는 이 '리턴 되는 배열'은 의미가 없습니다.
다만 첫 번째 argument인 콜백 애로우 함수보다 이 두 번째 함수가 먼저 실행된다는 것이 중요합니다.

콜백에서 중요한 작업을 하기전에 여기서 미리 기름칠을 해놓은 것으로 보시면 됩니다.

그러면 리턴은 버려지는, 맵 함수에서 하는 일은 무엇이고 하면
completion[name] = (completion[name]|0) + 1

먼저 completion 배열에 대한 map이므로 name은 completion의 각 인수를 의미하고, completion[name]을 정의하는데 (=), 뭐로 만드냐하면,
OR을 사용해서 "만약 completion[name]이 존재하면, 그 값에 +1을 한 것", 이고
"존재 안하면, 0+1(즉 1)"으로 정의해라 라고 했습니다.

그럼 여기서 뽀인트는 completion[name]이 뭐냐 인데..

자바스크립트의 배열은 사실 객체입니다.
typeof로 아무 배열을 만들어서 확인해보면, 오브젝트가 나오죠
그러므로 completion[name]을 하면 단순히 우리가 객체에서 'key-value'페어에서 밸류 접근하듯이
"completion객체 중 name이라는 키의 값이 뭐니"가 됩니다.

그리고 만약에 그런 키-밸류 페어가 존재하지 않으며 undefined를 리턴하게 되고, 이것은 false가 되므로, 위의 OR 구절에서 앞이 false였으므로 뒤의 값이 0이 출동하게 되고,
실제로 정의되는 값은 0 + 1인 1이 되죠.
그러므로 컴프피션 배열 안에 "하나만 있는 이름"은 모두 :1 이렇게 변하게 됩니다.

만약에 이름 두개인 경우에는 completion[name]이 이미 존재해서 1이라는 값을 리턴할 거고, 그러면 1+1이되어 2개가 되고,

이름이 더 많으면 계속 더해집니다.
completion배열의 각 요소에 대해 이 작업을 끝내주면 이 "맵 펑션" 부분의 존재 이유가 달성되고,
completion은 처음에 인풋 받았던 배열이 아닌, 이름: 개수 들이 "추가가 된" 배열이 되죠
실제로 바뀐 배열을 체크해보면 이렇습니다.

['cake', 'ball', 'sauce', 'cake', cake:2, ball:1, sauce:1]
조금 웃긴 모습이죠

그럼 이제 준비 작업이 끝났고 본론으로 갑니다.
근제 이전에 이해를 돕기 위해 하나 덧붙이자면, 이준비 작업은 단순히 준비 작업이기 때문에 저 가진나는 펑션에서 빼내서 따로 적어도 됩니다.
이렇게 말이죠

var solution = (participant, completion) => {
  completion.map(name =>
    completion[name] = (completion[name] | 0) + 1
  );
  return participant.find((name) => !completion[name]--);
};


이게 뭘까.. ㅋㅋㅋ

본론으로 오면 이제, 우리의 completion은 작업이 끝난 상태이므로 앞 부분에 모드 이름이 스트링으로 들어있고, 그걱에 이어서 각이름의 갯수가 키-밸류 패어로 들어있습니다.
먼저 find는 뒤 테스트의 "트로"를 주는 첫 번째 요소를 리턴합니다.
그러면 이제 !completion[name]-- 이게 무슨 테스트인지를 보고 어떤 경우에 '트루'를 주즈냐를 생각해보면 되겠죠.

!completion[name]-- 여기서 중요한건 2개입니다.
completion[name]은 다들 알다시피 그 해당 이름의 "갯수"를 불러올 거죠.

중요1: 느낌표는 뭘하고 있냐?
이건 간단히 느낌 표 뒤의 값을 "불리언"으로 바꿔주는데, 반대로 바꾸줍니다.
!true이면 거짓이 나오죠
또 !0, !5, !''등 테스트 해보세요,
참고로 js에 거짓이 되는 밸류는 총 7개뿐입니다.
(false, 0, -0, NaN, null, undefined)

중요2: --는 아시다시피 1을 삭감하는데, 여기서 중요한건 '후치'라는 겁니다.
즉 값을 불러오고, !을 통해 참거짓을 판별을 하고, 그 열의 계산이 다끝나고 다음 열로 갈 때야 값이 바뀌는 거죠.
"불러온 순간의 값"으로 참거짓을 판별한다는게 중요합니다.

participant에 find를 했으므로 이제 모든 참가자의 이름을 갖고 completion에 이 이름이 몇 개 존재했어나를 보게 됩니다.
그러면 만약에 이름이 1개였으며, "불려오는 순간에는 1이므로 이것은 불리언으로 보면 참",
그리고 find의 참거짓 판별은 앞의 !느낌표로 인해 참은 거짓으로 변해서 '거짓'이 됩니다.
1이상이었어도 마찬가지로 0이상의 숫자였을 거니까, 거짓으로 판명이 나고 find함수는 계속 이어집니다.
그러면 언제 "참"이 나오냐면, 그것은 completion[name]이 '거짓'이 되는 값을 반환했을 때 뿐입니다.

현 상황에서 completion[name]을 수행한 후에 나올 수 있는 거짓이 될 값은 0이거나, 아니면 'name'에 해당하는 페어가 completion에 존재하지 않을 떄느 undefined를 리턴하겠죠. 그러면 false들을 리턴하게 되고, !로 인해서 true로 바뀌고 정답 개봉박두 하게 되는거죠.

참고로 '0'을 리턴하는 경우에 '동명의 참가자가 2명 이상 있었느네, 그 중에 1명이 안들어왔을 때', 이고
undefined를 리턴하게 되는 경우는 '못들어온 이름이 1개 뿌니었고, 그 사람이 못들어왔을 때'입니다.

여담으로 undefined에 !때리고 나서 후치로 --를 때려주면
해당 키밸류 페어가 존재하게 되고, 값은 NaN이 됩니다.

참고로 저는 둘다 소트하고

let answer = participant.find((el, index) => el !== completion[index]);

했는데, 제 해법보다 위의 해법이 속도가 훠어어얼씬 빠드더라구요

의문이 남는 부분은 어레이에 들어가 있는 키 밸류페어네요.
예를 들어
['cake', 'ball', 'sauce', 'cake', cake: 2, ball: 1, sauce: 1] 를 forEach로 각 엘러먼트를 로깅해보면 키밸류는 안나오더라구요.

YuiN님

forEach로 키밸류가 안나오는 이유가 Symbol로 들어가서 라는데, ..
숫자로 안되는 이유는 completion[name] + 1이 숫자로 계산되어서..

그래서 이해했나..

이해 못했다.
밥을 먹으면서 천천히 다시 봐야 할 것 같다.

그것보다 find함수를 쓰길래 어떤것인지 궁금해서 찾아보고 정리해야겠다.

profile
코딩은 해봐야 아는 것

0개의 댓글