(프로그래머스 문제 푸는 중)
프로그래머스 문제를 중간 정도까지 해결했다. 예시로 나온 테스트 케이스들은 결과값이 잘 나오는데 실제 테스트케이스들은 하나도 통과를 못하고 런타임 에러를 낸다. …한글 변수명 때문에 그런가?
또 기존의 산문같은 코드를 findNext()나 isMovable() 같이 함수로 묶어봤는데 잘 되던 예시 테케도 잘 안된다. light를 인수로 넘겨주는 과정에 뭔가 오류가 있는 것 같은데 다음에 다시 살펴봐야겠다.
작성중 코드:
// 이상해지기 전 (함수로 추상화하기 전) 코드 (테스트케이스: function solution2(gridString) { // 1. 2d 배열 초기화: 0으로 채워진 n x n 배열을 만든다. // => 3d 배열 초기화: [상, 하, 좌, 우]로 채워진 row(=GRID_HEIGHT) x col(=GRID_LENGTH) 배열을 만든다. const GRID_LENGTH = gridString[0].length; const GRID_HEIGHT = gridString.length; const n = 3 // GRID: // ┌─────────┬─────┬─────┐ // │ (index) │ 0 │ 1 │ // ├─────────┼─────┼─────┤ // │ 0 │ 'S' │ 'L' │ // │ 1 │ 'L' │ 'R' │ // └─────────┴─────┴─────┘ const GRID = new Array(); for (let i = 0; i < GRID_HEIGHT; i++) { GRID[i] = gridString[i].split('') } // console.log("GRID: ") // console.table(GRID) // directionGrid: // ┌─────────┬────────────────────────────----──┬─────────────────────────────----─┐ // │ (index) │ 0 │ 1 │ // ├─────────┼────────────────────────────----──┼─────────────────────────────----─┤ // │ 0 │ { '상': true, '하': true, ... } │ { '상': false, '하': false, ... } │ // │ 1 │ { '상': false, '하': false, ... } │ { '상': false, '하': false, ... } │ // └─────────┴────────────────────────────----──┴─────────────────────────────----─┘ const directionGrid = JSON.parse(JSON.stringify( Array(GRID_HEIGHT).fill(Array(GRID_LENGTH).fill({ 상: false, 하: false, 좌: false, 우: false })) )); // console.log("directionGrid: ") // console.table(directionGrid) const 순환경로리스트 = []; const 막힌경로리스트 = []; // 순환 시작 function trackCycle(initialLight) { // initialLight = { // 떠날때방향: "하", // 현재좌표: { x: 0, y: 0 } // } const START_X = initialLight.현재좌표.x; const START_Y = initialLight.현재좌표.y; const 이경로발자국 = [{...initialLight.현재좌표}] const light = initialLight; // 2. 빛의 이동: function move(light) { let 도착좌표 = {...light.현재좌표} if (light.떠날때방향 == '상') { if (light.현재좌표.y > 0) { 도착좌표.y -= 1 } else { 도착좌표.y = GRID_HEIGHT - 1; } } else if (light.떠날때방향 == '하') { if (light.현재좌표.y < GRID_HEIGHT - 1) { 도착좌표.y += 1 } else { 도착좌표.y = 0; } } else if (light.떠날때방향 == '좌') { if (light.현재좌표.x > 0) { 도착좌표.x -= 1 } else { 도착좌표.x = GRID_LENGTH - 1; } } else { if (light.현재좌표.x < GRID_LENGTH - 1) { 도착좌표.x += 1 } else { 도착좌표.x = 0; } } // console.log("(현재좌표로 업데이트할) 도착좌표: ", 도착좌표) const 사방위상태 = directionGrid[light.현재좌표.x][light.현재좌표.y]; 사방위상태[light.떠날때방향] = true; // light.떠나온좌표 = {...light.현재좌표}; light.현재좌표 = {...도착좌표}; 이경로발자국.push(light.현재좌표) // 깊은 복사 처리 완료. // console.log("이경로발자국: ") // console.table(이경로발자국); } let loop = 1; // console.log(`\n--loop ${loop}--`); move(light) while (true) { loop++; // console.log(`\n--loop ${loop}--`); // 1. 다음 방향 탐색 (도착 직후) : // 1-1. light.떠날때방향 갱신: const 현재x = light.현재좌표.x const 현재y = light.현재좌표.y const 현재gridType = GRID[현재y][현재x]; // console.log("현재 좌표: ", light.현재좌표) // console.log("현재gridType: ", 현재gridType) // 1-2. 떠나온 방향에 따른 다음 나가는 방향 { 떠나올 때 방향: 나가는 방향 } const gridS = { 상: "상", 하: "하", 좌: "좌", 우: "우" } const gridL = { 상: "좌", 하: "우", 좌: "하", 우: "상" } const gridR = { 상: "우", 하: "좌", 좌: "상", 우: "하" } if (현재gridType == 'S') { light.떠날때방향 = eval('gridS.' + light.떠날때방향); } else if (현재gridType == 'L') { light.떠날때방향 = eval('gridL.' + light.떠날때방향); } else { light.떠날때방향 = eval('gridR.' + light.떠날때방향); } // console.log("떠날 방향: ", light.떠날때방향) // 1-3. 진짜 '떠날 수 있는지' 체크: // const 떠날때방향 = light.떠날때방향; // => 사방위상태.떠날때방향 : '떠날때방향'이라는 키로 새 속성을 만들어버림. 소용없다. const 사방위상태 = directionGrid[현재y][현재x]; // console.log("떠날 수 있는지? : ", 사방위상태) if (사방위상태[light.떠날때방향]) { // 일단 더이상 경로를 이어가지는 못함 // // 만약 지금이 시작 좌표라면 // if (이경로발자국.length == 1) { // // 순환/막힌경로리스트에 추가하지 않고 지나감. // break // } // 만약 현재 좌표가 순환을 시작한 좌표라면, '순환 완성' if (현재x === START_X && 현재y === START_Y) { 순환경로리스트.push(이경로발자국) // console.log("순환 완성: 순환경로리스트 ", 순환경로리스트) } else { // 그게 아니라면 순환 중에 경로가 겹쳤다는 뜻. 순환이 되지 못함. 이 경로는 폐기 처분. 막힌경로리스트.push(이경로발자국) // console.log("순환 실패: 막힌경로리스트 ", 막힌경로리스트) } // 이 경로 탈출 break; } // 2. 빛의 이동 (출발 -> 도착) : // 도착 좌표 계산, '현재 좌표(갱신 전)'의 떠날 방향=true 처리, // '현재 좌표' 갱신, '이경로'에 도착한 좌표 업데이트: let 도착좌표 = {...light.현재좌표} if (light.떠날때방향 == '상') { if (light.현재좌표.y > 0) { 도착좌표.y -= 1 } else { 도착좌표.y = GRID_HEIGHT - 1; } } else if (light.떠날때방향 == '하') { if (light.현재좌표.y < GRID_HEIGHT - 1) { 도착좌표.y += 1 } else { 도착좌표.y = 0; } } else if (light.떠날때방향 == '좌') { if (light.현재좌표.x > 0) { 도착좌표.x -= 1 } else { 도착좌표.x = GRID_LENGTH - 1; } } else { if (light.현재좌표.x < GRID_LENGTH - 1) { 도착좌표.x += 1 } else { 도착좌표.x = 0; } } // console.log("(현재좌표로 업데이트할) 도착좌표: ", 도착좌표) 사방위상태[light.떠날때방향] = true; light.현재좌표 = {...도착좌표}; 이경로발자국.push(light.현재좌표) // 깊은 복사 처리 완료. // console.log("이경로발자국: ") // console.table(이경로발자국); } } // 다른 순환 경로 탐색 // 각 GRID 좌표의 각 방위(directionGrid) 전부를 훑기 for (let row = 0; row < GRID_HEIGHT; row++) { for (let col = 0; col < GRID_LENGTH; col++) { let values = Object.values(directionGrid[row][col]); // console.log(`${row}행 ${col}열 사방위상태: ${values}`) if (!values.includes(false)) { // console.log('빠져나감') continue; } for (let i = 0; i < Object.entries(directionGrid[row][col]).length; i++) { let directions = Object.entries(directionGrid[row][col]); // console.log("3중 루프 안에서 현재 사방위상태: ", directionGrid[row][col]) // console.log("현재 검사 방향: ", directions[i]) if (!directions[i][1]) { // 예를 들어 '상'방위가 false라면 // 새 순환 경로 추적 시작 // // 필요 항목: 시작 좌표, 시작 방향 // trackCycle(시작좌표=[row, col], 시작방향=direction[0]); // 아니다 아예 light 객체를 넘기면 되겠구나 const newLight = { 떠날때방향: directions[i][0], 현재좌표: { x: row, y: col } } trackCycle(newLight) } } } } // 나중에 순환경로리스트를 다 채우면 const cycleLengths = 순환경로리스트.map(cycle => cycle.length - 1); // => 밟아온 총 좌표 길이에서 출발 좌표 하나를 빼야 '경로의 길이'가 됨. // console.log(`\n순환경로리스트: ${순환경로리스트}`) // console.log(`\n막힌경로리스트: ${막힌경로리스트}`) // console.log(`\ncycleLengths: ${cycleLengths}`) cycleLengths.sort((a, b) => a - b); return cycleLengths; }
배운 것:
- 내부 요소 하나씩 수정이 가능하도록 2차원 배열 만드는 방법 → 원시타입을 내부 요소로 채워 넣을 땐 Array.from()을 이용하는 방법과 JSON.stringify / JSON.parse 콤보를 이용하는 방법 두가지 모두 된다. 원시타입이 아니라 참조형(배열이나 객체)을 내부 요소로 채워 넣어야 할 때는 Array.from() 방식은 통하지 않고 JSON.stringify / JSON.parse 콤보를 이용하는 방법만 제대로 작동한다.
- 반복문 도중 변경된 사항 반영하는 방법 → for 선언부에 박아둔 반복 변수는 루프가 반복되어도 업데이트 되지 않는다. 따라서 반복 도중 업데이트 된 값을 가져와 쓰고 싶다면 for … of 선언부에 반복변수로 할당하지 말고, 보통의 for문에 인덱스를 반복변수로 할당한 후 body로 들어와서 원하는 부분을 인덱스로 참조해오는 방식으로 사용해야 한다.
느낀 점:
다음은 같은 캠프의 대원님과 얘기를 나누고 조언을 들으며 생각하게 된 점들이다.
- 코딩 (알고리즘) 문제를 풀 때 너무 오래 고민하고 있지 말자.
숙고하는 것은 시간이 많으면 좋은 방식이지만, 빠르게 달려나가야 할 때는 해답을 보면서 많은 문제를 습득해 나가는 것이 더 좋다.
- 관심 있는 분야의 구인 공고를 이제 들여다보아야 하는 시기가 다가온 것 같다.
- 관심 있는 분야, 내가 원하는 서비스를 찾기 위한 질문:
코딩 안할때 주로 뭘 하는지, 인터넷할 때 가장 많이 찾게 되는 것이 뭔지, 유튜브에서 자주 시청하게 되는 것이 뭔지 등.
- 여러 튜터님들 말하길, 내가 취업했을 때 현업에서 CRUD 만드는 것은 거의 안 할 것이다. 취업 준비할거면 알고리즘이랑 데이터쪽 을 많이 보라. 고 함.
2차원 배열 요소 하나를 수정했는데 한 열 전체가 바뀌어버리는 문제 ⇒ 내부 요소 하나만 수정 가능하도록 2차원 배열 만들기 ✔️
2차원 배열 요소 하나를 수정했는데 한 열 전체가 바뀌어버리는 문제 ⇒ 내부 요소 하나만 수정 가능하도록 2차원 배열 만들기 ✔️
다차원 배열. 다중 배열. 중첩 배열
2차원 배열을 만드는 기존의 방법:
// 방법 1.
const array = [...Array(행수)].map(e => Array(열수).fill(0))
// 방법 2.
const array2 = Array(행수).fill(Array(열수).fill(0)
이렇게 하면 처음에 만든 후로 요소 하나하나를 수정하지는 못하게 된다. 예를 들어 array[0][1] = 1이라고 수정했을 시
[[0,1,0],
[0,1,0],
[0,1,0]]
이렇게 모든 ‘행’이 바뀌게 된다. 그 이유는, 바로 Array.fill() 메소드는 내부적으로 하나의 배열만 만들어서 그걸 3번 반복해서 참조하는 방식으로 내부를 채우기 때문이다.
The reason for this was, that there has only been one row, which had internally been referenced three times. So when I changed the first index in "the second" row, it effectively changed all rows. (참조: https://stackoverflow.com/questions/9979560/javascript-multidimensional-array-updating-specific-element)
Array.fill()을 사용하지 말고, 반복문을 돌려서 매번 새 행(배열)을 만들어 push하는 방식으로 만들어줘야 이런 문제를 해결할 수 있다.
해결법 1.
let arr = Array.from(Array(5), () => {
return new Array(5).fill(0)
})
arr[0][2] = 1;
console.table(arr);
=> 결과:
┌─────────┬───┬───┬───┬───┬───┐
│ (index) │ 0 │ 1 │ 2 │ 3 │ 4 │
├─────────┼───┼───┼───┼───┼───┤
│ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │
│ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │
│ 2 │ 0 │ 0 │ 0 │ 0 │ 0 │
│ 3 │ 0 │ 0 │ 0 │ 0 │ 0 │
│ 4 │ 0 │ 0 │ 0 │ 0 │ 0 │
└─────────┴───┴───┴───┴───┴───┘
⇒ 하지만 이것도 내부의 .fill()
에 단순히 number인 0을 넣을 땐 괜찮지만, number가 아닌 빈 배열이나 객체를 넣을 시 요소 하나 수정에 전과 똑같은 문제가 발생하게 된다 :
let arr = Array.from(Array(3), () => {
return new Array(3).fill({})
})
arr[0][2] = true;
arr[0][1].우 = true;
console.table(arr)
=> 결과:
┌─────────┬────────────────┬────────────────┬──────┐
│ (index) │ 0 │ 1 │ 2 │
├─────────┼────────────────┼────────────────┼──────┤
│ 0 │ { '우': true } │ { '우': true } │ true │
│ 1 │ {} │ {} │ {} │
│ 2 │ {} │ {} │ {} │
└─────────┴────────────────┴────────────────┴──────┘
해결법 2.
let foo = JSON.parse(JSON.stringify(Array(3).fill(Array(3).fill(0))));
foo[0][1] = 1;
console.table(foo);
=> 결과:
┌─────────┬───┬───┬───┐
│ (index) │ 0 │ 1 │ 2 │
├─────────┼───┼───┼───┤
│ 0 │ 0 │ 1 │ 0 │
│ 1 │ 0 │ 0 │ 0 │
│ 2 │ 0 │ 0 │ 0 │
└─────────┴───┴───┴───┘
⇒ 코드가 다소 지저분해 보이긴 하지만 .fill()
에 0 대신 빈 배열이나 객체를 넣어도 요소 하나씩 수정이 가능하다 :
let foo = JSON.parse(JSON.stringify(Array(3).fill(Array(3).fill({}))));
foo[0][2] = true;
foo[0][0].우 = true;
console.table(foo)
=> 결과:
┌─────────┬────────────────┬────┬──────┐
│ (index) │ 0 │ 1 │ 2 │
├─────────┼────────────────┼────┼──────┤
│ 0 │ { '우': true } │ {} │ true │
│ 1 │ {} │ {} │ {} │
│ 2 │ {} │ {} │ {} │
└─────────┴────────────────┴────┴──────┘
반복문에서 순회하는 대상을 수정했는데 반영이 안 되는 문제 ⇒ 반복문에서 순회하는 대상을 수정하려면 ✔️
반복문에서 순회하는 대상을 수정했는데 반영이 안 되는 문제 ⇒ 반복문에서 순회하는 대상을 수정하려면 ✔️
⇒ 수정하지 않는 이상 순회하는 대상(target)은 변경되지 않는다는 속성을 이용해 filter와 indexOf를 조합하면 배열에 있는 중복을 제거 할 수 있다. 에서, “수정하지 않는 이상 순회하는 대상(target)은 변경되지 않는다”는 속성을 이용해 다음과 같이 배열.filter() 메소드를 활용했었다:
// 중복 요소 제거
const numbers = [1, 1, 2, 2, 3, 1, 4, 5];
const newNumbers = numbers.filter((number, index, target) => {
return target.indexOf(number) === index;
});
/*
number, index => target.indexOf(number), index
1, 0, => 0 === 0
1, 1, => 0 === 1
2, 2, => 2 === 2
2, 3, => 2 === 3
3, 4, => 4 === 4
1, 5, => 0 === 5
...
아. '처음 나타난 위치'를 제외하고는 모든 다른 위치(인덱스)에 재발견되는 요소들은 무시되게 되는 원리..!
*/
console.log(newNumbers);
// [1, 2, 3, 4, 5]
반대로 말하면, 반복문을 도는 중에 대상 배열에 수정이 가해진 것이 반영되게 만들고 싶으면, 대상 배열에 변화가 생기도록 만들면 되겠다.
‘수정이 가해진다’는 건 뭐지?
비슷한 Array.find() 공식 문서의 예시를 보면, 대략 이런 식으로 순회가 처리되는 것 같다:
find
가 처리할 배열 요소의 범위는 첫callback
이 호출되기 전에 먼저 결정됩니다. 반복 도중 추가:find
메서드가 실행 된 이후에 배열에 추가된 요소들에 대해서는callback
이 호출되지 않습니다. 반복 도중 삭제/수정: 아직callback
이 호출되지 않았던 배열 요소가callback
에 의해서 변경된 경우,find
가 해당 요소의 인덱스를 방문할 때의 값으로callback
함수에 전달될 것입니다. 즉, 삭제된 요소에도callback
이 호출됩니다.
아무튼 처음 반복문에 진입할 때의 배열 모습 그대로를 못박아두고 끝까지 돈다는 얘기다.
음.
Array.find(), Array.filter()같은 메소드는 위의 “처리할 배열 요소의 범위는 첫 callback
이 호출되기 전에 먼저 결정된다”, 그래서 “수정하지 않는 이상 순회하는 대상(target)은 변경되지 않는다”가 적용된다면,
보통의 반복문에서는 수정시 수정사항이 반영된다. 예를 들어 반복문 안에서 대상 배열의 길이를 줄이면, 그 때의 길이를 반영하여 반복문의 조건에 반영되고 종료된다. 반복문 안에서 요소를 수정하면 수정한 게 반복 동안에 반영된다.
엥.
그러면
// 다른 순환 경로 탐색
// 각 GRID 좌표의 각 방위(directionGrid) 전부를 훑기
for (let row = 0; row < GRID_HEIGHT; row++) {
for (let col = 0; col < GRID_LENGTH; col++) {
for (let direction of Object.entries(directionGrid[row][col])) {
if (!direction[1]) { // 예를 들어 '상'방위가 false라면
// 새 순환 경로 추적 시작
const newLight = {
떠날때방향: direction[0],
현재좌표: { x: row, y: col }
}
trackCycle(newLight)
}
}
}
}
3번째 for 루프를 처음 들어갈 때 Object.entries(directionGrid[row][col]) 의 값은 { '상': false, '하': false, '좌': false, '우': false }로, 사방위가 모두 진행 가능한 상태(false)이게 된다. 첫 방위 ‘상’으로 출발한 trackCycle(newLight)은 모든 방위를 true로 만들게 된다. 그러면 3rd for 루프의 두 번째 부터는 if문 안으로 들어가면 안 되는데 실제로는 이 후로도 ‘하’, ‘좌’, ‘우’ 세 번을 더하여 네 방위 모두를 false로 인식하고 들어간다. 이게 무슨일이지? 분명 반복문 안에서 업데이트 되는 사항은 반영이 된다고 했는데…
결론
몇 가지 실험을 해보고 다음과 같은 결론을 얻었다:
- 업데이트 된 순회 대상을 for의 바디 ‘안에서’ 참조하면 업데이트가 반영된 모습으로 참조된다.
- 그러나 for문의 선언부에서 선언해 놓은 반복 변수를 참조하면 업데이트 전의 모습으로 참조된다.
예를 들면 for (let direction of arr1) 루프 선언부에서 선언한 반복 변수 direction은 루프를 도는 동안 arr1이 수정되어도 업데이트가 반영되지 않는다. 저 위의 코드에서 3중 for문 안의 direction은 처음 가져온 상태 그대로, 즉 trackCycle()
로 인해 directionGrid가 업데이트 되기 전에 가져온 것 그대로 남아있게 된다.
이것을 내가 원하는 대로 첫 루프나 중간 어느 루프에서든 업데이트 된 사항이 direction에 반영되게 만들려면… 결국 for문 선언부에서 direction이라고 박아두면 안된다. 반복문의 body 내에서 매번 direction에 해당하는 내용물을 새로 뽑아오게 만들어야 한다. 결국 for…of 로는 안되고 인덱스로 호출하는 식이어야 한다.
수정한 코드 (잘 동작함) :
// 다른 순환 경로 탐색 // 각 GRID 좌표의 각 방위(directionGrid) 전부를 훑기 for (let row = 0; row < GRID_HEIGHT; row++) { for (let col = 0; col < GRID_LENGTH; col++) { for (let i = 0; i < Object.entries(directionGrid[row][col]).length; i++) { let directions = Object.entries(directionGrid[row][col]); if (!directions[i][1]) { // 예를 들어 '상'방위가 false라면 // 새 순환 경로 추적 시작 const newLight = { 떠날때방향: directions[i][0], 현재좌표: { x: row, y: col } } trackCycle(newLight) } } } }
Uploaded by N2T