도전장 정답 발표 및 해설, 배열 요소를 늘리는 방법? ~ 재밌는 정답도 소개 ~
몇 일 전에 Twitter에 이런 문제를 냈습니다
https://twitter.com/yumemiinc/status/1560086874303082497
유메미의 도전장 3탄
이 글에 퀴즈 정답 발표를 하려 합니다.
퀴즈 내용
const array1 = [1, 2, 3, 4, 5, 6];
const array2 = array1 /* 여기에 해답을 적어보세요 */.console
.log(array2);
// -> [1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6]
요는,
[1, 2, 3, 4, 5, 6] 배열을
[1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6]로 만들기 위해서는
어떻게 해야 될까?
이런 문제입니다.
더 요약하면,
array
배열 각 요소 체크,
홀수
는3개씩
증가
짝수
는2개씩
증가그리고
array2
변수에 넣을 것
이런 문제입니다.
정답 발표
여기서 정답을 발표하겠습니다.
여러 방법이 있지만, 제가 생각했던 범위에 해답은 이렇습니다.
const array2 = array1.flatMap(n => n % 2 ? [n, n, n], [n, n])
flatMap()
메서드를 보신적이 없다면 "이게 뭐여" 느낄 수도 있습니다.
글을 따라 해설을 보시길 바랍니다.
해설
우선 이번에 노렸던 것은
array
배열 각 요소 체크,
홀수
는3개씩
증가
짝수
는2개씩
증가그리고
array2
변수에 넣을 것
이거였습니다.
array1
배열을 근거로 array2
를 만든다는 접근이 필요했습니다.
이러기 위해서는 map()
를 사용할 수 있겠지요.
map()
를 사용해서,
우선 배열 모든 요소를 2개씩 증가시켜봅시다.
const array2 = array1.map((n) => [n, n]);
이러면 각 요소를 모두 2개씩 들어가는 배열로 만듭니다.
결과는 이렇죠.
console.log(array2);
// => [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6]]
당연히 2중 배열이 되고요.
그렇기에 마지막에 flat()
를 해야 합니다.
const array2 = array1.map((n) => [n, n]).flat(); // <- .flat() 추가!
이렇게요.
그러면 원하던 답이 됩니다.
console.log(array2);
// => [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6]
이렇게 2중 배열을 평탄하게 만들 필요가 있었습니다.
결과적으로 모든 요소가 2개씩 증가되었습니다.
flatMap() 메서드 사용해보자
상기한
map()
하고flat()
하고 싶다
이럴 때는 flatMap()
메서드를 쓸 수 있습니다.
const array2 = array1.flatMap((n) => [n, n]);
console.log(array2);
// => [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6]
이렇게요.
결국 flatMap()
메서드는
(n) => [n, n];
이렇게 함수로 표기할 수 있습니다.
배열 요소를 증가시키는 메서드라 할 수 있겠지요.
여담으로
flatMap()
메서드에
(n) => [];
이렇게 넘기면, 요소를 제외(뺄)시킬 수 있습니다.
퀴즈 해설을 이어 설명하면
const array2 = array1.flatMap((n) => [n, n]);
console.log(array2);
// => [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6]
아무튼 이렇게 요소를 2개씩 증가시켰습니다.
이제는
홀수
는3개씩
증가
짝수
는2개씩
증가
이 조건 분기처리가 필요하겠네요.
홀수/짝수 알아내기 위해
n % 2;
나머지 연산자 (%
)를 사용하면 좋겠지요
n
를 2
로 나눈 나머지를 구하면
n
를2
나눠 나머지가1
이면 홀수0
면 짝수
이렇게요.
※n % 2
결과는 1
, 0
둘 중 하나입니다. 2
나 5
같은건 없죠(너무 당연한가)
코드로 보면
if (n % 2 === 1) {
// 홀수는 여기에
} else {
// 짝수는 여기에
}
이런 느낌입니다.
조건에 따라 배열 요소를 늘려보자
앞서 설명한 flatMap()
메서드
(n) => [n, n];
이걸 함수에 넘기면 각 요소를 2개씩 증가시킬 수 있습니다.
여기에 홀수면 어떤가 조건을 더하고 싶기에...
(n) => {
if (n % 2 === 1) {
// 홀수면 3개!
return [n, n, n];
} else {
// 짝수면 2개!
return [n, n];
}
};
이렇게 쓰면 됩니다.
이 함수를 flatMap()
메서드에 넘겨주면
홀수
는3개씩
증가
짝수
는2개씩
증가
이 조건이 달성됩니다.
삼항을 사용해 1행으로 쓰면...
(n) => (n % 2 === 1 ? [n, n, n] : [n, n]);
이리됩니다.
n % 2===1
조건은
이번에는 n % 2
만 써도 되기에
(n) => (n % 2 ? [n, n, n] : [n, n]);
이렇게해도 OK입니다.
그럼 이 함수를 flatMap()
메서드에 넘겨봅시다.
const array1 = [1, 2, 3, 4, 5, 6];
const array2 = array1.flatMap((n) => (n % 2 ? [n, n, n] : [n, n]));
console.log(array2);
// -> [1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6]
이렇게하면 완성입니다.
해설은 이상입니다.
별답
이런 해답을 한 분도 계십니다.
https://twitter.com/cuttlefish_math/status/1560236022625357824?s=20&t=fe51-vqoXg-J-wJT8FJ5Gg
메인 처리로
const array2 = array1.flatMap((x) => Array((x % 2) + 2).fill(x));
이렇게 했습니다.
flatMap()
메서드를
(x) => Array((x % 2) + 2).fill(x);
이런 함수를 넘겼습니다.
x % 2 + 2
이 식을 보고 스마트하고 "머리가 좋구나" 생각했습니다.
x % 2
는x
가 홀수라면1
x
가 짝수라면0
이렇게 되기에
여기에 2
를 더해
x % 2 + 2
는x
가 홀수라면3
x
가 짝수라면2
이렇게 됩니다...!
Array(x % 2 + 2)
부분은
Array(3)
또는 Array(2)
이 된 것입니다.
이런 식으로 길이를 지정해 배열을 생성해서,
fill()
메서드로 채우면,
와우...
애초에 Array()
가 new
가 없어도 되는 거였군요...!
이것도 처음 알았습니다.
퀴즈 하나로 여기까지 공부가 될 줄이야... 엄청 이득본 기분...!
재밌는 대답 여럿
재밌는 대답 1
https://twitter.com/shindy_JP/status/1560245331778236417?s=20&t=fe51-vqoXg-J-wJT8FJ5Gg
메인 처리
const array2 = array1.concat(1, 1, 2, 3, 3, 4, 5, 5, 6).sort();
와... array1
에 부족한 부분만 더해 sort()
라니!
조금 억지 같기도 하지만 규칙상 정답입니다...!
재밌는 대답 2
https://twitter.com/D_drAAgon/status/1560261919659139073?s=20&t=fe51-vqoXg-J-wJT8FJ5Gg
메인 처리
const array2 = array1
.slice(6)
.concat([1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6]);
와 array1
요소는 어디에!
그냥 생으로 갈아 끼웠잖아!
애초에 출력하고 싶은 배열을 그냥 적었는데!
하지만, 규칙 상 정답입니다...!
재밌는 대답 3
https://twitter.com/uhyo_/status/1560109480062652416?s=20&t=fe51-vqoXg-J-wJT8FJ5Gg
메인 처리
const array2 = array1
.concat(array1, array1)
.reduce(
(a, b, c, d) => (d.push(...d.splice(Number(a % 16n), 1)), a >> 4n || d),
4721443915042874085729n
)
.slice(0, 15);
난독의 극치!
하지만, 실행해보니 정답이었습니다...!
역시 @uhyo 대선생님...!
재밌는 대답 4
https://twitter.com/fuwasegu/status/1560267913973870593?s=20&t=fe51-vqoXg-J-wJT8FJ5Gg
유저가 판단 해 배열에 추가라니!
몇 번 버튼을 누르는거야!
애초에 알고리즘을 "운용으로 커버"라니!
하지만 정답은 정답...?
재밌는 대답 5
https://twitter.com/yusuke/status/1560176286974492672?s=20&t=fe51-vqoXg-J-wJT8FJ5Gg
const array2 = array1.length;const 待避 = console.log console.log = function () { 待避('[1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6]'); };
한 줄에 몇개를 적는겁니까!
게다가 console.log()
메서드를 덮어쓰기라니!
하지만 세미콜론 금지 규칙었었으니...
정답이라 봐도 될지도...?
재밌는 대답 6
https://twitter.com/Yametaro1983/status/1560104149102239749?s=20&t=fe51-vqoXg-J-wJT8FJ5Gg
메인 처리
const array2 = array1.unko || [1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6];
어이 제발 array1
를 사용해줘!
(제 자신이 쓴 거지만)
총괄
마지막은 일발 개그 대회처럼 되었습니다만,
수 백여명의 참가해셔서 꽤 성대했습니다.
출제자 저 자신도
에, 이런 방법도 있었어!
공부되었습니다.
참가해주신 모든 분, 감사합니다!
만약 괜찮다면 도전해주세요
어쩌면 또 다른 답이 있을지도 모릅니다.
TypeScript 플레이그라운드를 준비해 두었으니,
괜찮으시면 도전해 주십시오.
그리고 재밌는 대답이 떠오르셨다면, 알려주세요... :)
https://twitter.com/Yametaro1983/status/1560091681042857985?s=20&t=Ixp1wzr_pmZ_Cr_CeD4gag
새 글도 썼습니다!
부록: 유메미의 도전장 3탄 reduce 이용한 접근법
안녕하세요 여러분. 얼마전, 일발 개그 대회 유메미의 도전장으로 JavaScript 퀴즈가 굉장히 성대했습니다. 링크처의 글에는 제 해답이 재밌는 대답으로 소개되어 있습니다. 모범 답안 flatMap
이 적혀있으니, 필자 해답에는 배열 조작의 핵심중 하나인 reduce
메서드를 사용했습니다.
그리고 접근법이 꽤 난해한 코드였기에 해설 글을 적습니다.
문제와 해답
제출된 퀴즈 내용은 이렇습니다. (상술한 글 인용).
const array1 = [1, 2, 3, 4, 5, 6];
const array2 = array1 /* 여기에 해답 */.console
.log(array2);
// -> [1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6]
배열을 지시대로 가공하는 코드를 쓰면 되는 문제입니다.
필자의 대답은 이러합니다.
const array1 = [1, 2, 3, 4, 5, 6];
const array2 = array1
.concat(array1, array1)
.reduce(
(a, b, c, d) => (d.push(...d.splice(Number(a % 16n), 1)), a >> 4n || d),
4721443915042874085729n
)
.slice(0, 15);
console.log(array2);
트위터에 담길 정도로 적었기에, 꽤 읽기 거시기합니다. 그렇기에 이 글에서는 이 코드가 어떻게 돌아가는가 해설합니다.
해설
위 코드를 알기 쉽게 적으면 이러합니다.
const array1 = [1, 2, 3, 4, 5, 6];
const work = array1.concat(array1, array1);
const ops = [1, 6, 1, 5, 10, 1, 4, 1, 3, 7, 1, 6, 2, 3, 3, 15, 15, 15];
for (const x of ops) {
const tmp = work.splice(x, 1)[0];
work.push(tmp);
}
const array2 = work.slice(0, 15);
console.log(array2);
work
배열에 splice
, push
에 의한 파괴적 조작을 반복함으로 답을 얻는 방식입니다. splice
는 지금까지 배열 (상기의 x
번째) 부터 요소를 1개씩 빼기 위해 준비했습니다. 뺀 요소는 push
로 뒤에 붙습니다.
이 2개의 조작 세트로 "배열 도중 요소를 하나 맨 뒤로 이동한다` 조작을 할 수 있습니다. 어떤 의미로 정렬입니다.
우선 작업용 배열 work
는 [1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
이며, 이걸 18번 정렬해서 work
는 [1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 2, 4]
로 만듭니다. 뒤의 3개는 남는 것이므로, slice
로 여분를 버려 array2
를 완성합니다.
정렬 조작의 구체적 내용은, 필자의 정성껏 정제하여, ops
가 되었습니다. 이 [1, 6, 1, ……]
배열은 먼저 첫 요소를 뒤로 옮기고 다음 6번 째 요소를 뒤로 옮겨.... 조작 열입니다.
정렬 순서를 수치로 인코딩
ops
를 정의했지만, 이건 꽤 낭비이므로 압축합시다.
이번 같은 정수의 열은 하나의 정수로 인코딩할 수 있습니다. ops
요소는 하나, 4비트로 표현할 수 있으므로, 딱 16진수 1요소 1행으로 표현할 수 있습니다. 구체적 말하면 ops
는 0xfff3326173141a5161
입니다. 본래 배열 요소가 뒤에서부터 순서대로 있습니다.
이 인코딩한 것으로 코드를 쓰면 다음과 같이 됩니다. 압축된 수치에서 본래 명령 열을 빼기 위해서는 이렇게 비트 연산을 해야합니다. 이 수치는 숫자 길이가 길어서 BigInt를 사용해야하니 주의해주세요.
const array1 = [1, 2, 3, 4, 5, 6];
const work = array1.concat(array1, array1);
for (let ops = 0xfff3326173141a5161n; ops; ops >>= 4n) {
const tmp = work.splice(Number(ops % 16n), 1)[0];
work.push(tmp);
}
const array2 = work.slice(0, 15);
console.log(array2);
splice
인수는 BigInt가 아니라 수치를 넘겨주기 위해 필요한 것이므로 Number(ops % 16n)
로 수치 변환해줍시다.
이 Number
코드가 눈에 바로 띄어 임팩트를 잃어버리는 것인 아쉽지만 낡은 언어 사양에 의해 원형타입 사이의 암묵적 변환을 반성하기 위함인지, JavaScript의 BigInt는 Number
를 사용해 명확히 변환하지 않으면 에러가 되므로 별 수 없습니다.
루프를 reduce로 변환
이번 문제는 array1.처리
이므로 for문으로 루프할 수 없습니다. ;
를 사용해 억지로 for문을 써도 되겠지만, 모처럼이니 아름다움을 위해 reduce를 사용했습니다.
애초에 ops
가 18요소 배열입니다. 또 work
도 18요소. 그렇기에 reduce
로 루프를 돌리면 딱 좋은 숫자입니다(애초에 ops
마지막 3개 15
는 아무 것도 하지 않음을 의미하므로 요소가 18개가 될 때까지 15
로 채우면 됩니다). 먼저 reduce
를 forEach
처럼 써봅시다.
const array1 = [1, 2, 3, 4, 5, 6];
const work = array1.concat(array1, array1);
let ops = 0xfff3326173141a5161n;
work.reduce(() => {
const tmp = work.splice(Number(ops % 16n), 1)[0];
work.push(tmp);
ops >>= 4n;
});
const array2 = work.slice(0, 15);
console.log(array2);
위 코드에서 let
에 넣은 ops
를 reduce
안에 담은 것을 알 수 있습니다. reduce
는 애초에 초기화를 준비하고 배열을 루프하며 그 값을 변환하기 위한 것입니다. 이번에는 ops
가 루프를하며 변환되기에, 이 기능을 사용합니다.
const array1 = [1, 2, 3, 4, 5, 6];
const work = array1.concat(array1, array1);
work.reduce((ops) => {
const tmp = work.splice(Number(ops % 16n), 1)[0];
work.push(tmp);
return ops >> 4n;
}, 0xfff3326173141a5161n);
const array2 = work.slice(0, 15);
console.log(array2);
최종에 가까운 코드가 되었습니다.
메서드 체인 느낌을 만든다
여기서는 메서드 체인 형태로 느껴지도록 코드를 고칩니다.
먼저 reduce
와 slice
를 잇죠. work.reduce
는 BigInt를 조작하며 루프하기 위해 최종적인 반환 값도 BigInt가 됩니다. 그렇기 위해 이 상태로 slice
를 할 수 없습니다.
실은 ops >> 4
반복해 ops
내용을 지워가면, 루프 종료 타이밍에 딱 0n
이 됩니다. 결국 work.reduce
반환 값은 0n
입니다.
이를 이용해서 루프 종료시에 reduce
반환 값이 BigInt가 아니라 배열이 되도록 하기 위해, slice
로 이어줘야 합니다. 다음 기술할 코드같이 ops >> 4n
가 정확한 값일 때 다음 루프에 준비한 BigInt를 반환, 0이 되면 마지막에는 work
를 반환함으로 다음 slice
를 잇습니다.
const array1 = [1, 2, 3, 4, 5, 6];
const work = array1.concat(array1, array1);
const array2 = work
.reduce((ops) => {
const tmp = work.splice(Number(ops % 16n), 1)[0];
work.push(tmp);
return ops >> 4n || work;
}, 0xfff3326173141a5161n)
.slice(0, 15);
console.log(array2);
이렇게 reduce
기억 영역을 2개의 용도로 사용하는 것만으로도 제입으로 말히 그렇지만 예능점수가 높습니다.
다음에는 concat
, reduce
도 이어봅시다. 여기서 어려운 점은 reduce
콜백 안에 work
에 대한 파괴적 조작을 하기에 먼저 work
변수에 넣을 필요가 있습니다. 메서드 체인하면 work
접근할 수 없어보입니다.
그러면 reduce
콜백 함수는 제 4인수에 this
같은 배열을 받는 것이 가능해집니다. 이를 사용해서 work
를 사전 변수를 넣을 필요가 없어집니다.
const array1 = [1, 2, 3, 4, 5, 6];
const array2 = array1
.concat(array1, array1)
.reduce((ops, _cur, _i, work) => {
const tmp = work.splice(Number(ops % 16n), 1)[0];
work.push(tmp);
return ops >> 4n || work;
}, 0xfff3326173141a5161n)
.slice(0, 15);
console.log(array2);
이렇게 하면 퀴즈 규칙에 따른 코드가 됩니다. 또 tmp
삭제, 변수명 1글자로 바꾸기 등 읽기 어렵게 합니다. 그리고 16진수로 적은 매직넘버는 10진수로 다시 고쳐 수상함을 올리는 것이 정석이죠.
이렇게하면 모두의 코드가 됩니다.
const array1 = [1, 2, 3, 4, 5, 6];
const array2 = array1
.concat(array1, array1)
.reduce(
(a, b, c, d) => (d.push(...d.splice(Number(a % 16n), 1)), a >> 4n || d),
4721443915042874085729n
)
.slice(0, 15);
console.log(array2);
더욱 개설할 수 있는 부분
위 코드는 잘 보면 컨셉을 바꾸지 않고서 개선할 점이 몇 있습니다. 하나는 이 코드에는 reduce
콜백 함수가 (a, b, c ,d) => (...)
이런 ( )
로 감싸는 형태입니다. 이는 안에 컴마연산자를 사용하기 때문입니다. 하지만 d.push
가 늘 1를 반환하는 걸 이용하면, 다음과 같이 괄호를 지울 수 있습니다. 이를 글을 올리기 전에 눈치 채지 못한 것이 반성할 부분입니다.
const array1 = [1, 2, 3, 4, 5, 6];
const array2 = array1
.concat(array1, array1)
.reduce(
(a, b, c, d) => (d.push(...d.splice(Number(a % 16n), 1)) && a >> 4n) || d,
4721443915042874085729n
)
.slice(0, 15);
console.log(array2);
또 .slice(0, 15)
가 아니라 .slice(3)
이 되도록 정렬(갈아 끼우기)하는 것이 조금 더 깔끔해 보일지도 모릅니다.
애초에 slice
가 필요한 경우는 array1.concat(array1, array1)
같이 18요소 배열을 사용하는 한 편, 콜이 15요소이기 때문입니다. 재료가 18요소로 되는 것은 필요한 수치를 하드코딩하지 않고, array1
를 재료로 하는 것이 아름답다 느끼기 때문입니다.
정리
필자가 준비한 답의 해설은 여기까지 입니다. 재료를 준비해 sort()
하는 해답은 다른 곳에서도 볼 수 있으나, 재료를 어디까지나 array1
에서 조달하며, sort()
를 사용하지 않게 정렬하는 점이 독자성이 있었다 생각합니다.
주저리주저리
그 외로 다음과 같은 해법을 검토했습니다만, 해답을 만들지 못해 날렸습니다.
- 결정적인 셔플 알고리즘을 매직넘버로 지정해 횟수만큼 돌려 우연 목적 형태
- 트위터에 담길 정도로 깔끔하게 잘 섞인 알고리즘이 의외로 만들어지지 않고, 매직넘버를 탐색했지만 찾을 수 없어 날림.
array1.concat(array1,array1)
에 map해서, 적절한 계수로Math.cos
나Math.sin
를 더한 계산.- 탐색해도 적당한 계수를 찾이 못해 날림.
이것이 어려웠기에 탐색에 의존하지 않고 해답을 만든 이번 형태가 되었습니다.