집합 (Set)
프로그래밍에서 집합(Set)은 중복되지 않는 고유한 요소들의 모음으로, 수학적 집합 개념을 기반으로 한다. 집합은 요소의 순서가 없으며, 동일한 요소가 두 번 이상 포함될 수 없다. 집합은 데이터 중복 제거, 멤버십 테스트, 교집합, 합집합 등의 연산에서 사용된다. 본 포스팅은 집합의 기본 개념부터 자바에서의 구현, 활용 알고리즘, 심화 주제까지 다룬다.
집합의 정의와 특징
집합은 고유한 요소들로 구성된 자료구조로, 각 요소는 단 한 번만 나타난다. 집합은 중복 제거, 멤버십 테스트, 집합 연산을 효율적으로 수행한다. 집합의 주요 특징은 다음과 같다:
- 중복 없음: 동일한 요소는 한 번만 저장된다.
- 순서 없음: 요소 간 순서가 정의되지 않는다.
- 빠른 조회: 요소 존재 여부를 O(1) 또는 O(log n) 시간에 확인한다.
집합의 장점은 다음과 같다:
- 효율적인 중복 제거: 중복 데이터를 자동으로 처리한다.
- 빠른 멤버십 테스트: 요소 포함 여부를 신속히 확인한다.
- 집합 연산 지원: 교집합, 합집합, 차집합 등을 수행한다.
단점은 다음과 같다:
- 순서 정보 부재: 인덱스나 순서를 통해 요소에 접근할 수 없다.
- 메모리 사용: 해시 기반 집합은 메모리를 더 사용할 수 있다.
- 복잡한 구현: 트리 기반 집합은 구현이 복잡할 수 있다.
집합의 종류
집합은 구현 방식에 따라 여러 형태로 나뉜다. 아래는 주요 구현 방식이다.
1. 해시 기반 집합(Hash-based Set)
해시 테이블을 사용해 요소를 저장한다. 평균적으로 O(1) 시간에 삽입, 삭제, 조회를 수행한다.
[Hash Table] [0] → [Element1] [1] → [Element2] → [Element3] ...
2. 트리 기반 집합(Tree-based Set)
이진 검색 트리(예: 레드-블랙 트리)를 사용해 요소를 정렬된 상태로 유지한다. O(log n) 시간에 연산을 수행하며, 정렬된 순회 가능.
[Element2] / \ [Element1] [Element3]
3. 비트 집합(Bit Set)
비트 벡터를 사용해 정수 집합을 효율적으로 표현한다. 메모리 효율이 높다.
[Bit Vector] [0 1 1 0 1 ...] ← 1은 요소 존재, 0은 부재
본 포스팅에서는 해시 기반 집합과 트리 기반 집합을 중심으로 설명한다.
자바에서 해시 집합 구현
해시 집합은 해시 테이블을 기반으로 빠른 연산을 제공한다. 아래는 간단한 해시 집합의 자바 구현이다.
class HashSet {
private int capacity;
private Node[] buckets;
private int size;
static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
public HashSet(int capacity) {
this.capacity = capacity;
buckets = new Node[capacity];
size = 0;
}
private int hash(int data) {
return Math.abs(data % capacity);
}
// 요소 추가
public boolean add(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) return false;
current = current.next;
}
Node newNode = new Node(data);
newNode.next = buckets[index];
buckets[index] = newNode;
size++;
return true;
}
// 요소 제거
public boolean remove(int data) {
int index = hash(data);
Node current = buckets[index];
Node prev = null;
while (current != null) {
if (current.data == data) {
if (prev == null) {
buckets[index] = current.next;
} else {
prev.next = current.next;
}
size--;
return true;
}
prev = current;
current = current.next;
}
return false;
}
// 요소 존재 확인
public boolean contains(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) return true;
current = current.next;
}
return false;
}
// 집합 크기
public int size() {
return size;
}
// 비어 있음 확인
public boolean isEmpty() {
return size == 0;
}
}
이 코드는 해시 테이블 기반 집합으로, 추가, 제거, 포함 여부 확인 기능을 제공한다.
집합의 기본 연산
집합의 주요 연산은 다음과 같다:
1. 추가(Add)
집합에 요소를 추가한다. 이미 존재하면 추가하지 않는다.
public boolean add(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) return false;
current = current.next;
}
Node newNode = new Node(data);
newNode.next = buckets[index];
buckets[index] = newNode;
size++;
return true;
}
2. 제거(Remove)
집합에서 요소를 제거한다.
public boolean remove(int data) {
int index = hash(data);
Node current = buckets[index];
Node prev = null;
while (current != null) {
if (current.data == data) {
if (prev == null) {
buckets[index] = current.next;
} else {
prev.next = current.next;
}
size--;
return true;
}
prev = current;
current = current.next;
}
return false;
}
3. 포함 여부 확인(Contains)
집합에 요소가 있는지 확인한다.
public boolean contains(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) high>return true;
current = current.next;
}
return false;
}
4. 집합 연산
교집합, 합집합, 차집합 등은 두 집합을 비교해 수행한다.
public HashSet union(HashSet other) {
HashSet result = new HashSet(capacity);
for (Node bucket : buckets) {
Node current = bucket;
while (current != null) {
result.add(current.data);
current = current.next;
}
}
for (Node bucket : other.buckets) {
Node current = bucket;
while (current != null) {
result.add(current.data);
current = current.next;
}
}
return result;
}
집합의 시간 복잡도
해시 기반 집합의 주요 연산 시간 복잡도는 다음과 같다:
- 추가(Add): 평균 O(1), 최악 O(n)
- 제거(Remove): 평균 O(1), 최악 O(n)
- 포함 여부 확인(Contains): 평균 O(1), 최악 O(n)
- 집합 연산(Union, Intersection): O(n + m), n과 m은 두 집합의 크기
트리 기반 집합은 삽입, 삭제, 조회가 O(log n)이며, 정렬된 순회가 가능하다.
트리 기반 집합 구현
트리 기반 집합은 정렬된 상태를 유지하며, 자바의 TreeSet
과 유사하다. 아래는 간단한 이진 검색 트리 기반 집합이다.
class TreeSet {
private Node root;
private int size;
static class Node {
int data;
Node left, right;
Node(int data) {
this.data = data;
left = right = null;
}
}
public TreeSet() {
root = null;
size = 0;
}
public boolean add(int data) {
if (root == null) {
root = new Node(data);
size++;
return true;
}
Node current = root, parent = null;
while (current != null) {
if (data == current.data) return false;
parent = current;
current = data < current.data ? current.left : current.right;
}
if (data < parent.data) parent.left = new Node(data);
else parent.right = new Node(data);
size++;
return true;
}
public boolean contains(int data) {
Node current = root;
while (current != null) {
if (data == current.data) return true;
current = data < current.data ? current.left : current.right;
}
return false;
}
public int size() {
return size;
}
}
트리 기반 집합은 정렬된 순회를 지원하며, 균형 트리로 확장 가능하다.
집합을 활용한 알고리즘
집합은 중복 제거, 멤버십 테스트, 집합 연산에서 사용된다.
1. 중복 제거
배열에서 중복된 요소를 제거한다.
public int[] removeDuplicates(int[] nums) {
HashSet set = new HashSet(nums.length);
for (int num : nums) {
set.add(num);
}
int[] result = new int[set.size()];
int i = 0;
for (Node bucket : set.buckets) {
Node current = bucket;
while (current != null) {
result[i++] = current.data;
current = current.next;
}
}
return result;
}
2. 교집합 계산
두 배열의 공통 요소를 찾 교집합을 계산한다.
public int[] intersection(int[] nums1, int[] nums2) {
HashSet set = new HashSet(nums1.length);
for (int num : nums1) {
set.add(num);
}
HashSet result = new HashSet(nums2.length);
for (int num : nums2) {
if (set.contains(num)) {
result.add(num);
}
}
int[] arr = new int[result.size()];
int i = 0;
for (Node bucket : result.buckets) {
Node current = bucket;
while (current != null) {
arr[i++] = current.data;
current = current.next;
}
}
return arr;
}
3. 고유 요소 세기
배열에서 고유한 요소의 개수를 계산한다.
public int countUnique(int[] nums) {
HashSet set = new HashSet(nums.length);
for (int num : nums) {
set.add(num);
}
return set.size();
}
심화 주제: 집합 최적화
집합을 효율적으로 사용하려면 몇 가지 기법을 고려한다.
1. 해시 충돌 최소화
해시 함수를 개선하고, 적절한 로드 팩터를 유지한다.
private int hash(int data) {
int hash = data;
hash ^= (hash >>> 20) ^ (hash >>> 12);
return hash ^ (hash >>> 7) ^ (hash >>> 4);
}
2. 동적 크기 조정
해시 집합의 크기를 동적으로 확장한다.
private void resize() {
if ((double)size / capacity < 0.75) return;
Node[] oldBuckets = buckets;
capacity *= 2;
buckets = new Node[capacity];
size = 0;
for (Node bucket : oldBuckets) {
Node current = bucket;
while (current != null) {
add(current.data);
current = current.next;
}
}
}
3. 트리 기반 최적화
AVL 트리나 레드-블랙 트리를 사용해 균형을 유지한다.
집합의 한계와 대안
집합은 순서 정보가 없고, 중복을 허용하지 않는다. 자바에서는 java.util.HashSet
와 java.util.TreeSet
을 제공한다.
import java.util.HashSet;
HashSet<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.remove(1);
TreeSet
은 정렬된 집합을 제공한다.
import java.util.TreeSet;
TreeSet<Integer> set = new TreeSet<>();
set.add(1);
set.add(2);
int first = set.first(); // 1
문제
집합을 활용한 알고리즘을 연습하기 위해 난이도별 문제를 제시하고, 자바 코드로 해답을 제공한다.
난이도 하: 집합 연산
시간 제한: 1 초 | 메모리 제한: 256 MB |
정수 연산이 주어진다. 연산은 “add x” (x를 집합에 추가), “remove x” (x를 집합에서 제거), “contains x” (x가 집합에 있는지 확인)이다. 모든 연산을 수행한 결과를 출력한다.
입력
첫째 줄에 연산 수 N (1 ≤ N ≤ 100,000)이 주어진다. 다음 N줄에 “add x”, “remove x”, “contains x” (x는 -1,000 ≤ x ≤ 1,000)가 주어진다.
출력
각 “contains” 연산에 대해 x가 집합에 있으면 “YES”, 없으면 “NO”를 출력한다.
입력 예시
5
add 1
add 2
contains 1
remove 2
contains 2
출력 예시
YES
NO
문제 출처: 자체 제작
해답
해답 펼치기
import java.util.Scanner;
public class SetOperations {
static class HashSet {
private int capacity;
private Node[] buckets;
private int size;
static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
HashSet(int capacity) {
this.capacity = capacity;
buckets = new Node[capacity];
size = 0;
}
private int hash(int data) {
return Math.abs(data % capacity);
}
boolean add(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) return false;
current = current.next;
}
Node newNode = new Node(data);
newNode.next = buckets[index];
buckets[index] = newNode;
size++;
return true;
}
boolean remove(int data) {
int index = hash(data);
Node current = buckets[index];
Node prev = null;
while (current != null) {
if (current.data == data) {
if (prev == null) {
buckets[index] = current.next;
} else {
prev.next = current.next;
}
size--;
return true;
}
prev = current;
current = current.next;
}
return false;
}
boolean contains(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) return true;
current = current.next;
}
return false;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
HashSet set = new HashSet(N);
sc.nextLine();
for (int i = 0; i < N; i++) {
String operation = sc.nextLine();
String[] parts = operation.split(" ");
int x = Integer.parseInt(parts[1]);
if (parts[0].equals("add")) {
set.add(x);
} else if (parts[0].equals("remove")) {
set.remove(x);
} else {
System.out.println(set.contains(x) ? "YES" : "NO");
}
}
sc.close();
}
}
난이도 중: 고유 요소 세기
정수 배열이 주어진다. 배열에서 고유한 요소의 개수를 계산한다.
입력
첫째 줄에 배열의 길이 N (1 ≤ N ≤ 100,000)이 주어진다. 둘째 줄에 N개의 정수가 공백으로 구분되어 주어진다. 각 정수는 -1,000 ≤ x ≤ 1,000이다.
출력
고유한 요소의 개수를 출력한다.
입력 예시
6
1 2 2 3 3 3
출력 예시
3
문제 출처: 자체 제작
해답
해답 펼치기
import java.util.Scanner;
public class CountUnique {
static class HashSet {
private int capacity;
private Node[] buckets;
private int size;
static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
HashSet(int capacity) {
this.capacity = capacity;
buckets = new Node[capacity];
size = 0;
}
private int hash(int data) {
return Math.abs(data % capacity);
}
boolean add(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) return false;
current = current.next;
}
Node newNode = new Node(data);
newNode.next = buckets[index];
buckets[index] = newNode;
size++;
return true;
}
int size() {
return size;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
HashSet set = new HashSet(N);
for (int i = 0; i < N; i++) {
int num = sc.nextInt();
set.add(num);
}
System.out.println(set.size());
sc.close();
}
}
난이도 상: 두 배열의 교집합
두 정수 배열이 주어진다. 두 배열의 공통 요소를 반환한다.
입력
첫째 줄에 첫 번째 배열의 길이 N (1 ≤ N ≤ 100,000)과 두 번째 배열의 길이 M (1 ≤ M ≤ 100,000)이 주어진다. 둘째 줄에 N개의 정수가 공백으로 구분되어 주어진다. 셋째 줄에 M개의 정수가 공백으로 구분되어 주어진다. 모든 정수는 -1,000 ≤ x ≤ 1,000이다.
출력
공통 요소를 공백으로 구분해 출력한다. 결과는 임의의 순서로 출력해도 된다.
입력 예시
4 3
1 2 3 4
2 3 5
출력 예시
2 3
문제 출처: LeetCode "Intersection of Two Arrays"
해답
해답 펼치기
import java.util.Scanner;
public class Intersection {
static class HashSet {
private int capacity;
private Node[] buckets;
private int size;
static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
HashSet(int capacity) {
this.capacity = capacity;
buckets = new Node[capacity];
size = 0;
}
private int hash(int data) {
return Math.abs(data % capacity);
}
boolean add(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) return false;
current = current.next;
}
Node newNode = new Node(data);
newNode.next = buckets[index];
buckets[index] = newNode;
size++;
return true;
}
boolean contains(int data) {
int index = hash(data);
Node current = buckets[index];
while (current != null) {
if (current.data == data) return true;
current = current.next;
}
return false;
}
int size() {
return size;
}
int[] toArray() {
int[] result = new int[size];
int i = 0;
for (Node bucket : buckets) {
Node current = bucket;
while (current != null) {
result[i++] = current.data;
current = current.next;
}
}
return result;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int M = sc.nextInt();
HashSet set = new HashSet(N);
for (int i = 0; i < N; i++) {
int num = sc.nextInt();
set.add(num);
}
HashSet result = new HashSet(M);
for (int i = 0; i < M; i++) {
int num = sc.nextInt();
if (set.contains(num)) {
result.add(num);
}
}
int[] intersection = result.toArray();
for (int i = 0; i < intersection.length; i++) {
System.out.print(intersection[i]);
if (i < intersection.length - 1) {
System.out.print(" ");
}
}
System.out.println();
sc.close();
}
}
마무리
집합은 프로그래밍에서 중복 제거, 멤버십 테스트, 집합 연산을 효율적으로 수행하는 강력한 자료구조이다. 자바에서 HashSet
은 빠른 연산을, TreeSet
은 정렬된 순회를 제공한다. 본 포스팅에서 다룬 구현과 알고리즘을 통해 집합의 동작 원리를 이해하고, 실제 문제 해결에 적용할 수 있다.
집합은 단순하지만 강력하다. 중복을 제거하거나 공통 요소를 찾는 문제를 풀 때, 집합을 떠올리자. 연습 문제들을 풀어보며 집합의 활용법을 익히고, 더 복잡한 문제에 도전해보자.
핵심 요약
- 집합은 중복 없는 고유 요소의 모음이다.
- 해시 집합은 O(1) 연산, 트리 집합은 O(log n) 연산과 정렬을 제공한다.
- 중복 제거, 교집합 계산 등에 유용하다.
- 자바의
HashSet
,TreeSet
을 활용하자.
궁금한 점이나 추가로 알고 싶은 주제가 있다면 언제든 질문하자. 집합을 마스터하면 데이터 처리와 알고리즘 풀이가 한결 수월해진다!
'코딩 공부 > 알고리즘(자바)' 카테고리의 다른 글
기본 자료구조 - 해시 테이블 (Hash Table) (0) | 2025.05.08 |
---|---|
기본 자료구조 - 덱 (Deque) (3) | 2025.05.06 |
기본 자료구조 - 큐 (Queue) (1) | 2025.05.04 |
기본 자료구조 - 스택 (Stack) (0) | 2025.05.03 |
기본 자료구조 - 연결 리스트 (Linked List) (1) | 2025.05.03 |