멀티셋(또는 가방)은 일반 집합과 유사한 데이터 구조이지만, 동일한 요소를 여러 번 저장할 수 있다.
예를 들어, 일반 집합에 요소 1, 2, 2를 추가하면 집합은 두 개의 항목만 포함한다. 두 번째로 2를 추가해도 아무런 영향이 없다.
var set = Set<Int>()
set.add(1) // set은 이제 [1]
set.add(2) // set은 이제 [1, 2]
set.add(2) // set은 여전히 [1, 2]
반면, 멀티셋에 요소 1, 2, 2를 추가하면 세 개의 항목을 포함한다.
var set = Multiset<Int>()
set.add(1) // set은 이제 [1]
set.add(2) // set은 이제 [1, 2]
set.add(2) // set은 이제 [1, 2, 2]
이것이 배열과 매우 비슷해 보일 수 있다. 그렇다면 왜 멀티셋을 사용할까? 두 가지의 차이점을 살펴보자.
멀티셋의 일반적인 연산은 다음과 같다.
멀티셋의 실제 사용 사례로는 한 문자열이 다른 문자열의 부분 아나그램인지 확인하는 것이 있다. 예를 들어, "cacti"는 "tactical"의 부분 아나그램이다. (즉, "tactical"의 글자를 재배열하여 "cacti"를 만들 수 있고, 몇 글자가 남는다.)
var cacti = Multiset<Character>("cacti")
var tactical = Multiset<Character>("tactical")
cacti.isSubSet(of: tactical) // true!
이 Multiset 구현체는 내부적으로 딕셔너리를 사용해 각 엘리먼트와 그 추가 횟수를 매핑한다.
핵심 코드는 다음과 같다:
public struct Multiset<Element: Hashable> {
private var storage: [Element: UInt] = [:]
public init() {}
이 클래스를 사용해 문자열 멀티셋을 생성하는 방법은 다음과 같다:
var set = Multiset<String>()
엘리먼트를 추가할 때는 해당 엘리먼트의 카운터를 증가시키거나, 존재하지 않으면 1로 설정한다:
public mutating func add (_ elem: Element) {
storage[elem, default: 0] += 1
}
이 메서드를 사용해 앞서 생성한 셋에 엘리먼트를 추가하는 방법은 다음과 같다:
set.add("foo")
set.add("foo")
set.allItems // ["foo", "foo"] 반환
이제 셋에는 두 개의 엘리먼트가 존재하며, 둘 다 문자열 "foo"다.
엘리먼트를 제거하는 방법은 추가와 유사하다. 엘리먼트의 카운터를 감소시키거나, 제거 전 값이 1이면 내부 딕셔너리에서 제거한다.
public mutating func remove (_ elem: Element) {
if let currentCount = storage[elem] {
if currentCount > 1 {
storage[elem] = currentCount - 1
} else {
storage.removeValue(forKey: elem)
}
}
}
특정 아이템의 개수를 가져오는 방법은 간단하다. 내부 딕셔너리에서 해당 아이템의 값을 반환한다.
public func count(for key: Element) -> UInt {
return storage[key] ?? 0
}
Swift Algorithm Club의 Simon Whitaker가 작성