Set은 유일한 요소들의 컬렉션
특징
용도 : 중복을 허용하지 않고, 요소의 유무만 중요한 경우에 사용됨
- 예시
- List : 장바구니 목록, 순서가 중요한 일련의 이벤트 목록
- Set : 회원 ID 집합, 고유한 항목의 집합
add(value): 셋에 값을 추가하고, 중복 데이터는 저장하지 않음contains(value): 셋에 값이 있는지 확인함remove(value): 셋에 있는 값을 제거함
add(), contains()만 구현 package collection.set;
import java.util.Arrays;
public class MyHashSetV0 {
private int[] elementData = new int[10];
private int size = 0;
// O(n)
public boolean add(int value) {
if (contains(value)) {
return false;
}
elementData[size] = value;
size++;
return true;
}
// O(n)
public boolean contains(int value) {
for (int data : elementData) {
if (data == value) {
return true;
}
}
return false;
}
public int getSize() {
return size;
}
@Override
public String toString() {
return "MyHashSetV0{" +
"elementData=" + Arrays.toString(Arrays.copyOf(elementData, size)) +
", size=" + size + '}';
}
}
package collection.set;
public class MyHashSetV0Main {
public static void main(String[] args) {
MyHashSetV0 set = new MyHashSetV0();
set.add(1); // O(1)
set.add(2); // O(n)
set.add(3); // O(n)
set.add(4); // O(n)
System.out.println(set);
boolean result = set.add(4);//중복 데이터 저장
System.out.println("중복 데이터 저장 결과 = " + result);
System.out.println(set);
System.out.println("set.contains(3): " + set.contains(3)); // O(n)
System.out.println("set.contains(99): " + set.contains(99)); // O(n)
}
}
실행결과
MyHashSetV0{elementData=[1, 2, 3, 4], size=4}
중복 데이터 저장 결과 = false
MyHashSetV0{elementData=[1, 2, 3, 4], size=4}
set.contains(3): true
set.contains(99): false
문제
package collection.set;
import java.util.Arrays;
public class HashStart1 {
public static void main(String[] args) {
Integer[] inputArray = new Integer[4];
inputArray[0] = 1;
inputArray[1] = 2;
inputArray[2] = 5;
inputArray[3] = 8;
System.out.println("inputArray = " + Arrays.toString(inputArray));
int searchValue = 8;
//4번 반복 O(n)
for (int inputValue : inputArray) {
if (inputValue == searchValue) {
System.out.println(inputValue);
}
}
}
}
실행결과
inputArray = [1, 2, 5, 8]
8


package collection.set;
import java.util.Arrays;
public class HashStart2 {
public static void main(String[] args) {
//입력: 1, 2, 5, 8
//[null, 1, 2, null, null, 5, null, null, 8, null]
Integer[] inputArray = new Integer[10];
inputArray[1] = 1;
inputArray[2] = 2;
inputArray[5] = 5;
inputArray[8] = 8;
System.out.println("inputArray = " + Arrays.toString(inputArray));
int searchValue = 8;
Integer result = inputArray[searchValue]; // O(1)
System.out.println(result);
}
}
실행결과
inputArray = [null, 1, 2, null, null, 5, null, null, 8, null]
8

