| 해시 문제 목록 링크 / 문제 링크 / Github 링크 |
|---|
![]() |
import Foundation
func solution(_ genres:[String], _ plays:[Int]) -> [Int] {
return []
}
import Foundation
func solution(_ genres: [String], _ plays: [Int]) -> [Int] {
let songInfo = Dictionary(uniqueKeysWithValues: zip(genres.indices, zip(genres, plays)))
let songSorted = songInfo
.sorted { $0.key < $1.key }
.sorted { $0.value.1 > $1.value.1 }
.sorted { $0.value.0 > $1.value.0 }
var resultAlbum: [Int] = []
var genrePicked1: Set<String> = []
var genrePicked2: Set<String> = []
for (key, value) in songSorted {
let genre = value.0
if !genrePicked1.contains(genre) {
resultAlbum.append(key)
genrePicked1.insert(genre)
} else if !genrePicked2.contains(genre) {
resultAlbum.append(key)
genrePicked2.insert(genre)
} else { continue }
}
return resultAlbum
}
| 문제 발생 |
|---|
![]() |
주어진 조건을 모두 달성하는가?
1. 속한 노래가 많이 재생된 장르를 먼저 수록합니다. ❌
2. 장르 내에서 많이 재생된 노래를 먼저 수록합니다. ✅
3. 장르 내에서 재생 횟수가 같은 노래 중에서는 고유 번호가 낮은 노래를 먼저 수록합니다. ✅
4. 장르별로 노래를 두 개씩 모아 수록합니다. ✅
4'. 장르에 속한 곡이 하나라면, 하나의 곡만 선택합니다. ✅
let songInfo = Dictionary(uniqueKeysWithValues: zip(genres.indices, zip(genres, plays)))
let songSorted = songInfo
.sorted { $0.key < $1.key } // 고유 번호 정렬 (조건 3)
.sorted { $0.value.1 > $1.value.1 } // 재생수 내림차순 정렬 (조건 2)
.sorted { $0.value.0 > $1.value.0 } // 장르 정렬 (조건 1 미달)
var resultAlbum: [Int] = []
var genrePicked1: Set<String> = []
var genrePicked2: Set<String> = []
for (key, value) in songSorted {
let genre = value.0
if !genrePicked1.contains(genre) { // 장르 별 한 번 뽑기 (조건 4')
resultAlbum.append(key)
genrePicked1.insert(genre)
} else if !genrePicked2.contains(genre) { // 장르 별 두 번 뽑기 (조건 4)
resultAlbum.append(key)
genrePicked2.insert(genre)
} else { continue }
}
songInfo의 장르에 따른 정렬이 가능["pop": 0, "classic": 1]) // 빈 딕셔너리 생성
var totalPlaysByGenre: [String: Int] = [:]
// 장르별 재생수 취합
for info in songInfo.values {
let genre = info.0, plays = info.1
totalPlaysByGenre[genre, default: 0] += plays
}
// 취합한 것을 정렬 후 배열로 생성
let genreByPlays = totalPlaysByGenre
.sorted(by: { $0.value > $1.value })
.map({ $0.key })
// (Key: 장르, Value: 장르 순위)인 딕셔너리를 생성
let genreRank = Dictionary(uniqueKeysWithValues: zip(genreByPlays, genreByPlays.indices))
let songSorted = songInfo
.sorted { $0.key < $1.key }
.sorted { $0.value.1 > $1.value.1 }
.sorted { genreRank[$0.value.0, default: -1] < genreRank[$1.value.0, default: -1] } // 딕셔너리를 참고해 정렬 (조건 1)
| 문제 해결 |
|---|
![]() |
import Foundation
func solution(_ genres: [String], _ plays: [Int]) -> [Int] {
// 곡 정보 (Key: 곡 고유번호, Value: (장르, 재생 수))
var songInfo = Dictionary(uniqueKeysWithValues: zip(genres.indices, zip(genres, plays)))
// 장르별 재생수 취합
var totalPlaysByGenre: [String: Int] = [:]
for info in songInfo.values {
let genre = info.0, plays = info.1
totalPlaysByGenre[genre, default: 0] += plays
}
let genreByPlays = totalPlaysByGenre
.sorted(by: { $0.value > $1.value })
.map({ $0.key })
// 장르 순위 (Key: 장르, Value: 장르 순위)
let genreRank = Dictionary(uniqueKeysWithValues: zip(genreByPlays, genreByPlays.indices))
// 곡 순위
let songSorted = songInfo
.sorted { $0.key < $1.key } // 고유 번호 정렬
.sorted { $0.value.1 > $1.value.1 } // 재생 수 정렬
.sorted { genreRank[$0.value.0, default: -1] < genreRank[$1.value.0, default: -1] } // 장르 순위 정렬
// 베스트앨범 추출
var resultAlbum: [Int] = []
var genrePicked1: Set<String> = []
var genrePicked2: Set<String> = []
for (key, value) in songSorted {
let genre = value.0
if !genrePicked1.contains(genre) { // 해당 장르에서 뽑는 1번째 곡
resultAlbum.append(key)
genrePicked1.insert(genre)
} else if !genrePicked2.contains(genre) { // 해당 장르에서 뽑는 2번째 곡
resultAlbum.append(key)
genrePicked2.insert(genre)
} else { continue } // 해당 장르에서는 더 이상 뽑지 않음
}
return resultAlbum
}
songInfo가 꼭 필요한가? 필요하다면 왜 필요한가?딕셔너리 → 배열 → 딕셔너리로 구현되고 있다. 필요없는 과정을 줄일 수 없을까?for문에서 임시적으로 쓰이는 변수는 명시적으로 구분되게 할 수 없을까? (for문 안에서 처리하기 등)import Foundation
func solution(_ genres: [String], _ plays: [Int]) -> [Int] {
// 곡 정보 (Key: 곡 고유번호, Value: (장르, 재생 수))
var songInfo = Dictionary(uniqueKeysWithValues: zip(genres.indices, zip(genres, plays)))
// 장르별 재생수 취합 (Key: 장르, Value: 재생수)
var totalPlaysByGenre: [String: Int] = [:]
for info in songInfo.values {
let genre = info.0, plays = info.1
totalPlaysByGenre[genre, default: 0] += plays
}
// 장르 순위
let genreRank = totalPlaysByGenre.keys.sorted {
totalPlaysByGenre[$0]! > totalPlaysByGenre[$1]!
}
// 곡 순위
let songSorted = songInfo
.sorted { $0.key < $1.key } // 고유 번호 정렬
.sorted { $0.value.1 > $1.value.1 } // 재생 수 정렬
.sorted { genreRank.firstIndex(of: $0.value.0)! < genreRank.firstIndex(of: $1.value.0)! } // 장르 순위 정렬
// 베스트앨범 추출
var resultAlbum: [Int] = []
var genrePicked: [String:Int] = [:]
for (key, value) in songSorted {
let genre = value.0
if genrePicked[genre, default: 0] < 2 {
resultAlbum.append(key)
genrePicked[genre, default: 0] += 1
}
}
return resultAlbum
}
songInfo가 꼭 필요한가? 필요하다면 왜 필요한가?songInfo를 사용하면 고유번호를 기준으로 곡 정보를 쉽게 조회할 수 있다.딕셔너리 → 배열 → 딕셔너리로 구현되고 있다. 필요없는 과정을 줄일 수 없을까?for문에서 임시적으로 쓰이는 변수는 명시적으로 구분되게 할 수 없을까? (for문 안에서 처리하기 등)for문 안에 변수를 넣는 것은 구조적으로 어렵지만, for문 자체를 더 간략하게 줄일 순 있었다..sorted { genreRank[$0.value.0, default: -1] < genreRank[$1.value.0, default: -1] }
genreRank는 Dictionary이다.sorted 함수의 전체 시간복잡도는 O(n log n)이다.n은 songInfo의 곡 개수.sorted 자체의 O(n log n)가 그대로 유지된다..sorted { genreRank.firstIndex(of: $0.value.0)! < genreRank.firstIndex(of: $1.value.0)! }
genreRank는 배열이다.firstIndex(of:)를 호출하면 최악의 경우 배열의 모든 요소를 탐색해야 하므로 시간복잡도는 O(m)이다.m은 genreRank의 길이, 즉 고유한 장르의 수이다.sorted 함수의 전체 시간복잡도는 O(n log n) 호출당 비교 비용 O(m)이 추가되어 O(n m log n)이 된다.위의 비교를 미루어 보아
Case A가Case B보다 빠르다.
단,Case B에서는Case A의 장르 순위 정렬 부분이 간략화되어 고차함수의 사용이 줄었다.
이를 고려해도 그런지 확인해보자.
let genreByPlays = totalPlaysByGenre
.sorted(by: { $0.value > $1.value }) // O(m log m)
.map({ $0.key }) // O(m)
let genreRank = totalPlaysByGenre.keys.sorted {
totalPlaysByGenre[$0]! > totalPlaysByGenre[$1]! // O(1) 조회
} // O(m log m)
totalPlaysByGenre의 값을 비교한다..sorted { genreRank[$0.value.0, default: -1] < genreRank[$1.value.0, default: -1] }
genreRank는 Dictionary..sorted { genreRank.firstIndex(of: $0.value.0)! < genreRank.firstIndex(of: $1.value.0)! }
genreRank는 Array.firstIndex(of:)의 시간복잡도는 O(m) (최악의 경우).sorted의 시간복잡도는 O(n m log n).Case A:
Case B:
n)가 많아질수록 성능이 훨씬 더 좋다.firstIndex(of:)의 비효율로 인해 곡 순위 정렬에서 성능이 떨어진다.따라서, 곡 수(n)가 많을 경우, Case A가 더 유리하다.
배열이 아닌 딕셔너리 자료구조를 이용한다는 것은, 곧 Hash를 이용한다는 것이기에 조회가 빠르다.
그런데 Hash값 또한 메모리에 저장되는 값이니 그 부분에서 손해를 보고 있을 것이다.
두 케이스의 공간복잡도를 한 번 비교해보자.
n: 곡의 수.m: 고유한 장르의 수.genreRank가 Dictionary로 저장되고, Case B에서는 Array로 저장된다.공간복잡도와 메모리 효율은 상관 관계가 있지만 같은 개념은 아닌가보다.
두 개념의 차이에 대해서 짚어보자.
n)에 따라 표현한 것.n에 비례하여 임시 배열을 생성하면 O(n) 공간복잡도를 가진다.| 공간복잡도 | 메모리 효율 |
|---|---|
| 이론적인 메모리 사용량 증가율 | 실제 메모리 사용량과 효율성 |
| 입력 크기 증가에 따른 메모리 변화율 | 데이터 구조 설계에 따른 메모리 낭비 |
| 빅오 표기법으로 표현 | 구현에 따라 상수 배수가 달라질 수 있음 |
따라서 같은 공간복잡도를 가지더라도, 메모리 효율을 고려한 선택이 중요할 때가 많다.
Dictionary에 sorted() 메서드를 사용해서 문제를 풀었는데,
원래 Dictionary는 순서가 보장되지 않는 컬렉션이 아닌가?
Swift의 Dictionary는 본래 순서가 없는 컬렉션이다. 키와 값의 쌍은 내부적으로 해시 테이블을 기반으로 저장되어 특정 순서를 보장하지 않는다. 그런데도 딕셔너리를 정렬된 형태로 다룰 수 있는 이유는 딕셔너리를 시퀀스로 변환하거나 정렬 메서드를 활용할 수 있기 때문이다.
Dictionary를 Array로 변환Swift의 Dictionary는 기본적으로 순서를 보장하지 않지만, 딕셔너리의 키-값 쌍은 Array로 변환하여 시퀀스로 취급할 수 있다. Array는 순서가 있는 컬렉션이므로 정렬이 가능하다.
let dict = ["b": 2, "a": 1, "c": 3]
// 딕셔너리의 키-값 쌍을 배열로 변환한 후 정렬
let sortedByKey = dict.sorted { $0.key < $1.key }
let sortedByValue = dict.sorted { $0.value < $1.value }
print(sortedByKey) // [("a", 1), ("b", 2), ("c", 3)]
print(sortedByValue) // [("a", 1), ("b", 2), ("c", 3)]
sorted(by:) 메서드딕셔너리는 Sequence 프로토콜을 준수하기 때문에, sorted(by:) 메서드를 호출하면 정렬된 배열을 반환한다. 이 메서드는 정렬 기준을 클로저로 전달받아 사용자가 원하는 방식으로 키 또는 값을 기준으로 정렬할 수 있게 한다.
let sortedDict = dict.sorted { $0.key < $1.key }
// 반환 타입은 배열 [(Key, Value)]
딕셔너리를 정렬할 때, 원래의 딕셔너리 자체는 변하지 않고 정렬된 결과는 새로운 배열로 반환된다. 따라서 딕셔너리 자체가 정렬된 상태로 유지되는 것이 아니라, 필요할 때마다 정렬된 배열을 생성하여 사용하는 방식이다.
Swift의 Dictionary는 본래 순서가 없는 자료 구조지만, Array로 변환하거나 정렬 메서드를 사용하여 정렬된 결과를 배열 형태로 얻을 수 있다. 이는 Dictionary가 Sequence 프로토콜을 준수하기 때문이다.
이렇게 많은 걸 배울 수 잇는 문제엿다니