0w0

도전장 정답 발표 및 해설, 배열 요소를 늘리는 방법? ~ 재밌는 정답도 소개 ~

몇 일 전에 Twitter에 이런 문제를 냈습니다

https://twitter.com/yumemiinc/status/1560086874303082497

유메미의 도전장 3탄

array quiz

이 글에 퀴즈 정답 발표를 하려 합니다.

퀴즈 내용

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;

나머지 연산자 (%)를 사용하면 좋겠지요

n2로 나눈 나머지를 구하면

이렇게요.

n % 2 결과는 1, 0 둘 중 하나입니다. 25같은건 없죠(너무 당연한가)

코드로 보면

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 이 식을 보고 스마트하고 "머리가 좋구나" 생각했습니다.

이렇게 되기에

여기에 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행으로 표현할 수 있습니다. 구체적 말하면 ops0xfff3326173141a5161입니다. 본래 배열 요소가 뒤에서부터 순서대로 있습니다.

이 인코딩한 것으로 코드를 쓰면 다음과 같이 됩니다. 압축된 수치에서 본래 명령 열을 빼기 위해서는 이렇게 비트 연산을 해야합니다. 이 수치는 숫자 길이가 길어서 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로 채우면 됩니다). 먼저 reduceforEach 처럼 써봅시다.

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에 넣은 opsreduce 안에 담은 것을 알 수 있습니다. 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);

최종에 가까운 코드가 되었습니다.

메서드 체인 느낌을 만든다

여기서는 메서드 체인 형태로 느껴지도록 코드를 고칩니다.

먼저 reduceslice를 잇죠. 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()를 사용하지 않게 정렬하는 점이 독자성이 있었다 생각합니다.

주저리주저리

그 외로 다음과 같은 해법을 검토했습니다만, 해답을 만들지 못해 날렸습니다.

이것이 어려웠기에 탐색에 의존하지 않고 해답을 만든 이번 형태가 되었습니다.