문제
입력: 0-99 사이의 여러 값이 입력되고, 중복된 값은 입력되지 않음
찾기: 0-99 사이의 값이 하나 입력되고, 입력된 값 중에 찾는 값이 있는지 확인
검색 속도를 높이기 위해 앞서 학습한 것처럼 데이터의 값을 배열의 인덱스로 사용
package collection.set;
import java.util.Arrays;
public class HashStart3 {
public static void main(String[] args) {
//{1, 2, 5, 8, 14, 99}
//[null, 1, 2, null, null, 5, null, null, 8, .., 14 ....., 99]
Integer[] inputArray = new Integer[100];
inputArray[1] = 1;
inputArray[2] = 2;
inputArray[5] = 5;
inputArray[8] = 8;
inputArray[14] = 14;
inputArray[99] = 99;
System.out.println("inputArray = " + Arrays.toString(inputArray));
int searchValue = 99;
Integer result = inputArray[searchValue]; // O(1)
System.out.println(result);
}
}
실행결과
inputArray = [null, 1, 2, null, null, 5, null, null, 8, null, null, null, null,
null, 14, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, 99]
99
데이터의 값을 인덱스로 사용한 덕분에 O(1)의 빠른 검색 속도를 얻을 수 있고, 해당 코드는 정상적으로 수행됨
하지만 낭비되는 메모리 공간이 너무 많음
만약 입력 값의 범위를 0-99를 넘어서 int 숫자의 모든 범위를 입력할 수 있도록 하려면 배열의 크기를 얼마로 잡아야 할까?
- 0-99까지의 범위 입력
- 사이즈 100의 배열이 필요 : 4byte * 100 (단순히 값의 크기로만 계산)
- int 범위 입력
- int 범위 : -2,147,483,648 - 2,147,483,647
- 약 42억 사이즈의 배열 필요
- 4byte * 42억 = 약 17기가바이트 필요(단순히 값의 크기로만 계산)
데이터의 값을 인덱스로 사용할 때, 입력할 수 있는 값의 범위가 int라면 한번의 연산에 최신 컴퓨터의 메모리가 거의 다 소모되어 버림
따라서 데이터의 값을 인덱스로 사용하는 방식은 입력 값의 범위가 넓다면 사용하기 어려워 보임
데이터의 값을 인덱스로 사용하는 방법은 매우 빠른 성능을 보장하지만, 입력 값의 범위가 조금만 커져도 메모리 낭비가 너무 심하기 때문에, 그대로 사용하기에는 문제가 있다
앞에서 이야기한 것처럼 모든 숫자를 입력할 수 있다고 가정하면, 입력값의 범위가 너무 넓어져서 데이터의 값을 인덱스로 사용하기는 어려움
하지만 입력값의 범위가 넓어도 해당 범위의 값을 모두 입력하는 것은 아님
- 공간도 절약하면서, 넓은 범위의 값을 사용할 수 있는 방법이 있는데, 바로 나머지 연산을 사용하는 것
- 저장할 수 있는 배열의 크기를 10으로 가정하고 그 크기에 맞추어 나머지 연산을 사용하면 됨
나머지 연산
- 1 % 10 = 1
- 2 % 10 = 2
- 5 % 10 = 5
- 8 % 10 = 8
- 14 % 10 = 4
- 99 % 10 = 9


package collection.set;
import java.util.Arrays;
public class HashStart4 {
static final int CAPACITY = 10;
public static void main(String[] args) {
//{1, 2, 5, 8, 14, 99}
System.out.println("hashIndex(1) = " + hashIndex(1));
System.out.println("hashIndex(2) = " + hashIndex(2));
System.out.println("hashIndex(5) = " + hashIndex(5));
System.out.println("hashIndex(8) = " + hashIndex(8));
System.out.println("hashIndex(14) = " + hashIndex(14));
System.out.println("hashIndex(99) = " + hashIndex(99));
Integer[] inputArray = new Integer[CAPACITY];
add(inputArray, 1);
add(inputArray, 2);
add(inputArray, 5);
add(inputArray, 8);
add(inputArray, 14);
add(inputArray, 99);
System.out.println("inputArray = " + Arrays.toString(inputArray));
//검색
int searchValue = 14;
int hashIndex = hashIndex(searchValue);
System.out.println("searchValue hashIndex = " + hashIndex);
Integer result = inputArray[hashIndex]; // O(1)
System.out.println(result);
}
private static void add(Integer[] inputArray, int value) {
int hashIndex = hashIndex(value);
inputArray[hashIndex] = value;
}
static int hashIndex(int value) {
return value % CAPACITY;
}
}
실행 결과
hashIndex(1) = 1
hashIndex(2) = 2
hashIndex(5) = 5
hashIndex(8) = 8
hashIndex(14) = 4
hashIndex(99) = 9
inputArray = [null, 1, 2, null, 14, 5, null, null, 8, 99]
searchValue hashIndex = 4
14
hashIndex()
add()
조회
inputArray[hashIndex]입력 값의 범위가 넓어도 실제 모든 값이 들어오지는 않기 때문에 배열의 크기를 제한하고, 나머지 연산을 통해 메모리가 낭비되는 문제도 해결할 수 있음
해시 인덱스를 사용해서 O(1)의 성능으로 데이터를 저장하고, O(1)의 성능으로 데이터를 조회할 수 있게 되었음

먼저 99의 값을 저장
다음으로 9의 값을 저장
결과적으로 배열의 인덱스 9에는 처음에 저장한 값 99는 사라지고, 마지막에 저장한 값 9만 남게 됨
이 문제를 해결하는 가장 단순한 방법은 CAPACITY를 값의 입력 범위만큼 키우면 됨




