0. 오늘은 무엇을 했는가
오늘은 평범한 나날들 중 하루였습니다.
8시 50분에 출석하고, 코드 카타를 풀었습니다.
그리고 오전 스크럼을 진행하고, 컬렉션 공부를 마저 했습니다.
오후에는 개발종합반 코틀린 강의 5주차를 듣고,
코드카타 21번 ~ 30번까지 하고 나서
남는 시간 동안 개인 과제를 조금 진행했습니다.
조금은 한가로웠던 날이었습니다.
1. 코드 카타
[ 09:00 ~ 10:00 ]
오늘 코드 카타는 쉬운 편이었습니다.
특별한 어떤 알고리즘이 존재하기보다도, 단순히 계산식만 생각해내면 되는 문제였습니다.
△△ 게임대회가 개최되었습니다. 이 대회는 N명이 참가하고, 토너먼트 형식으로 진행됩니다. N명의 참가자는 각각 1부터 N번을 차례대로 배정받습니다. 그리고, 1번↔2번, 3번↔4번, ... , N-1번↔N번의 참가자끼리 게임을 진행합니다. 각 게임에서 이긴 사람은 다음 라운드에 진출할 수 있습니다. 이때, 다음 라운드에 진출할 참가자의 번호는 다시 1번부터 N/2번을 차례대로 배정받습니다. 만약 1번↔2번 끼리 겨루는 게임에서 2번이 승리했다면 다음 라운드에서 1번을 부여받고, 3번↔4번에서 겨루는 게임에서 3번이 승리했다면 다음 라운드에서 2번을 부여받게 됩니다. 게임은 최종 한 명이 남을 때까지 진행됩니다.
이때, 처음 라운드에서 A번을 가진 참가자는 경쟁자로 생각하는 B번 참가자와 몇 번째 라운드에서 만나는지 궁금해졌습니다. 게임 참가자 수 N, 참가자 번호 A, 경쟁자 번호 B가 함수 solution의 매개변수로 주어질 때, 처음 라운드에서 A번을 가진 참가자는 경쟁자로 생각하는 B번 참가자와 몇 번째 라운드에서 만나는지 return 하는 solution 함수를 완성해 주세요. 단, A번 참가자와 B번 참가자는 서로 붙게 되기 전까지 항상 이긴다고 가정합니다.
제한사항
N : 21 이상 220 이하인 자연수 (2의 지수 승으로 주어지므로 부전승은 발생하지 않습니다.)
A, B : N 이하인 자연수 (단, A ≠ B 입니다.)
내용이 너무 길어서 숨이 막힐 것 같지만 사실 핵심 내용만 보면 됩니다.
결국 핵심 내용은 2의 지수로 이루어진 사람들이 참가하는 토너먼트에서 a, b번째 참가자가 무조건 이긴다고 한다면, 언제 만나는가? 입니다.
그래서 저는 주어진 숫자 a, b에 대해서 +1을 하고, 2로 나누는 방식을 계속해서 두 값이 같아질 때까지 셌습니다.
다음은 제가 작성한 코드입니다.
class Solution {
fun solution(n: Int, a: Int, b: Int): Int {
var answer = 0
var nextA: Int = a
var nextB: Int = b
while(nextA != nextB) {
nextA = (nextA + 1) / 2
nextB = (nextB + 1) / 2
answer ++
}
return answer
}
}
빨리 풀고 나서 30분 정도 남아서 잠시 다른 사람의 풀이를 확인했습니다.
그런데...

