티스토리 뷰

코드카타

 

공원산책

 

문제

 

시작지점에서 주어진 방향으로 이동해 도착한 지점을 반환하는 문제이다.

 

 

입출력 예시

 

이동 중에 장애물이 있거나 공원을 빠져나왔다면 해당 이동명령은 실행하지 않는다.

 

 

테스트케이스 추가

입력값 > ["XXX", "XSX", "XXX"], ["E 2", "S 3", "W 1"]
출력값 > [1, 1]

시작위치의 Y 좌표값이 0이 아닐수도 있음을 고려해야한다. 

 

 

풀이

class Solution {
    fun solution(park: Array<String>, routes: Array<String>): List<Int> {
		
        // 시작 위치 찾기
        val sY = park.indexOfFirst { it.contains('S') } // 'S'가 있는 행 찾기
        val sX = park[sY].indexOf('S') // 'S'가 있는 열 찾기

        var res = mutableListOf<Int>(sY, sX) // 시작 위치 초기화
		
        // 장애물 위치 저장
        val obstacle = mutableListOf<List<Int>>()
        for (i in park.indices) {
            for (j in park[i].indices) {
                if (park[i][j] == 'X') {
                    obstacle.add(listOf(i, j))
                }
            }
        }
		
        // 주어진 방향으로 주어진 거리만큼 이동이 가능한지 검사하는 함수
        fun isMoveCan(pos: List<Int>, d: String, m: Int): Boolean {
            var y = pos[0]
            var x = pos[1]
			
            // 주어진 방향으로 1칸씩 이동하면서
            for (i in 1..m) {
                when (d) {
                    "N" -> y -= 1
                    "S" -> y += 1
                    "W" -> x -= 1
                    "E" -> x += 1
                }
                // 이동한 위치에 장애물이 있는지 확인
                if (listOf(y, x) in obstacle) return false
                // 공원 경계를 벗어나는지 확인
                if (y !in park.indices || x !in park[0].indices) return false
            }
            
            return true
        }
		
        // 루트 순회
        for (i in routes) {
            val d = i.split(" ")[0] // 이동방향
            val m = i.split(" ")[1].toInt() // 이동거리
			
            // d방향으로 m만큼 이동이 가능하다면 이동실행
            if (isMoveCan(res, d, m)) {
                when (d) {
                    "N" -> res[0] -= m
                    "S" -> res[0] += m
                    "W" -> res[1] -= m
                    "E" -> res[1] += m
                }
            }
        }

        return res // 최종 위치 반환
    }
}

모든 제한사항을 고려하여 코드를 짠다.

 

 

indexOfFirst

val park = listOf("SOO", "OOO", "OOO") 
val n = park.indexOfFirst { it.contains('S') } // 0

indexOfFirst 함수는 주어진 조건을 만족하는 첫 번째 요소의 인덱스를 반환한다.

 2차원 배열에서 특정값이 위치하는 열 인덱스를 찾는데 용이하게 쓸 수 있다.

 

indexOf 함수와 마찬가지로 값을 찾을 수 없다면 -1을 반환한다.

 


 

Kotlin 문법 강의 3주차 정리

 

메소드 설계

 

메소드란?

// Kotlin의 메소드(함수) 구조

fun 메소드이름(변수명:자료형, 변수명:자료형 ....) : 반환자료형 {
    소스코드 로직
}

코틀린에서 메소드를 지정할 땐 매개변수와 반환값의 자료형을 필수적으로 지정해줘야 한다.

로직에는 메소드의 용도에 맞는 소스코드가 들어가고 반환자료형과 일치하는 값을 return해줘야 한다.

 

 

메소드의 용도

메소드는 로직을 추상화하고 상황에 맞게 실행하는데에 쓰인다.

메소드를 사용하여 코드의 재사용성을 높일 수 있다.

 

 

 

클래스 설계

 

객체지향 프로래밍의 정의

코틀린은 모든 것이 클래스 형태이므로 객체화가 가능하다.

 

코틀린에선 프로그램에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 

객체를 만들고, 객체들간의 적절한 결합을 통해 유지보수를 쉽게 할 수 있도록 한다.

 

 

클래스란?

class 클래스이름 {
    정보1
    정보2

    행위1
    행위2
}

프로그램의 각 요소별 설계도라고 정의할 수 있다.

 

코틀린에선 class 키워드를 사용해 클래스를 만들고

클래스에는 정보(프로퍼티)와 행위(메소드)를 작성한다.

 

보통 하나의 파일은 한 개의 클래스를 의미하지만, 

