해시 문제 목록 링크 / 문제 링크 / 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
프로토콜을 준수하기 때문이다.
이렇게 많은 걸 배울 수 잇는 문제엿다니