O(n)의 성능을 보임O(n)의 성능을 보임O(1)의 성능을 제공함package collection.set;
import java.util.Arrays;
import java.util.LinkedList;
public class HashStart5 {
static final int CAPACITY = 10;
public static void main(String[] args) {
//{1, 2, 5, 8, 14, 99 ,9}
LinkedList<Integer>[] buckets = new LinkedList[CAPACITY];
for (int i = 0; i < CAPACITY; i++) {
buckets[i] = new LinkedList<>();
}
add(buckets, 1);
add(buckets, 2);
add(buckets, 5);
add(buckets, 8);
add(buckets, 14);
add(buckets, 99);
add(buckets, 9); //중복
System.out.println("buckets = " + Arrays.toString(buckets));
//검색
int searchValue = 9;
boolean contains = contains(buckets, searchValue);
System.out.println("buckets.contains(" + searchValue + ") = " + contains);
}
private static void add(LinkedList<Integer>[] buckets, int value) {
int hashIndex = hashIndex(value);
LinkedList<Integer> bucket = buckets[hashIndex]; // O(1)
if (!bucket.contains(value)) { // O(n)
bucket.add(value);
}
}
private static boolean contains(LinkedList<Integer>[] buckets, int searchValue) {
int hashIndex = hashIndex(searchValue);
LinkedList<Integer> bucket = buckets[hashIndex]; // O(1)
return bucket.contains(searchValue); // O(n)
}
static int hashIndex(int value) {
return value % CAPACITY;
}
}
실행 결과
buckets = [[], [1], [2], [], [14], [5], [], [], [8], [99, 9]]
bucket.contains(9) = true
결과

LinkedList<Integer>[] buckets = new LinkedList[CAPACITY]
먼저 배열의 이름을 buckets이라고 지었음
LinkedList는 하나의 바구니
쉽게 이야기해서 바구니들(배열) 안에 각각의 바구니(연결 리스트)가 있고, 바구니(연결 리스트) 안에 데이터가 들어가는 구조
private static void add(LinkedList<Integer>[] buckets, int value) {
int hashIndex = hashIndex(value);
LinkedList<Integer> bucket = buckets[hashIndex]; //O(1)
if (!bucket.contains(value)) { //O(n)
bucket.add(value);
}
}
데이터를 등록할 때 먼저 해시 인덱스(hashIndex)를 구함
해시 인덱스로 배열의 인덱스를 찾음
셋은 중복된 값을 저장하지 않음
따라서 바구니에 값을 저장하기 전에 contains()를 사용해서 중복 여부를 확인함
contains()는 모든 항목을 다 순회하기 때문에 O(n)의 성능임O(1)의 성능을 제공함private static boolean contains(LinkedList<Integer>[] buckets, int searchValue) {
int hashIndex = hashIndex(searchValue);
LinkedList<Integer> bucket = buckets[hashIndex]; //O(1)
return bucket.contains(searchValue); //O(n)
}
bucket.contains(searchValue) 메서드를 사용해서 찾는 데이터가 있는지 확인contains()는 모든 항목을 다 순회하기 때문에 O(n)의 성능임O(n)의 추가 연산을 해야 하므로 성능이 덜어짐CAPACITY 값을 변경하면서 실행
CAPACITY = 1: [[1, 2, 5, 8, 14, 99, 9]]
CAPACITY = 5: [[5], [1], [2], [8], [14, 99, 9]]
CAPACITY = 10: [[], [1], [2], [], [14], [5], [], [], [8], [99, 9]]
CAPACITY = 11: [[99], [1], [2], [14], [], [5], [], [], [8], [9], []]
CAPACITY = 15: [[], [1], [2], [], [], [5], [], [], [8], [99, 9], [], [], [], [], [14]]
CAPACITY = 1 : 배열의 크기가 하나밖에 없을 때는 모든 해시가 충돌한다.CAPACITY = 5 : 배열의 크기가 입력하는 데이터 수 보다 작은 경우 해시 충돌이 자주 발생한다.CAPACITY = 10 : 저장할 데이터가 7개 인데, 배열의 크기는 10이다. 7/10 약 70% 정도로 여유있게 데이터가 저장된다. 이 경우 가끔 충돌이 발생한다. CAPACITY = 11 : 저장할 데이터가 7개 인데, 배열의 크기는 11이다. 가끔 충돌이 발생한다. 여기서는 충돌이 발생하지 않았다.CAPACITY = 15 : 저장할 데이터가 7개 인데, 배열의 크기는 15이다. 가끔 충돌이 발생한다. 여기서는 충돌이 발생했다.아주 간단한 예제로 알아보았지만, 통계적으로 입력한 데이터의 수가 배열의 크기를 75% 넘지 않으면 해시 인덱스는 자주 충돌하지 않음
75%를 넘으면 자주 충돌하기 시작함배열의 크기를 크게 만들면 해시 충돌은 줄어서 성능은 좋아지지만, 많은 메모리가 낭비됨
반대로 배열의 크기를 너무 작게 만들면 해시가 자주 충돌해서 성능이 나빠짐
상황에 따라 다르겠지만 보통 75%를 적절한 크기로 보고 기준으로 잡는 것이 효과적임
- 데이터 저장
- 평균: O(1)
- 최악: O(n)
- 데이터 조회
- 평균: O(1)
- 최악: O(n)
O(1)에 가까운 매우 빠른 성능을 보여줌