(프로그래머스 문제 해결함)
GRID를 만들고 좌표를 불러오는 과정에서 row, col로 나타내는 방식과 x, y 좌표로 나타내는 방식이 일정치 못한 데서 생긴 에러였다. GRID와 directionGrid를 호출하는 곳에서 [x][y]로 부르던 것을 [y][x]로 고치고, light에 들어가는 “현재 좌표”도 { x: col, y: row }로 고치니 테스트케이스가 다 통과됐다!
(일단) 정답 코드:
// 이상해지기 전 (함수로 추상화하기 전) 코드 (테스트케이스: 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]; const 사방위상태 = directionGrid[light.현재좌표.y][light.현재좌표.x]; 사방위상태[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 } 현재좌표: { x: col, y: row } } 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; }
이제 이것을 함수나 클래스로 분리하고, 뽑아낸 순환경로를 표시한다거나 하는 식으로 기능을 덧붙이고 싶다. 그렇지만 일단 어제 받은 조언을 반영해, 다음에 하도록 하자. 이미 과하게 시간을 썼고 할 일이 많으니.
배운 것:
타입스크립트의 열거형 Enums와 제네릭에 대하여.
(삼성 알고리즘 학습 & 풀이 사이트)
- 문제 풀이도 강의도 C와 C++, Java, Python 밖에 제공하지 않아서 아쉽다.
제네릭
: 선언 시점이 아닌 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법
제네릭 인터페이스
제네릭 인터페이스
예시:
interface MyInterfaceG<GenericValue> {
value: GenericValue;
}
const stringObjectG: MyInterfaceG<string> = { value: "hello, world!" };
const numberObjectG: MyInterfaceG<number> = { value: 1234 };
const stringArrayObjectG: MyInterfaceG<Array<string>> = { // MyInterface<string[]> 기능적으로는 동일
value: ["hello", "world!"],
};
내가 이해한 제네릭:
- const ~~로 변수를 생성하는 시점에 <T> 자리에 들어갈 타입이 정해진다!
- => 이후로 타입은 그것 하나로 고정됨
- 타입이 “정해진다(고정된다)”의 장점 : 전달받은 타입을 검사할 수 있다, 반환할 때도 명확한 타입을 명시해 반환할 수 있다. ↔ any도 어떤 타입이든지 넣을 수 있다는 공통점이 있지만 이쪽은 아예 타입 검사를 하지 않는다는 게 다름.
- 함수에 인수를 넣어 호출하는 것처럼 인터페이스나 타입 옆에도 제네릭타입을 넣어서 써준다고 기억하면 되겠다.
- 선언 시점의 코드를 보고 T가 정확히 어떤 타입인지 알 수 있다는 소소한 장점도 있음.
- 제네릭을 쓸 거라고 처음 interface에 소개해주는(?) 시점에 디폴트 타입(기초값)을 넣어줄 수 있다.
// 제네릭에 기초값 지정하기 예시 : interface MyInterfaceGG<GenericValue = string> { value: GenericValue; } const stringObjectGG: MyInterfaceGG = { value: "hello, world!" }; // ✅ const myRandomObjectGG: MyInterfaceGG = { value: true }; // ❌ Type 'boolean' is not assignable to type 'string'. const myRandomObjectGG2: MyInterfaceGG<boolean> = { value: true }; // 제네릭 타입을 <boolean>이라고 명시해 줘야 에러가 안 뜬다.
- 제네릭으로 선언 시 한 번 만든 interface 등을 수정하지 않고 여러 타입에 재활용하여 사용할 수 있다.
// 제네릭 없이 여러 타입이 들어올 수 있게 interface를 만들면: interface MyInterface { value: string | number | string[] // | number[] | boolean | ... } const stringObject: MyInterface = { value: "hello, world!" }; const numberObject: MyInterface = { value: 1234 };
⇒ 제네릭을 사용하지 않을 땐 interface 정의 자체에 타입을 계속 추가해줘야 해 번거롭다.
막간 - MyInterface<Array<string>>과 MyInterface<string[]>
제네릭 표기 Array<string>
와 배열 표기 string[]
는 기능적으로는 같다.
다만 제네릭과 readonly
속성은 같이 쓸 수 없다는 차이가 있다. readonly
를 사용하고 싶을 땐 배열 표기 string[]
와 함께 쓰든지, 유틸리티 함수 ReadonlyArray
를 이용해 제네릭 표기를 하든지 해야 한다.
// 예시:
const error: readonly Array<boolean> = [false];
const okay: readonly boolean[] = [true];
// 유틸리티 함수 ReadonlyArray를 이용해 제네릭 Array와 readonly를 같이 쓸 수 있다.
const okayGeneric: ReadonlyArray<boolean> = [false];
제네릭 함수
제네릭 함수
예시:
function getData<T>(data: T): T {
return data;
}
console.log(getData<string>("string data"));
⇒ getData(”string data”)라고만 해줘도 타입 추론이 되지만, 이 때는 “string data”라는, 내가 준 값이 캡쳐가 되어서 이 리터럴이 T의 타입으로 정해지게 된다.
console.log(getData("string data"));
Object.keys
enum Status {
Initiated = "Initiated",
Pending = "Pending",
Shipped = "Shipped",
Delivered = "Delivered",
}
interface Order {
buyer: string;
orderStatus: Status;
}
// Object.keys()
// ❌ Type 'string' is not assignable to type 'Status'.
const orders: Order[] = Object.keys(Status).map((status, index) => { // 이 status는 string 타입임.
return {
buyer: `buyer #${index}`,
orderStatus: status, // 여기서 에러.
};
});
Object.keys()
의 반환타입은 항상 string[]
이고, 여기서 다시 map을 걸면 (status, index)의 status는 바로 string 타입이 된다. 그래서 enum Status 타입만 넣을 수 있는 orderStatus 자리에 string인 status를 배정해줘서 안된다고 하는 것.
해결책:
// 1번 방식 - 비추
const orders: Order[] = Object.keys(Status).map((status, index) => {
return {
buyer: `buyer #${index}`,
orderStatus: status as Status,
};
});
// 2번 방식 - .keys() 대신 .values() 사용하기
const orders: Order[] = Object.values(Status).map((status, index) => {
return {
buyer: `buyer #${index}`,
orderStatus: status,
};
});
Object.keys()의 시그니처: keys(
o
: object): string[];
⇒ keys<T>(
o
: object): string[];
같이 되어 있지 않아서 에러가 뜸.
Object.values()의 시그니처: values<T>(
o
: { [
s
: string]: T } | ArrayLike<T>): T[];
⇒ Status가 T로 캡처되어 들어감. 리턴값이 Status[] 배열로 정해짐. values()의 인수로 넣어 줄 수 있는 것은 Status 타입을 값(들)으로 가지는 객체, Status[] 배열이다.
이쯤에서 알아보는…
열거형 Enums
열거형 Enums
: Enumerated Type.
: 기본적으로 ‘숫자’를 ‘열거했다’는 뜻이다.
: 어떤 ‘숫자’들에 특별한 의미를 담은 호칭을 붙여주고 싶다는 것이다.
: 어떤 ‘수’들을 특별한 상수로 지정하고 (문자열인)이름을 붙여주겠다는 것이다.
이것의 ‘정체’
Enum은 그 이름 자체로 타입이고, Enum.키
들도 각자 타입처럼 쓰일 수 있다.
기본 형식: enum 이름 { enum키1 = enum값1, enum키2 = enum값2, … }
‘enum 값’들에는 숫자나 문자열이 올 수 있다.
‘enum 값’들의 타입은 Enum명.키
가 되고, 이 안에 근본 타입인 number나 string이 들어 있다.
어떻게 만드는가
- 숫자형 Enum: ‘enum 값’들이 숫자인 Enum.
enum Color { Red, // = 0 Green, // = 1 Blue = 200, Purple, // 201 }
- 디폴트 ‘enum 값’은 0부터 시작하는 ‘숫자’임.
const yourColor = Color.Blue; console.log(yourColor) // 2
- 디폴트 값 말고 명시적 선언도 가능함.
- 리버스 매핑(Reverse mapping)이 가능함. (= ‘enum 값’으로 ‘키’를 참조하는 방법)
// 인덱싱으로 참조 console.log(Color[0]); // "Red" console.log(Color["0"]); // "Red" console.log(Color["Red"]); // 0 // '.속성' 방식으로 참조 console.log(Color.Red); // 0 // => 이 넷 외에는 모두 에러가 난다.
- 리버스 매핑이 가능한 이유: 숫자형 enum의 경우 각 키-값 쌍을 뒤집은 쌍도 자동으로 같이 넣어서 저장되기 때문
enum Color { Red = 300, Green, } console.log(Color) => 결과: { '300': 'Red', '301': 'Green', Red: 300, Green: 301 }
- 디폴트 ‘enum 값’은 0부터 시작하는 ‘숫자’임.
- 문자형 Enum
: ‘enum 값’이 문자열인 Enum.
: “특별한 ‘문자’들을 별명 지어 부르겠다.”
enum Color { Red = "Red", Green = "Green", BLUE = "BLUE", MyNickname = "Turtle" } console.log(Color.Red) // "Red" console.log(Color["Red"]) // "Red"
- 키와 값을 대문자로 쓸지는 개인 취향이다.
- 객체의 속성을 참조하는 것과 똑같이 .Red와 [’Red’] 방식이 모두 가능하다.
- 문자형 Enum의 경우에는 리버스 매핑을 위한 “리버스 변환”값이 저장되지 않는다.
- 숫자형 + 문자형 콤보 Enum
enum Color { Red = 300, Green, // = 301 Purple = "PURPLE", } console.log(Color) => { '300': 'Red', '301': 'Green', Red: 300, Green: 301, Purple: 'PURPLE' }
- 뒤집어진 숫자형은 당연히 문자형 enum 값이 된다.
- 여전히 숫자형 enum값 쌍만 리버스되고, 별다른 건 없다.
타입 타입 타입
enum Color {
Red = 300,
Green, // = 301
Purple = "PURPLE",
}
const myColor = Color.Red;
console.log(myColor) // 300
const yourColor = Color.Purple;
console.log(yourColor) // "PURPLE"
myColor의 근본 타입은 number이먀, 그를 감싸고 Color.Red가 타입으로 보여진다.
yourColor의 근본 타입은 string이며, 그를 타입 Color.Purple이 또 한 번 감싸고 있다.
- 왜 자꾸 null과 undefined를 막고 싶어 하는 걸까요
- Color.Red는 포장 타입? 그 근본 타입은 string이라구?
Uploaded by N2T