🔴 문제
링크 : https://school.programmers.co.kr/learn/courses/30/lessons/12915
문제 설명
문자열로 구성된 리스트 strings와, 정수 n이 주어졌을 때, 각 문자열의 인덱스 n번째 글자를 기준으로 오름차순 정렬하려 합니다. 예를 들어 strings가 ["sun", "bed", "car"]이고 n이 1이면 각 단어의 인덱스 1의 문자 "u", "e", "a"로 strings를 정렬합니다.
제한 조건
- strings는 길이 1 이상, 50이하인 배열입니다.
- strings의 원소는 소문자 알파벳으로 이루어져 있습니다.
- strings의 원소는 길이 1 이상, 100이하인 문자열입니다.
- 모든 strings의 원소의 길이는 n보다 큽니다.
- 인덱스 1의 문자가 같은 문자열이 여럿 일 경우, 사전순으로 앞선 문자열이 앞쪽에 위치합니다.
입출력 예입출력 예 설명
입출력 예 1
"sun", "bed", "car"의 1번째 인덱스 값은 각각 "u", "e", "a" 입니다. 이를 기준으로 strings를 정렬하면 ["car", "bed", "sun"] 입니다.
입출력 예 2
"abce"와 "abcd", "cdx"의 2번째 인덱스 값은 "c", "c", "x"입니다. 따라서 정렬 후에는 "cdx"가 가장 뒤에 위치합니다. "abce"와 "abcd"는 사전순으로 정렬하면 "abcd"가 우선하므로, 답은 ["abcd", "abce", "cdx"] 입니다.
🔵 풀이
문제를 보자마자 sorted()를 사용해야겠다는 생각이 들었다.
문제는 클로저 작성이었다!
아직 익숙지 않기에, 일단은 클로저를 간소화하지 않고 풀었다.
🔹 초기 코드 (실패)
string의 n번째 인덱스로 접근해야하므로,
String을 Array로 바꿀 수 있는 map 고차함수를 이용했다. (-> Array init(_:)을 사용해도 괜찮을 것 같다)
func solution(_ strings:[String], _ n:Int) -> [String] {
return strings.sorted(by: {(first: String, second: String) -> Bool in return first.map{$0}[n] < second.map{$0}[n]})
}
print(solution(["sun", "bed", "car"],1)) // ["car", "bed", "sun"]
print(solution(["abce", "abcd", "cdx"], 2)) // ["abce", "abcd", "cdx"]
그러나 결과를 보면 알겠지만,
solution(["abce", "abcd", "cdx"], 2)는 -> ["abcd", "abce", "cdx"]가 되어야 하는데,
"abcd"와 "abce"의 순서가 바뀌어 출력된다.
원인은 위 코드에서 인덱스 값이 같으면 정렬하지 않고 원본 순서대로 가져오기 때문이다.
그러나 문제에서 인덱스 값이 같을 때 사전순으로 정렬하라고 했으므로 코드 수정이 필요했다.
🔹 1번째 수정
if문을 활용해, n번째 인덱스 값이 같은 경우와 같지 않은 경우를 구분했다.
그리고 성공했다!
func solution1(_ strings:[String], _ n:Int) -> [String] {
return strings.sorted(by: {(first: String, second: String) -> Bool in
if first.map({$0})[n] != second.map({$0})[n] {
return first.map{$0}[n] < second.map{$0}[n]
} else { return first < second }
})
}
print(solution1(["sun", "bed", "car"],1)) // ["car", "bed", "sun"]
print(solution1(["abce", "abcd", "cdx"], 2)) // ["abcd", "abce", "cdx"]
🔹 2번째 수정 - 코드 리팩터링
1. sorted()의 클로저를 끝까지 축약하면 내부 상수( $0 키)를 사용해야 하는데 이미 map 에서 $0을 사용하고 있어 혼란스러울 것 같다.
map 대신 Array init(_:)을 사용해보자.
2. 지금 코드는 first와 second의 n번째 글자를 추출하여 글자가 다른지 판별한 후, 만약 다르면 또다시 n번째 글자를 추출하여 크기를 비교한 Bool 을 리턴한다. n번째 글자를 추출하는 로직이 반복되지 않도록 n번째 글자를 변수에 담아놓자.
3. if... else 문을 삼항연산자로 바꾸자
// 코드 리팩터링
func solution2(_ strings:[String], _ n:Int) -> [String] {
return strings.sorted(by: {(first: String, second: String) -> Bool in
let firstN: Character = Array(first)[n]
let secondN: Character = Array(second)[n]
return firstN != secondN ? firstN < secondN : first < second
})
}
🔹 3번째 수정 - 클로저 줄이기; Type Annotation 생략
// 클로저 축약 - Type Annotation 생략
func solution3(_ strings:[String], _ n:Int) -> [String] {
return strings.sorted(by: {(first, second) in // Type Annotation 생략
let firstN: Character = Array(first)[n]
let secondN: Character = Array(second)[n]
return firstN != secondN ? firstN < secondN: first < second
})
}
🔹 4번째 수정 - 클로저 줄이기; 매개변수 생략 -> 내부 상수($0, $1) 활용
first, second 매개변수를 생략하는 대신 내부 상수($0, $1)을 사용해 코드를 구현해보았다.
// 클로저 축약 - 매개변수, in 생략 -> 내부 상수($0, $1) 활용
func solution4(_ strings:[String], _ n:Int) -> [String] {
return strings.sorted(by: {
let firstN: Character = Array($0)[n]
let secondN: Character = Array($1)[n]
return firstN != secondN ? firstN < secondN: $0 < $1
})
}
고민되는 점은, firstN, secondN을 선언하는 부분이다...
위처럼 3줄로 작성하여 가독성과 계산 속도를 높일지, 또는 계산을 두 번 하더라도 코드를 한 줄로 줄일지 고민된다.
🔹 5번째 수정 - 한 줄로 코드 리팩터링 + 트레일링 클로저
만약 한 줄로 작성한다면 코드는 다음과 같이 줄일 수 있다.
// firstN, secondN 없애기
func solution5(_ strings:[String], _ n:Int) -> [String] {
return strings.sorted(by: {
return Array($0)[n] != Array($1)[n] ? Array($0)[n] < Array($1)[n] : $0 < $1
})
}
✔️ 그리고 이처럼 클로저가 한 줄일 땐 'return'을 생략할 수 있고,
✔️ 마지막 매개변수가 클로저 한 개일 땐 인자 레이블을 생략하고 꼬리처럼 붙일 수도 있다. (트레일링 클로저)
// return, 인자 레이블 생략, 꼬리붙이기
func solution6(_ strings:[String], _ n:Int) -> [String] {
return strings.sorted() { Array($0)[n] != Array($1)[n] ? Array($0)[n] < Array($1)[n] : $0 < $1 }
}