리스트는 요소들의 순차적인 컬렉션이다. 요소들은 특정 순서를 가지며, 같은 요소가 여러 번 나타날 수 있다.
세트는 유일한 요소들의 컬렉션이다. 참고로 세트보다는 셋으로 많이 불린다.
public class MyHashSetV0 {
private int[] elementData = new int[10];
private int size = 0;
public boolean add(int value) {
if (contains(value)) {
return false;
}
elementData[size] = value;
size++;
return true;
}
public boolean contains(int value) {
for (int i = 0; i < elementData.length; i++) {
if (elementData[i] == value) {
return true;
}
}
return false;
}
public int size() {
return size;
}
@Override
public String toString() {
return "MyHashSetV0{" +
"elementData=" + Arrays.toString(elementData) +
", size=" + size +
'}';
}
}
public class MyHashSetV0Main {
public static void main(String[] args) {
MyHashSetV0 myHashSetV0 = new MyHashSetV0();
myHashSetV0.add(1);
myHashSetV0.add(2);
myHashSetV0.add(3);
myHashSetV0.add(4);
myHashSetV0.add(5);
System.out.println(myHashSetV0);
boolean result = myHashSetV0.add(3);
System.out.println("중복 데이터 저장 결과 : " + result);
System.out.println(myHashSetV0);
System.out.println("myHashSetV0.contains(3) : " + myHashSetV0.contains(3));
System.out.println("myHashSetV0.contains(99) : " + myHashSetV0.contains(99));
}
}
add(value)
: 셋에 중복된 값이 있는지 체크하고, 중복된 값이 있으면 false
를 반환한다. 중복된 값이 없으면 값을 저장하고 true
를 반환한다.contains(value)
: 셋에 값이 있는지 확인한다. 값이 있으면 true
를 반환하고, 값이 없으면 false
를 반환한다.add(value)
의 경우 contains(value)
를 사용하는데 이 때, contains(value)
의 경우 for문을 통해 탐색하는데 이 때 시간 복잡도는 O(N)
이다. 중복 데이터 검색 O(N)
+ 데이터 추가 O(1)
이므로 최종 시간 복잡도는 O(N)
이다.contains(value)
로 데이터를 찾을 때는 배열에 있는 모든 데이터를 찾고 비교해야 하므로 평균적으로 O(N)
이 걸린다.public class HashStart1 {
public static void main(String[] args) {
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);
}
}
O(1)
의 시간 복잡도로 설계할 수 있다.public class HashStart2 {
public static void main(String[] args) {
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));
}
}
O(1)
의 빠른 검색을 얻었으나 입력 값의 범위가 넓어지게 된다면 낭비되는 메모리 공간이 그에 비례해 많아지게 된다.모든 숫자를 입력할 수 있다고 가정하면 입력값의 범위가 너무 넓어져서 데이터의 값을 인덱스로 사용하기는 어렵다. 공간도 절약하면서 넓은 범위의 값을 사용할 수 있는 방법으로 나머지 연산이 있다.
✔️해시 인덱스
배열의 인덱스로 사용할 수 있도록 원래의 값을 계산(여기선 나머지 연산)한 인덱스를 해시 인덱스라 한다. 기본 용량을 10으로 가정할 때 14의 해시 인덱스는 4, 99의 해시 인덱스는 9인 것이다.
public class HashStart3 {
private static final int CAPACITY = 10;
public static void main(String[] args) {
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;
}
private static int hashIndex(int value) {
return value % CAPACITY;
}
}
✔️코드 문제점 : 해시 충돌
예를 들어, 99를 10으로 나눈 나머지는 9고 19를 10으로 나눈 나머지 역시 9다. 이렇게 되면 같은 해시 인덱스를 가지게 되어 해시 충돌이 발생하게 된다.
해시 충돌의 해결 방안은 바로 해시 충돌이 일어났을 때 단순하게 같은 해시 인덱스의 값을 같은 인덱스에 함께 저장하는 것이다. 아래와 같이 배열 안에 배열을 만드는 것이다.
빅오 표기법은 최악의 경우를 기준으로 시간 복잡도를 제공한다. 위와 같이 예를 들어, 9, 19, 29, 99만 저장한다고 가정하면 이 경우 모든 해시 인덱스가 9가 된다. 따라서 9번 인덱스에 데이터가 모두 저장된다. 데이터를 찾을 때 9번에 가서 저장한 데이터의 수만큼 반복해서 값을 찾아야 한다. 따라서 최악의 경우 O(N)
의 성능을 보인다.
public class HashStart4 {
private static final int CAPACITY = 10;
public static void main(String[] args) {
// 전체의 큰 연결 리스트 하나
LinkedList<Integer>[] buckets = new LinkedList[CAPACITY];
System.out.println("buckets : " + Arrays.toString(buckets));
// 그 안에 각 해시 인덱스 값에 속하는 세분화된 연결 리스트 하나
for (int i = 0; i < CAPACITY; i++) {
buckets[i] = new LinkedList<>();
}
System.out.println("buckets : " + Arrays.toString(buckets));
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(N)
if (!bucket.contains(value)) {
bucket.add(value);
}
}
private static boolean contains(LinkedList<Integer>[] buckets, int searchValue) {
int hashIndex = hashIndex(searchValue);
LinkedList<Integer> bucket = buckets[searchValue];
return bucket.contains(hashIndex);
}
private static int hashIndex(int value) {
return value % CAPACITY;
}
}
O(N)
의 연산이 필요하다. 따라서 성능을 위해서 가급적이면 해시 충돌이 발생하지 않도록 해야 한다.