하나의 파일안에 여러 개의 클래스가 존재할 수도 있다. 

 

 

클래스 종류

data class 클래스이름 {
    정보1
    정보2
}

데이터 클래스는 정보(프로퍼티)만 가지고 있는 클래스를 선언할 때 사용된다.

 

 

sealed class 부모클래스 {
	class 자식클래스1 : 부모클래스생성자
	class 자식클래스2 : 부모클래스생성자
}

실드 클래스는 상속받을 자식클래스를 정의할 수 있어 무분별한 상속을 방지한다.

 

 

object class 클래스이름 {
    정보1
    정보2

    행위1
    행위2
}

오브젝트 클래스는 프로그램을 실행하는 동시에 클래스를 인스턴스화 한다.

그렇기에 해당 클래스의 프로퍼티나 메소드를 바로 갖다쓸 수 있다.

 

 

enum class 클래스1 {
    JAVA, KOTLIN
}

enum class 클래스2(val code: Int) {
    JAVA(10), KOTLIN(20)
}

fun main() {
    println(클래스1.JAVA.toString()) // 출력: JAVA
    println(클래스2.KOTLIN.code) // 출력: 20
    println(클래스2.KOTLIN.name) // 출력: KOTLIN
}

열거 클래스는 상수를 정의하는 클래스이다.

열거 클래스를 사용하여 상수값에 대한 관리 지점을 줄일 수 있다.

 

 

 

생성자의 활용

 

생성자란?

생성자는 클래스의 인스턴스를 초기화하는 메서드이다.

생성자는 객체가 생성될 때 호출되며, 객체의 상태를 초기화하거나 필요한 초기 설정을 수행한다.

생성자는 특정한 반환 타입을 가지지 않으며, 클래스의 이름과 동일한 이름을 가진다.

 

 

기본 생성자

class Person(val name: String, var age: Int) {
    init {
        println("Person created: $name, $age")
    }
}

기본 생성자는 클래스의 헤더에 직접 선언된 생성자이다.

init 블록은 객체가 생성될 때 실행된다.

 

 

명시적 생성자

class Person {
    val name: String
    var age: Int

    // 명시적 생성자
    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
    
    // 추가적인 명시적 생성자
    constructor(name: String) {
        this.name = name
        this.age = 0 // 기본값으로 초기화
    }
}

명시적 생성자는 클래스 본문 내에 선언된 생성자로 constructor 키워드를 사용하여 정의한다.

기본 생성자와 다르게 모든 매개변수를 받지 않더라도 정의가 가능하다.

 

 

생성자 개념 정리

한 가지 형태로 클래스를 실체화 할 때는 기본 생성자를 활용하며,

여러 형태로 클래스를 실체화 할 떄는 명시적 생성자를 활용한다.

 

 

객체의 활용

 

객체란?

// 클래스 정의
class Person(val name: String, var age: Int) {
    // 메서드 정의
    fun greet() {
        println("안녕, 내 이름은 ${name}이고, 난 ${age}살이야.")
    }
}

모든 인스턴스를 포함하는 개념으로

클래스 타입으로 선언된 것들을 객체(Object)라고 한다.

 

 

인스턴스란?

fun main() {
    val person = Person("주영", 18) // 객체의 인스턴스화
    person.greet() // 객체의 메서드 호출
    
    // 실행결과 : "안녕, 내 이름은 주영이고, 난 18살이야."가 출력됨.
}

클래스는 일종의 틀이며, 이 틀을 기반으로 생성된 실제 데이터를 가리키는 것이 인스턴스이다.

여기서 "클래스"는 객체의 설계도이며, "인스턴스"는 실제로 사용할 수 있는 객체에 해당한다.

 

 

클래스 실체화 과정

클래스를 실체화 하여  프로그램에 로딩하면 해당 클래스의

프로퍼티와 메소드의 위치정보를 메모리에 로딩한다.

프로그램은 객체의 위치정보를 변수에 저장하고, 필요할 때 참조한다.

 

 

 

상속

 

상속이란?

// 부모 클래스
open class Vehicle {
    fun start() {
        println("Vehicle started.")
    }
}

// 자식 클래스
class Car : Vehicle() {
    fun drive() {
        println("Car is driving.")
    }
}

fun main() {
    val car = Car() // 자식 클래스의 인스턴스 생성
    
    // Car(자식 클래스)는 Vehicle(부모 클래스)를 상속하므로 
    // 자식 클래스에서 부모 클래스의 메서드를 호출하는 것이 가능하다.
    car.drive()
}

