들어가기 전
Kotlin 언어에 더 익숙해지기 위해서 요즘 우아한 테크코스 5기의 미션들을 하나씩 풀어가고 있다.
그 중 온보딩 미션의 풀이과정과 공부하면서 배운 내용들을 정리하고자 이 포스팅을 쓰게되었다.
문제1
: pobi 와 crong 각각 왼쪽,오른쪽 페이지 중 + / * 값이 더 큰 값을 구하고 이를 pobi와 crong의 대표값으로 설정한다.
pobi와 crong 의 대표값 중 더 큰 값을 찾는 문제이다.
나의 풀이
fun getMaxNumber(page: Int): Int {
var add = page
var mul = page
var resultAdd = 0
var resultMul = 1
while (add != 0) {
resultAdd += (add % 10)
add /= 10
}
while (mul != 0) {
resultMul *= (mul % 10)
mul /= 10
}
return max(resultAdd, resultMul)
}
fun getPobiMaxNumber(pobi: List<Int>): Int {
return max(getMaxNumber(pobi[0]), getMaxNumber(pobi[1]))
}
fun getCrongMaxNumber(crong: List<Int>): Int {
return max(getMaxNumber(crong[0]), getMaxNumber(crong[1]))
}
fun checkException(who: List<Int>): Boolean {
return (who[1] - who[0] == 1)
}
fun solution1(pobi: List<Int>, crong: List<Int>): Int {
return if (checkException(pobi) && checkException(crong)) {
if (getPobiMaxNumber(pobi) > getCrongMaxNumber(crong)) 1
else if (getPobiMaxNumber(pobi) < getCrongMaxNumber(crong)) 2
else 0
} else {
-1
}
}
나의 풀이 코드는 위와 같다.
효율적이게 코드를 작성하고자 나름대로 노력을 하였는데 그 중 하나가 함수로 나누어서 코드를 작성하는 것이였다.
함수 하나에 기능 하나가 들어가도록 모든 기능들을 함수단위로 나누어서 구현하였다.
=> 지금 보니 add와 mul 부분도 함수로 각 1개씩 나누어서 코드를 작성하는게 더 좋을 것 같다. (리팩토링 해야지)
문제를 풀 때 발생할 수 있는 예외상황을 페이지가 연속되지 않는 경우만 생각해서 문제를 풀었다. 하지만 왼쪽의 페이지가 홀수, 오른쪽의 페이지가 짝수인 것도 예외 상황을 고려해서 문제를 풀어야 하나? 라는 의문이 생긴다. (왜냐하면 문제에 왼쪽은 홀수, 오른쪽은 짝수 라고 이미 적혀있어서 해당 예외는 고려하지 않아도 된다고 생각했다.) 리팩토링을 하면서 이 부분의 예외처리도 해봐야겠다.
코드를 작성할 시점에는 몰랐으나, 다른 공부를 하던 와중에 else 예약어를 사용하지 말자! 라는 글을 봤다.
else 보다는 if 문으로 작성해 early return이 되도록 하는 것을 지향하는 것이 좋다는 내용이였다.
그렇게 코드를 작성하면 가독성 측면에서도 현재 어떤 조건을 말하고 있는지 파악하기가 더 좋을 것 같다.
나의 코드에 해당 부분을 조금이나마 적용한다면
fun solution1(pobi: List<Int>, crong: List<Int>): Int {
return if (checkException(pobi) && checkException(crong)) {
if (getPobiMaxNumber(pobi) > getCrongMaxNumber(crong)) 1
if (getPobiMaxNumber(pobi) < getCrongMaxNumber(crong)) 2
else 0
} else {
-1
}
}
이런 방법으로 가능한걸까 싶다. 남은 else 부분을 if로 다 바꾸니 if에는 반드시 else가 필요하다고 오류가 뜬다. early return에 대해서 더 공부해봐야겠다.
이렇게 문제1을 풀어보았고, 난이도 자체는 높지 않았으나 더 효율적이고 가독성 좋은 코드를 작성하기 위해 많이 고민한 것 같다.
문제2
: 문자열의 중복을 제거한 문자열을 구하는 문제이다.
fun checkDuplication(cryptogram: String): String {
var idx = 0
var flag = 0
var msg = cryptogram
var startIdx = -1
while (idx < msg.length) {
if (idx + 1 < msg.length) {
if (msg[idx] == msg[idx + 1]) {
flag = 1
if (startIdx == -1) {
startIdx = idx
}
idx++
} else {
if (flag == 1) {
msg = msg.removeRange(startIdx, idx + 1)
idx = 0
startIdx = -1
flag = 0
} else {
idx++
}
}
} else if (flag == 1) {
msg = ""
} else {
break
}
}
return msg
}
fun solution2(cryptogram: String): String {
return checkDuplication(cryptogram)
}
문제를 풀기위한 아이디어를 구현하기에 급급했던 문제이다. 결론적으로 테스트케이스 및 정답은 맞췄지만, 리팩토링이 필요한 코드 중 하나이다. 좀 더 가독성 좋고 효율적으로 작성할 수 있는 방법이 반드시 존재할 것이다.
문제 풀이 아이디어는 다음과 같다.
1. 문자열을 인덱스 0에서 부터 쭉 훑으면서 현재 idx와 다음 idx의 문자열이 같은 부분이 있는지 확인한다.
2. 같은 부분이 있다면 해당 idx를 startIdx로 설정하고, 현재 중복된 부분이 존재한다는 flag = 1 로 설정한다.
없다면 idx ++ 하면서 계속 문자열을 훑는다.
3. 각 케이스 확인하기
- 3-1. flag=1인데 idx와 idx+1이 같지 않다면 startIdx에서부터 현재 idx 이전까지 중복이 있음을 판단하고 remove 한다.
remove 후 idx를 다시 0으로 setting 해 1번 과정을 반복한다.
- 3-2. flag =1 이고 idx와 idx+1 이 같고, startIdx 가 현재 idx보다 더 작다면 1번 과정을 반복하면서 3-1 케이스가 나타날 때까지 중복의 끝 idx를 찾는다.
- 3-3. flag = 0 이면 1번 과정을 계속 반복한다.
이 문제를 풀면서 kotlin 에서 string 일부를 삭제하는 removeRange 메서드를 알게되었다. removeRange(startIdx, endIdx)로 사용되고, endIdx는 삭제되는 idx + 1 을 넣어줘야 한다.
또한 매개변수인 cryptogram은 val 이므로 cryptogram = cryptogram.removeRange 가 적용되지 않음을 알게되었다.
따라서 따로 msg 변수를 만들어서 해당 코드를 작성하였다.
이 포스팅에 problem2 코드를 리팩토링한 버전도 곧 추가하겠다.
문제3
: 3,6,9 게임으로 입력된 숫자에서 3,6,9가 나왔을 때 박수를 친다. 이 때 박수의 카운트를 출력하는 문제이다.
fun countClapNum(number: Int): Int {
var calNum = number
var cnt =0
while (calNum != 0) {
if ((calNum % 10 == 3) || (calNum % 10 == 6) || (calNum % 10 == 9)) {
cnt ++
}
calNum/=10
}
return cnt
}
fun calculate369(number: Int): Int {
var cnt = 0
for (i: Int in 1..number) {
cnt += countClapNum(i)
}
return cnt
}
fun solution3(number: Int): Int {
return calculate369(number)
}
해당 문제는 로직을 떠올리는데는 큰 어려움이 없었다.
하지만 예외상황으로 33 과 같은 입력값은 2번의 박수를 쳐야한다는 걸 간과했다.
이 부분을 수정하고 문제 풀이를 완성할 수 있었다.
함수 네이밍을 조금 고민했는데, 처음에 각 숫자 박수 카운트를 체크 하는 함수를 calculate369, for 문이 존재하는 함수를 countClapNum이라 작성했다가 두 함수의 의미가 바뀐 느낌이라 네이밍을 교체했다.
아무래도 현재 countClapNum이 한 숫자에 대해서 나타날 수 있는 박수 count를 하는 의미가 더 가깝다고 생각했고,
calculate369 함수가 369 게임 자체를 진행하는 의미가 더 강하다고 생각했기 때문이다.
별거 아닌건가 싶지만 함수 네이밍 또한 중요하다는 생각에 여러 고민을 해보았다.
문제4
StringBuilder의 존재에 대해 알게되었다. replace 는 해당 문자열이 있는 모든 문자를 변경하고자 하는 문자로 변경하기 때문에 지금 문제에서는 적절하지 않은 method였다.
fun changeLowerCase(alphabet: Char): Char {
return (219 - (alphabet.code)).toChar()
}
fun changeUpperCase(alphabet: Char): Char {
return (155 - (alphabet.code)).toChar()
}
fun changeWord(word: String): String {
var resultWord = StringBuilder(word)
for (i: Int in word.indices) {
if (resultWord[i].isUpperCase()) {
resultWord[i] = changeUpperCase(resultWord[i])
} else if (resultWord[i].isLowerCase()) {
resultWord[i] = changeLowerCase(resultWord[i])
}
}
return resultWord.toString()
}
fun solution4(word: String): String {
return changeWord(word)
}
네, StringBuilder는 문자열 조작이나 수정이 필요한 상황에서 자주 사용되는 클래스 중 하나입니다. 이유는 문자열은 불변(immutable)하므로 문자열을 변경할 때마다 새로운 문자열 객체가 생성되는데, 이는 메모리를 낭비하게 되고 성능 저하를 일으킬 수 있기 때문입니다. StringBuilder는 이러한 성능 문제를 해결하기 위해 고안된 클래스로, 내부적으로 가변적인 버퍼를 사용하여 문자열을 조작하고 수정합니다. 이를 통해 문자열을 변경하더라도 새로운 객체를 생성하지 않고 기존의 버퍼를 활용함으로써 성능 개선을 이룰 수 있습니다. 따라서 반복적으로 문자열을 조작하거나 수정해야 할 경우에는 StringBuilder를 사용하는 것이 좋습니다. 예를 들어 문자열을 빌드하거나, 반복문 안에서 문자열을 수정하는 작업을 수행할 때 StringBuilder를 활용하면 성능 향상을 기대할 수 있습니다.
온보딩 문제를 풀면서 클린한 Kotlin 코드를 작성하기 위해 컨벤션 공부를 해야겠다
문제5
: 돈을 최대한 큰 화폐 단위로 바꾸는 문제로 각 화폐의 필요한 갯수를 리스트로 출력하는 문제이다.
fun calculateMoney(money: Int) : List<Int> {
var calculateMoney = money
var resultMoneyList= mutableListOf(0,0,0,0,0,0,0,0,0)
var modNumber = listOf(50000,10000,5000,1000,500,100,50,10,1)
for (i : Int in 0 ..8) {
val modResult = calculateMoney/modNumber[i]
resultMoneyList[i] = modResult
calculateMoney %= modNumber[i]
}
return resultMoneyList
}
fun solution5(money: Int): List<Int> {
return calculateMoney(money)
}
문제6
: 연속된 문자가 존재하는 닉네임의 메일을 출력하는 문제이다. (오름차순으로, 중복을 제거한 뒤)
fun searchName(name1: String, name2: String): Boolean {
for (i: Int in name1.indices) {
for (j: Int in name2.indices) {
if ((name1[i] == name2[j]) && name1.length - 1 >= i + 1 && name2.length - 1 >= j + 1) {
if (name1[i + 1] == name2[j + 1]) {
return true
}
}
}
}
return false
}
fun allSearchName(forms: List<List<String>>): List<String> {
var mailList: MutableList<String> = mutableListOf()
for (i: Int in forms.indices) {
for (j: Int in forms.indices) {
if (searchName(forms[i][1], forms[j][1]) && i != j) {
mailList.add(forms[i][0])
mailList.add(forms[j][0])
}
}
}
mailList.sort()
return mailList.distinct().toMutableList()
}
fun solution6(forms: List<List<String>>): List<String> {
return allSearchName(forms)
}
문제7
: 사용자의 아는 친구 or 방문자에 따라 점수를 더해 높은 점수의 추천친구를 출력하는 문제이다.
var memberList: MutableMap<String, MutableList<String>> = mutableMapOf()
var userFriendsList: MutableList<String> = mutableListOf()
var userKnownFriendsList: MutableMap<String, Int> = mutableMapOf()
fun solution7(
user: String,
friends: List<List<String>>,
visitors: List<String>
): List<String> {
createMemberList(friends)
searchUserFriend(user)
searchUserKnownFriend(user)
searchVisitorFriend(visitors)
return resultList(sortScore())
}
fun resultList (result:List<String>) : List<String> { //5명 넘어갈 시 최대 5명만 구하는 함수
if (result.size < 5) {
return result
}
else {
return result.subList(0,5)
}
}
fun sortScore (): List<String>{ //value에 따라 sort 하고 만약 value가 같으면 이름순 정렬하는 함수
val sortedMap = userKnownFriendsList.toSortedMap()
var map = sortedMap.toList()
map = map.sortedByDescending { it.second }
return map.toMap().keys.toList()
}
fun isUserFriend (friend:String) : Boolean { //이미 user에 friend인지 확인하는 함수
userFriendsList.forEach {
if (it == friend)
return true
}
return false
}
fun searchVisitorFriend(visitors: List<String>) { // 방문자 목록을 추천 가능한 친구들 목록에 점수 +1과 함께 추가하는 함수
var findFriendFlag=0
visitors.forEach { visitor ->
findFriendFlag=0
userKnownFriendsList.forEach { friend ->
if (friend.key == visitor) {
println("if문 $visitor and ${friend.value}")
userKnownFriendsList[friend.key] = userKnownFriendsList[friend.key]!!+1
findFriendFlag = 1
}
}
if (findFriendFlag == 0 && !isUserFriend(visitor)) {
println("else문 $visitor")
userKnownFriendsList.put(visitor , 1)
}
}
println(userKnownFriendsList.toString())
}
fun searchUserKnownFriend(user: String) { // 유저 친구의 친구들 즉 추천 가능한 친구들을 구하고, 점수를 +10 한다.
userFriendsList.forEach { userFriend ->
memberList.forEach { member ->
if (member.key == userFriend) {
member.value.forEach {
if (userKnownFriendsList.containsKey(it) && it != user) {
userKnownFriendsList[it]= userKnownFriendsList[it]!!+10
} else {
if (it != user) {
userKnownFriendsList.put(it, 10)
}
}
}
}
}
}
println(userKnownFriendsList.toString())
}
fun searchUserFriend(user: String) { // 유저와 친구인 사람의 목록을 구하는 함수
memberList.forEach { it ->
if (it.key == user) {
it.value.forEach {
userFriendsList.add(it)
}
}
}
println("user친구들 ${userFriendsList.toString()}")
}
fun createMemberList(friends: List<List<String>>) { //모든 각 멤버의 친구 목록을 구하는 함수
var flagFirst: Int
var flagSecond: Int
for (i in friends.indices) {
flagFirst = 0
flagSecond = 0
if (memberList.isEmpty()) {
memberList.put(friends[i][0], mutableListOf(friends[i][1]))
memberList.put(friends[i][1], mutableListOf(friends[i][0]))
println(memberList.toString())
} else {
memberList.forEach {
if (it.key == friends[i][0]) {
it.value.add(friends[i][1])
flagFirst = 1
}
if (it.key == friends[i][1]) {
it.value.add(friends[i][0])
flagSecond = 1
}
}
if (flagFirst == 0) {
memberList.put(friends[i][0], mutableListOf(friends[i][1]))
}
if (flagSecond == 0) {
memberList.put(friends[i][1], mutableListOf(friends[i][0]))
}
println(memberList.toString())
}
}
}
forEach 구문 적절히 활용하기, map에 대한 이해도를 높일 수 있는 문제였음.
'알고리즘 > 문제풀이 (C++,Kotlin)' 카테고리의 다른 글
[프로그래머스/Kotlin] Lv2 가장 큰 수 : 정렬 (0) | 2023.09.03 |
---|---|
[프로그래머스/Kotlin] Lv2 프로세스 : Queue (0) | 2023.08.31 |
[BOJ/C++] 10974번 모든 순열 (0) | 2023.03.15 |
[BOJ/C++] 2231번 분해합 (0) | 2023.03.14 |
[BOJ/C++] 1012번 유기농 배추 : BFS (0) | 2023.01.23 |