다음과 같은 코드가 추천 수가 제일 많은 게 아니겠습니까?
class Solution {
fun solution(n: Int, a: Int, b: Int) = ((a - 1) xor (b - 1)).toString(2).length
}
아니 이게 된다고?
제가 먼저 든 생각이었습니다. 그리고 든 생각은 어떻게 이렇게 할 생각을 했지? 였습니다.
궁금한 저는 남은 코드 카타 시간동안 저 코드를 하나씩 살펴봤습니다.
먼저, 앞에서부터 코드를 해석했습니다. 받은 a와 b를 -1을 해서 1~n까지 있는 걸 0~(n-1)로 바꿨습니다.
그리고 xor로 2진법을 활용해서 두 수의 xor(배타적 논리합) 연산을 진행했습니다.
xor 연산은 2진법으로 바꾼 후 서로 비교해서 같은 부분에는 0, 다른 부분에는 1을 작성하는 연산입니다.
그렇게 나온 값은 toString(2) 함수로 값이 바뀌는데요.
여기서 toString(2)를 적으면 2진법으로 쓸 수 있다는 걸 처음 알았습니다.
2가 아닌 다른 숫자를 적으면 그 숫자의 진법으로 쓸 수 있다고 합니다.
그렇게 2진수가 된 값의 length를 구합니다.
저는 이 부분이 처음에 이해가 안 됐습니다. 왜 길이를 구하지?
설명하기 위해, 입출력 예시 값을 넣어보겠습니다.
(4-1) xor (7-1) 는 0011 xor 0110 이므로, 5(0101)가 됩니다.
그리고 toString(2)로 이진수인 0101가 됩니다. 여기서 length를 구하면, 3이 나옵니다.
이런 풀이 과정인 걸 깨닫고, 저는 다시 한번 놀랐습니다. 진짜 어떻게 이렇게 할 생각을 했지?
이게 정답이 되는 이유는 대진표에서 상대로 만나는 사람은 무조건 (0,1), (2,3) 처럼 옆에 붙어있고,
이걸 2진수로 표현하면 (0000, 0001), (0010, 0011)이 됩니다.
즉, 가장 앞의 1은 항상 같은 곳에 있다는 뜻입니다.
그러므로, xor 연산을 하게 되면 바로 옆에 있는 사람과는 값이 다른 건 맨 마지막 자리고,
만약 다른 쪽과 비교하게 되면 이게 1자리씩 점점 멀어지게 됩니다.
그래서 위같은 코드가 작동되는 겁니다...
이건 정말 생각도 못한 참신하고, 특별하고, 충격적이고, 똑똑한 발상이었습니다.
2. Kotlin 컬렉션 - 2
[ 11:00 ~ 12:00 ]
오늘도 컬렉션에 대해 공부했습니다.
오늘은 Array, ArrayList, Pair에 대해 공부했습니다.
먼저, Array는 배열로, 고정 크기를 갖고 있습니다.
val arr: Array<타입> = arrayOf(값)
다른 컬렉션들처럼 get(), size, set() 등의 확장 함수와 프로퍼티를 갖고 있었습니다.
그리고, 다음과 같이도 초기화할 수 있습니다.
val arr = Array(n) { i -> i * 3 }
이 방식은 List에서도 똑같이 사용 가능합니다.
다음으로, ArrayList는 동적 배열로, 요소에 접근하는 속도는 빠르지만, 추가 및 삭제 과정은 느립니다.
val arrl: ArrayList<타입> = arrayListOf(값)
ArrayList는 MutableList를 구현하기 때문에 add(), remove(), get(), set() 등과 같은 함수를 쓸 수 있습니다.
여기서 Array, ArrayList, List를 비교해보겠습니다.
Array는 고정 크기의 배열입니다.
반면, List는 읽기 전용 컬렉션이며, ArrayList는 MutableList를 구현하는 동적 크기의 배열입니다.
그 다음으로, Pair에 대해 공부했습니다.
Pair는 포커를 하면 나오는 원페어, 투페어를 생각하면 됩니다.

