집합은 같은 타입의 서로 다른 값을 중복 없이 저장하고자 할 때 사용되는 집단 자료형이다. 집합은 배열과 매우 유사하지만, 배열을 사용하기에는 순서가 그다지 중요하지 않은 데이터들이거나 중복 없이 한 번만 저장되어야 하는 데이터들을 다룰 때 배열 대신 사용할 수 있는 자료형이다.
집합은 내부적으로 해시(Hash) 연산의 결과값을 이용하여 데이터를 저장하므로 집합에 저장할 데이터 타입은 해시 연산을 할 수 있는 타입이어야 한다. 다시 말해 집합에 저장할 데이터 타입은 반드시 해시값을 계산하는 방법을 제공해야 한다는 뜻이다. 해시 연산의 결과로 얻을 수 있는 해시값은 일련의 정수로 구성되며, 대상의 값이 같다면 해시 연산에 의해 결과값도 마찬가지로 일치하는 특성이 있다.
스위프트에서 제공하는 모든 기본 타입은 기본적으로 해시 연산을 할 수 있으므로 집합의 아이템으로 사용하기에 문제가 없다. 만약 스위프트에서 기본으로 제공하는 타입이 아니라 우리가 임의로 만든 타입을 사용하여 집합의 아이템으로 저장하려면, 스위프트 표준 라이브러리에서 제공하는 Hashable 프로토콜을 구현해야 한다. 프로토콜을 구현한다는 의미를 지금 이해하기는 어려우므로 단지 hashValue라고 불리는 해시값을 만들어 낼 수 있는 기능을 정의해야 한다는 정도로만 이해하고 넘어가도록 한다.
집합의 정의
집합을 정의할 때는 초기값을 사용하여 바로 정의하거나 빈 집합을 선언하고 초기화하는 과정을 거쳐 정의할 수 있다. 먼저 초기값을 사용해 집합을 정의하는 방법이다.
초기값을 사용하여 집합을 정의할 때, 값으로 사용되는 데이터는 배열과 동일하다. 하지만 단순히 배열 데이터를 사용하여 정의하면 컴파일러는 데이터들을 집합이 아닌 배열로 인식한다. 따라서 집합 타입이라는 것을 컴파일러에게 직접 알려주기 위해 타입 어노테이션 Set 을 기재해야 한다.
var genres: Set = ["Classic", "Rock", "Balad"]
위의 경우에는 초기값이 지정되었기 때문에 집합에 저장할 타입을 명시하지 않았지만, 만약 빈 배열을 사용하여 초기화할 경우에는 저장할 타입을 명시해야 한다.
var genres: Set<String> = []
위의 빈 배열로 초기화된 집합에 데이터를 추가하기 위해서는 insert를 사용할 수 있다.
genres.insert("Classic")
genres.insert("Rock")
genres.insert("Balad")
이 코드가 실행되면 집합 genres에는 세 개의 아이템이 추가되며, 집합의 크기는 3이된다. 집합의 크기는 count 변수를 통해 확인할 수 있다.
빈 집합인지를 확인할 때는 count가 0인지를 판단할 수도 있겠지만, isEmpty를 사용하여 명확하게 빈 집합인지 여부를 알 수 있다. 아래 코드는 확인하는 예시이다.
// 빈 집합인지 확인
if genres.isEmpty {
print("집합이 비어있습니다.")
} else {
print("집합에는 현재 \(genres.count)개의 아이템이 저장되어 있습니다.")
}
// 결과값
// 집합에는 현재 3개의 아이템이 저장되어 있습니다.
이 상태에서는 esle문의 코드가 실행되게 된다. 현재 genres.count는 3일 것이다. 이 상황에서 만약 아래 코드가 실행된다면 genres.count 의 값은 변하지 않는다. 그 이유는 이미 genres라는 집합에는 Balad라는 데이터가 들어있기 때문에 같은 데이터를 insert 했을 때는 집합 안으로 들어가지 않을것이다. 따라서 genres.count의 값은 여전히 3일 것이다.
genres.insert("Balad")
print(genres.count)
// 결과값
// 3
집합 순회 탐색
집합도 배열처럼 for ~ in 구문을 사용하여 순회 탐색이 가능하다. 배열처럼 인덱스를 활용할 순 없지만 순회 속성이 제공되므로 for ~ in 구문을 사용하면 된다.
var genres: Set = ["Classic", "Rock", "Balad"]
for i in genres {
print("\(i)")
}
// 실행 결과
// Balad
// Rock
// Classic
실행 결과를 보면 집합의 속성으로 순서가 보장되지 않는다.
그렇지만 sorted() 메소드를 사용되면 정렬된 결과를 받을 수 있다. 집합 자체의 순서를 결정하는 것은 아니고 단지 메소드의 반환값을 정렬하는 것이다.
for i in genres.sorted() {
print("(\i)")
}
// 결과값
// Balad
// Classic
// Rock
집합의 동적 추가와 삭제
집합에 아이템을 추가할 때는 isnert(_:) 구문을 사용하기도 한다. 이 메소드는 함께 전달된 인자값을 집합에 추가하지만, 이미 같은 아이템이 저장되어 있을 때는 아무 처리도 하지 않는다.
var genres: Set = ["Classic", "Rock", "Balad"]
genres.insert("Jazz")
// genres = {"Jazz", "Classic", "Rock", "Balad"}
genres.insert("Rock")
// genres = {"Jazz", "Classic", "Rock", "Balad"}
genres.insert("Rock")
// genres = {"Jazz", "Classic", "Rock", "Balad"}
집합의 아이템을 삭제할 때는 remove(_:) 메소드를 사용한다. 인자값에는 삭제하고자 하는 값이 사용된다. 메소드가 호출되면 집합 내부 저장소를 검색하여 입력된 인자와 일치하는 아이템을 찾고, 있으면 아이템을 삭제하고 삭제한 값을 반환한다. 그러나 삭제할 값이 없다면 nil을 반환한다. 메소드의 반환값을 활용하면 아이템의 삭제 결과를 손쉽게 판단할 수 있어 후속 처리에 편리하다.
if let removedItem = genres.remove("Rock") {
print("아이템 \(removedItem)의 삭제가 완료되었습니다.")
} else {
print("삭제할 값이 없습니다.")
}
// 결과값
// 아이템 Rock의 삭제가 완료되었습니다.
genres 집합에 Rock이 포함되어 있기 때문에 삭제 후 Rock을 반환하였다. 만약 집합에 없는 값을 삭제하려 했다면 nul이 반환되어 else 구문이 실행되었을 것이다.
스위프트는 집합의 아이템 전체를 삭제할 수 있는 메소드 removeAll()을 제공한다. 이 메소드는 인자값 없이 호출되어 모든 아이템을 일괄 삭제한다.
genres.removeAll() // 집합의 모든 아이템 삭제
if genres.isEmpty {
print("모든 아이템이 삭제되었습니다.")
} else {
print("아직 \(genres.count)개의 아이템이 남아있습니다.")
}
// 결과값
// 모든 아이템이 삭제되었습니다.
이외에도 스위프트는 집합에 특정 아이템이 있는지 확인할 수 있는 contains(_:) 메소드를 제공한다. 이 메소드는 인자값으로 입력된 데이터를 사용하여 해당 집합 내에서 일치하는 아이템이 있는지 검색하여 있으면 true를, 없으면 false를 반환한다.
var genres: Set = ["Classic", "Rock", "Balad"]
if genres.contains("Rock") {
print("Rock 아이템이 저장되어 있습니다.")
} else {
pirnt("Rock 아이템이 저장되어 있지 않습니다.")
}
// 결과값
// Rock 아이템이 저장되어 있습니다.
집합 연산
기본 집합 연산
집합 자료형은 우리가 수학에서 배웠던 집합의 개념과 거의 동일하다. 이 때문에 집합 자료형끼리는 집합 연산이 가능하다. 스위프트에서는 집합끼리의 연산을 쉽게 처리할 수 있도록 여러 메소드를 제공한다.
intersection(_:)
// 수학에서의 교집합을 의미한다.
// 양쪽 집합에서 공통되는 아이템만 선택하여 새로운 집합을 만들어주는 메소드이다.
symmetricDifference(_:)
// 교집합의 여집합이다. 양쪽 집합에서 공통되는 교집합을 제외하고
// 한쪽에만 포함된 아이템을 집합으로 만들어주는 메소드이다.
union(_:)
// 수학에서의 합집합이다.
// 양쪽 집합에 있는 아이템을 선택하여 새로운 집합을 만들어 주는 메소드이다.
subtract(_:)
// 수학에서의 차집합이다. 한쪽 집합에 있는 모든 아이템에서 다른 쪽 집합에도 속하는 공통 아이템을 제외하고
// 새로운 집합을 만들어주는 메소드이다. 주로 한쪽 집합에서 다른 쪽 집합을 빼는 연산으로 처리된다.
이 중 subtract(_:)를 제외하면 나머지 메소드는 양쪽 집합의 위치가 바뀌더라도 결과값은 동일하다. 즉 두 지합 중 하나를 객체로 사용하고 다른 하나를 인자값으로 사용하더라도 결과에는 차이가 없다는 것이다.
var odd: Set = [1, 3, 5, 7, 9] // 홀수 집합
var even: Set = [0, 2, 4, 6, 8] // 짝수 집합
let prime: Set = [2, 3, 5, 7] // 소수 집합
1. odd.intersection(even).sorted()
// []
2. odd.symmetricDifference(prime).sorted()
// [1, 2, 9]
3. odd.union(even).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
4. odd.subtract(prime)
odd.sorted()
// [1, 9]
1번에서 intersection(_:)은 교집합으로 홀수 집합과 짝수 집합간 공통된 아이템이 없으므로 빈 집합 즉 공집합이다.
2번에서 symmetricDifference(_:)는 교집합을 제외한 집합이므로 아이템 3, 5, 7이 제외되고 나머지 아이템을 반환한다.
3번에서 union(_:)은 합집합이다. 홀수 집합과 짝수 집합 간의 교집합이 없으므로 두 집합의 아이템을 합쳐서 반환한다.
4번에서 subtract(_:)은 차집합이다. 두 집합 사이의 교집합은 3, 5, 7이므로 결과값은 [1, 9]가 된다. 하지만 또 알아야 할 점이 있다. subtract(_:)는 위 3개의 함수와 달리 대상 집합의 내용을 직접 변경한다. 위의 3개 함수는 연산이 완료되고 난 이후 새로운 집합을 반환하고 원래의 집합을 변경하지 않는다. 하지만 subtract(_:)은 대상 집합을 직접 변경하여 odd에는 원래의 집합이 아니라 [1, 9]가 저장되어 있는 것이다. 이렇게 변경 가능성이 있기 때문에 odd는 상수 let 이 아니라 변수 var로 선언되어야 한다. 또한 subtract(_:)는 값을 반환하지 않기 때문에 뒤이어 바로 sorted()를 사용할 수 없다.
부분집합과 포함관계 판단 연산
집합 A의 모든 원소가 집합 B에도 있는 경우 집합 A는 집합 B의 부분집합이 된다. 프로그래밍의 관점으로 이야기하면 집합 A와 B의 아이템 모두가 일치할 때 A==B가 성립하며 이와 동시에 두 집합은 서로의 부분집합이 된다. 스위프트에서는 집합 자료형에 대해 부분집합 관계를 확인해주는 메소드를 제공한다.
isSubset(of:)
// 주어진 집합의 값 전체가 특정 집합에 포함되는지를 판단하여 true, false를 반환한다.
// 수학적으로 이야기하면 하나의 집합이 다른 집합의 부분집합인지 여부를 판단한다.
isSuperset(of:)
// 주어진 집합이 특정 집합의 모든 값을 포함하는지를 판단하여 true, false를 반환환다.
// isSubset(of:)와는 반대 상황을 판단하는 것으로
// 집합이 다른 집합의 상위집합 역할을 하는가에 대한 판단을 담당하고 메소드라고 할 수 있다.
isStrictSubset(of:)와 isStrictSuperset(of:)
// 조금 전 설명한 두 메소드처럼 주어진 집합이 특정 집합의 부분집합인지 아니면 상위집합인지를 판단하는 역할을 하지만
// 두 집합이 서로 같은 경우의 결과값이 다르게 반환된다. 두 집합이 서로 일치할 경우 수학적으로는 서로가 서로의
// 부분집합이자 상위집합이 될 수 있으므로 isSubset(of:), isSuperset(of:) 메소드가 true를 반환하는 반면,
// isStrictSubset(of:)와 isStrictSuperset(of:) 메소드는 이를 더 엄격하게 판단하여 정확히는 부분집합
// 또는 상위집합일 때만 true를 반환한다. 서로 일치하는 않는 집합은 동일한 집합으로 판단하고, 부분집합이나
// 상위집합으로 판단하지 않는다는 뜻이다.
isDisjoint(with:)
// 두 집합 사이의 공통 값을 확인하여 아무런 공통 값이 없을때 true, 공통 값이 하나라도 있으면 false를 반환한다.
아래 코드는 예제이다.
let A: Set = [1, 3, 5, 7, 9]
let B: Set = [3, 5]
let C: Set = [3, 5]
let D: Set = [2, 4, 6]
1. B.isSubset(of: A) //true
2. A.isSuperset(of: B) // true
3. C.isStrictSubset(of: A) // true
4. C.isStrictSubset(of: B) // false
5. A.isDisjoint(with: D) // true
- 집합 B의 아이템은 집합 A에 모두 속해있으므로 true를 반환한다.
- 집합 B가 A의 부분집합이라는 것은 A가 B의 상위집합이라는 것으로 true를 반환한다.
- A는 C의 부분집합이므로 true를 반환한다.
- B와 C는 동일한 집합으로 isStrictSubset(of: B)의 실행 결과는 false 이다.
- A와 D는 교집합이 공집합이므로 true를 반환한다.
다음으로는 배열에서 중복 아이템을 제거하는 방법이다.
var A = [4, 2, 5, 1 ,7, 4, 9, 11, 3, 5, 4] // 배열
let B = Set(A) // 집합
A = Array(B) // 중복이 제거된 배열
// [2, 4, 9, 5, 7, 3, 1, 11]
위 과정은 배열 객체를 집합 객체로 만들었다가 다시 배열 객체로 만드는 과정이다. 집합으로 만들면서 중복이 제거되고 그것을 다시 배열로 만드는 것이다. 하지만 아래의 코드를 사용하면 이 과정을 더 짧게 줄일 수 있다.
A = Array(Set(A))'swift' 카테고리의 다른 글
| 스위프트 딕셔너리 (1) | 2024.04.08 |
|---|---|
| 스위프트 튜플 (0) | 2024.04.08 |
| 스위프트 배열 (0) | 2024.04.06 |
| 스위프트 집단 자료형 (1) | 2024.04.06 |
| 스위프트 제어 전달문 (1) | 2024.04.06 |