클래스 간의 상속이 이루어지면 자식 클래스는

부모 클래스의 프로퍼티와 메소드를 호출할 수 있다.

 

상속관계는 보통 부모/자식 클래스간의 공통적인 요소가 있을 때 만든다.

 

코틀린에서는 무분별한 상속으로 예상치 못한 흐름을 방지하고자

코틀린의 클래스는 생략된 final 키워드로 기본적으로 클래스간 상속이 막혀있다.

 

코틀린은 부모 클래스에 open 키워드를 붙여 상속을 허용할 수 있다.

 

 

상속의 필요성

클래스 간의 상속은 코드의 재사용성과 확장성을 높이는 데 큰 역할을 한다.

 

 

 

오버라이딩

 

오버라이딩이란?

// 부모 클래스
open class Animal {
    open fun makeSound() {
        println("동물이 소리를 내고 있습니다.")
    }
}

// 자식 클래스
class Dog : Animal() {
    // 부모 클래스의 메서드 오버라이딩
    override fun makeSound() {
        println("멍멍!")
    }
}

fun main() {
    val dog = Dog()
    dog.makeSound() // 출력: 멍멍!
}

자식클래스에서 상속받은 부모 클래스의 메서드를

재설계하는 것을 오버라이딩(Overriding)이라고 한다.

 

 

오버라이딩의 필요성

객체지향 프로그래밍은 클래스 간의 관계를 만들고, 관계의 일관성을 유지하는 목표를 가진다.

 

필요한 기능이 있을 때마다 메서드를 별도의 이름으로 만들게 된다면 일관성을 해치게 되며

이는 코드의 재사용성이 떨어지는 결과로 이어져 코드의 유지보수가 어려워진다.

 

 

 

오버로딩

 

오버로딩이란?

class MathOperations {
    fun add(a: Int, b: Int): Int {
        return a + b
    }
    
    fun add(a: Int, b: Int, c: Int): Int {
        return a + b + c
    }

    fun add(a: Double, b: Double): Double {
        return a + b
    }
}

fun main() {
    val math = MathOperations()

    val result1 = math.add(2, 3)
    println("두 정수의 합: $result1") // 출력: 5

    val result2 = math.add(2, 3, 4)
    println("세 정수의 합: $result2") // 출력: 9

    val result3 = math.add(2.5, 3.5)
    println("두 실수의 합: $result3") // 출력: 6.0
}

오버로딩(Overloading)은 같은 이름을 가진 메서드를 여러 개 정의하는 것을 말한다.

 

매개변수의 갯수나 자료형을 다르게하면 메서드의 오버로딩이 가능하며

메서드의 반환자료형은 오버로딩에 영향을 주지 않는다.

 

 

오버로딩의 필요성

오버로딩을 진행한 메서드들은 같은 기능을 수행하지만

다양한 매겨변수 형식으로 호출할 수 있어 사용자에게 편의성을 제공한다.

 

오버로딩은 코드의 가독성과 유지보수성을 높이는데 기여한다.

 

 

 

인터페이스

 

인터페이스 사용 이유

코틀린의 부모 클래스는 한 개이기 떄문에 모든 자식 클래스를 상속으로 처리하기 힘들다.

따라서 근본적인 공통점은 상속 받고, 추가적인 기능은 인터페이스로 구현할 수 있다.

 

 

인터페이스 구조

interface 인터페이스이름 {
    fun 메소드이름()
}

로직이 존재하지 않는 메소드를 추상 메소드라고 한다.

인터페이스의 내용은 추상 메소드로 구성된다.

 

 

인터페이스의 활용

// 인터페이스 정의
interface Shape {
    fun calculateArea(): Double
}

// 클래스의 메서드는 인테페이스를 오버라이딩하여 구성한다.
class Rectangle(val width: Double, val height: Double) : Shape {
    override fun calculateArea(): Double {
        return width * height
    }
}

fun main() {
    val rectangle = Rectangle(4.0, 6.0)

    // 오버라이딩한 인터페이스 메서드 호출해 도형 면적 계산 
    println("${rectangle.calculateArea()}") // 출력: 24
}

인터페이스 내에서 추상 메소드를 정의하고

구현할 클래스의 메소드는 인터페이스의 추상메소드를 오버라이딩 하여 구현한다.

 

 

 

학습소감

 

긴가민가했던 객체지향 프로그래밍의 개념들을 정리할 수 있어 유익한 시간이었다.

위 개념들을 적절히 사용하여 유지보수가 편리하도록 클래스를 설계해보는 연습을 해야겠다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함