두 값이 같지는 않지만, 위처럼 쌍으로 이루어진 걸 Pair로 표현합니다.
val pair = Pair(값1, 값2)
그리고, 첫 값과 두 번째 값들을 가져올 수도 있습니다.
// 첫 번째 값
pair.first
pair.component1()
// 두 번째 값
pair.second
pair.component2()
또, data class이기 때문에, 구조 분해 선언과 toString()같은 함수도 쓸 수 있습니다.
val (a, b) = Pair(1, "안녕")
pair.toString()
아무래도 컬렉션의 기초적인 건 이정도면 될 것 같으니 내일은 다른 내용을 다뤄보겠습니다.
3. 코틀린 5주차 강의
[ 13:00 ~ 15:00 ]
오늘은 예상대로 가장 어려웠던 강의였습니다.
가장 먼저 배운 건 자료형 변환이었습니다.
숫자.to자료형()
Integer.parseInt(변수)
문자열.toDouble()
위처럼 자료형을 변환하는 함수들이 존재합니다.
그리고, 객체끼리도 변환이 가능합니다. 이 경우는 상속 관계에서만 가능합니다.
자식 객체가 부모 객체가 되는 걸 업 캐스팅, 반대의 상황을 다운 캐스팅이라고 합니다.
강의에서는 업캐스팅 위주로 다뤘기 때문에 저도 우선 이 부분만 복습하고, 다운캐스팅은 이후에 다뤄보겠습니다.
업캐스팅은 자식 객체를 as 부모 객체로 부모 객체처럼 다루는 것입니다.
자식객체 as 부모객체
is 키워드를 쓰면 자료형을 확인할 수 있습니다.
val str: String = ""
str is String
그리고 배운 부분이 바로, Scope Function이었습니다.
제가 이전에 혼자서 공부하면서 어려웠던 부분이 총 3군데 있었습니다.
첫 번째가 람다였고, 두 번째가 Scope, 세 번째가 코루틴이었습니다.
물론, 다른 부분도 있었지만, 위 3개는 어려운데 자주 쓰여서 더 고민이 컸습니다.
오늘은 가볍게 강의 내용만 복습하고, 아마 내일 다루지 않을까 싶습니다.
먼저, 변수.let {} 은 { } 안에 있는 it 값을 바꾸고 결과를 반환합니다.
with(변수) {}는 { } 안에 있는 변수 값을 this로 표현하고, this를 생략할 수 있습니다.
대신, with에 들어가는 변수는 null이 아닐 때만 쓸 수 있습니다.
.also { }는 it으로 객체를 전달하고, 객체를 반환합니다.
.apply { }는 this로 객체를 전달하고, 객체를 반환합니다.
.run { }은 with와 같은 행동을 하지만, 확장함수라는 점이 다릅니다.
run은 확장 함수이기 때문에, ?. 안전 호출연산자를 쓸 수 있어, run을 보통 많이 씁니다.
즉, run, with, let은 블록 수행 결과를 반환하고, apply, also는 객체 자신을 반환하며,
run, with, apply는 this를 쓰고, let, also는 it을 씁니다.
총 5개의 함수가 있고, 이게 확실히 공부를 안 하면 헷갈릴 것 같습니다.
다음은 제가 아직까지도 어려워하는 쓰레드와 코틀린입니다.
이론적인 부분은 어느 정도 이해하고 있지만, 사용할 때 약간 헷갈리네요.
우선, 순서대로 하나씩 진행하는 걸 동기적 프로그래밍이라고 합니다.
이 반대는 비동기적 프로그래밍이라고 합니다. 여러 개를 같이 진행하는 걸 뜻합니다.
쓰레드는 로직을 동시에 실행할 수 있습니다. 기본적으로 메인 쓰레드가 존재하고, 여기서 메인을 실행합니다.
그리고 그 자식 쓰레드들에서 나머지 일들을 처리합니다. 쓰레드는 다음과 같이 쓸 수 있습니다.
thread(start = true) {
// 쓰레드로 처리할 내용
}
쓰레드와 코루틴을 코틀린에서 쓸 땐 종속성을 추가해줘야 합니다.
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0-RC")
코루틴은 쓰레드보다 쉽게 비동기적 프로그래밍을 할 수 있는 기능입니다.
코루틴은 빌더를 사용하는데,
코루틴스코프.launch { }
코루틴스코프.async { }
launch는 실행 결과값이 없을 때, async는 결과값이 있을 때 사용하며, Deffered 타입을 리턴합니다.
그리고 코루틴 스코프는 다음과 같이 있습니다.
GlobalScope(Dispatchers)
CoroutineScope(Dispatchers)
GlobalScope는 앱 실행 이후 계속 수행돼야 할 때 사용하고, CoroutineScope는 필요할 때만 생성합니다.
CoroutineScope는 사용 후 정리를 해줘야 합니다.
Dispatchers에는 다음과 같은 값이 있습니다.
Dispatchers.Main
Dispatchers.IO
Dispatchers.Default
Main은 메인 쓰레드를 뜻하며, IO는 입출력 작업에 최적화된 쓰레드, Default는 CPU에 최적화된 쓰레드입니다.
코루틴과 쓰레드에 대한 내용은 내일 다시 복습하면서 자세하게 다뤄보겠습니다.
4. 알고리즘 기억 노트(31~40번)
[ 15:00 ~ 18:00 ]
오늘 풀었던 알고리즘들은 어제보다 조금 어려웠습니다.
사실 제가 시간복잡도나 코드 양을 생각하면서 여러 함수를 적용시켜보려고 하고,
추가적으로 다른 분들의 풀이도 보면서 공부하다 보니 조금 더 시간이 걸렸습니다.
여기서는 앞서 공부했던 Array (n) { } 초기화 방법을 사용해 다시 풀었습니다. 많이 유용한 방법인 것 같습니다.
처음에는 다른 방법으로 했다가, zip 함수로 푸는 걸 보았습니다.
a.zip(b)를 하면, a와 b 컬렉션을 Pair로 만드는 방법을 썼습니다. zip 함수를 기억해둬야겠습니다.
저는 sorted()를 Array에 썼을 때 없다고 해서, 아예 없는 줄 알았는데 형태가 달랐습니다.
Array.sortedArray()로 정렬한 배열을 반환받을 수 있습니다.
숫자로만 구성돼 있는 걸 찾기 위해 isDisit()을 썼는데, toIntOrNull()을 쓰면, 자동으로 걸러지는 걸 깨달았습니다.
이것도 Array 초기화 방법을 이용하긴 했는데, 행과 열로 이루어져 있어서, 이중으로 사용했습니다.
이중으로 되는 건 처음 알았습니다.
이건 알고리즘보다 문제에서 제공해준 코드에 주목했습니다.
readLine()!!.split(' ').map(String::toInt)
다른 부분보다 map 함수를 통해 List를 만들 때, String::toInt로 String 클래스의 toInt 함수를 참조한다는 점에 주목했습니다.
toInt()를 참조했기에, 각 요소를 Int로 바꿔줍니다.
참조에 쓰는 ::를 많이 접하지 못해 익숙하지 못했습니다.
StringBuilder의 .reverse()는 알고 있었는데, String의 .reversed()는 몰랐습니다.
5. 끝
이후로는 개인 과제를 조금 만지작 거리다가 끝났기에, 더 이상 쓸 내용이 없는 것 같습니다...
오늘 공부했던 내용들은 내일 다시 다루겠습니다.
그리고 오늘 너무 더웠습니다... 집중하기 어려워지는 것 같습니다.
끝.