<개인 학습 기록>
🇶1. HashSet의 내부 동작 방식과 중복 제거 메커니즘을 설명하고, HashSet이 효율적인 중복 체크를 할 수 있는 이유를 설명해주세요.
🇶2. O(n)과 O(log n)의 성능 차이를 실생활 예시를 들어 설명하고, 데이터의 크기가 1백만 개일 때 각각 대략 몇 번의 연산이 필요한지 비교해주세요.
Set은 중복을 허용하지 않는 데이터 컬렉션을 의미한다.
Java에서 Set인터페이스의 대표 구현체가 바로 HashSet이다.
HashMap.put(E, PRESENT) 방식으로 처리된다.E는 원소, PRESENT는 고정된 더미(dummy)값예시(코드)
HashSet<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
내부적으로는 아래와 같이 동작한다.
HashMap<String, Object> map = new HashMap<>();
map.put("apple", DUMMY);
map.put("banana", DUMMY);
여기에서 DUMMY는 HashSet에서 정적으로 선언된 값이다.
Collection.synchronizedSet()메서드를 사용할 수 있다.
// Java program to demonstrate HashSet
import java.util.*;
class GFG {
public static void main(String[] args) {
Set<String> hash_Set = new HashSet<String>();
hash_Set.add("GEEKS");
hash_Set.add("GeeksForGeeks");
hash_Set.add("GEEKS");
hash_Set.add("Example");
System.out.println(hash_Set);
}
}
HashSet<String> 타입의 hash_Set 객체를 만든다."Geeks", "GeeksForGeeks", "Geeks"(중복), "Example" 네개의 값을 추가한다.System.out.println(hash_Set) 으로 전체 셋의 값을 출력한다.[Example, Geeks, GeeksForGeeks]"Geeks"는 두번 add 했지만 한번만 포함되고, 데이터의 입력 순서는 보장되지 않는다.(1) HashSet이 HashMap을 내부적으로 사용한다.
| key | value |
|---|---|
| "Geeks" | "E" |
| "GeeksForGeeks" | "E" |
| "Geeks" | "E" |
(2) 코드에서의 실제 동작(key/value 저장 모습)
hash_set.add("Geeks");
hash_set.add("GeeksforGeeks");
hash_set.add("Geeks");
hash_set.add("Example");
HashSet이 어디에 위치하는지 보여주는 UML 구조도이다.반복 가능한 것 - java.lang.Iterable <>
"for-each 반복문으로 돌릴 수 있는 객체"
for (String s : list) {
System.out.println(s);
} // 이게 가능한 이유가 Iterable 때문이다.
java.util.Collection <>
관계를 보면
Iterable
↑ extends
Collection
Java에서 "데이터 모음"의 가장 기본 인터페이스
대표 메서드로는 add(), remove(), size(), iterator()
java.util.Set (Converting a Collection to a Set)
관계를 보면
Collection
↑ extends
Set
Set의 특징으로는 중복 허용 안함, 순서 보장 안함.
Set<Inteager> set = new HashSet<>();
set.add(1);
set.add(1);
System.out.println(set); //[1]
Abstract class 등장 (*)
Java에서는 인터페이스를 바로 구현하기 어렵기 때문에 중간에 Abstract 클래스가 들어간다.
대표적으로 java.Abstractcollection, java.util.AbstractSet
구조를 보면
Collection (interface)
↑
AbstractCollection (abstract class)
↑
AbstractSet (abstract class)
이 클래스들이 하는 일은 "공통 로직을 미리 구현해 둔다"
예를 들어서 size(), isEmpty(), toString() 그래서 실제 구현 클래스는 핵심 로직만 구현하면 된다.
java.util.HashSet (최종 구현 클래스 : HashSet)
관계를 보면
HashSet
extends AbstractSet
implements Set
실제 구조를 코드로 보면
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, Serializable
추가 인터페이스
HashSet은 두개 더 구현한다.
HashSet<String> copy = (HashSet<String> original.clone();object -> byte streamInterface -> Abstract -> Concreate class인터페이스 기반 설계를 위해서이다.HashSet
↓
HashMap 즉, HashSet = HashMap wrapper 로 볼수 있다. 그래서 시간복잡도를 아래와 같이 확인할수 있다.add() O(1)
remove() O(1)
contains() O(1) 
https://www.geeksforgeeks.org/dsa/analysis-algorithms-big-o-analysis/
시간 복잡도(Time Complexity)는 입력 데이터 크기 n이 커질 때 알고리즘 실행 시간이 어떻게 증가 하는지를 나타내는 성장률(growth rate) 분석이다. 표기법은 Big-O를 사용한다. 대표적으로 O(1), O(log n), O(n)이 자주 등장한다.
1. O(1) - Constant Time (상수 시간)
데이터 개수와 관계없이 항상 같은 시간이 걸리는 연산
int value = array[5]; 이 연산은 항상 동일하다./ 시간이 거의 변하지 않는다.
데이터 10개 -> 1 step
데이터 1,000개 -> 1 step
데이터 1,000,000개 -> 1 step
대표적으로 배열 인덱스 접근, 해시 조회, 스택 push/pop 이 있고, 대표적인 자료 구조로는 HashMap, HashSet이 있다.
예를 들어서
HashSet<String> set = new HashSet<>();
set.contains("apple");
평균 시간 복잡도 = O(1) / 이유는 hash table
for(int 1=0; i<n; i++) {
System.out.println(array[i]); 데이터 증가n=10 -> 10번 실행
n=100 -> 100번 실행
n=1000 -> 1000번 실행 대표적으로 배열 전체 탐색, 리스트 검색, 파일 전체 읽기에서 사용된다.List<String> list = new ArrayList<>();
for(String s: list) {
if(s.equals("apple"))
return true;
}자료구조로는 Java ArrayListO(n) 처음부터 끝까지 찾아야 한다.데이터 16개
1 step -> 8개
2 step -> 4개
3 step -> 2개
4 step -> 1개 그래서 log₂(16) = 4int binarySearch(int[] arr, int targete) {
int left = 0;
int right = arr.length-1;
while(left <= right) {
int mid = (left+right)/2;
if(arr[mid] == target)
return mid;
if(arr[mid] == target)
return mid;
if(arr[mid] < target)
left = mid+1;
else
right = mid-1;
}
return -1;
}초기에는 조금 증가하지만 n이 커질수록 증가 속도가 매우 느리다.TreeMap,TreeSet이 있다. 연산은 insert,delete,search예를 들어 데이터가 1,000,000개 있을 때
| 복잡도 | 연산횟수 | 차이 |
|---|---|---|
| O(1) | 1 | 즉시 |
| O(log n) | 약 20 | 매우 빠름 |
| O(n) | 1,000,000 | 느림 |
그래서 알고르즘 설계에서 `O(n) -> O(log n) -> O(1) 방향으로 개선하는 것이 중요하다.
| 자료 구조 | 검색 |
|---|---|
| ArrayList | O(n) |
| LinkedList | O(n) |
| HashSet | O(1) |
| HashMap | O(1) |
| TreeSet | O(log n) |
| TreeMap | O(log n) |
List.contains() 이 알고리즘의 시간 복잡도는? O(n)
HashSet.contains() 는? O(1)
그래서 List -> Set 으로 성능이 수천 배 좋아질 수 도 있다.
그렇다면, HashSet이 O(1)인가?
Hash function
-> bucket
-> direct access
하지만 hash collision 발생하면 최악 -> O(n)
요약하자면,
O(1) -> 데이터 크기와 상관없이 일정
O(log n) -> 데이터를 절반씩 줄이며 탐색
O(n) -> 데이터 크기만큼 증가, n이면 최대 𝑛번 확인(데이터를 하나씩 다 봐야 한다.)
예시 1. 도서관에서 책 찾기
상황 : 도서관에 1,000,000권 책이 있다.
O(n) 방식(선형 탐색) - 책이 정렬되어 있지 않다.
찾는 방법.. 첫번째 책 확인, 두번째 책 확인, 세번째 책 확인... 찾을때 까지 계속 확인
최악의 경우 1,000,000번 확인, 연산횟수 O(n) = 1,000,000
O(log n) 방식(이진 탐색) - 책이 제목 순서로 정렬되어 있다.
찾는 방법.. 1.가운데 책 확인(500,000번째) 2.찾는 책이 앞인지 뒤인지 판단 3.남은 절반에서 다시 가운데 확인
1번째 -> 500,000
2번째 -> 250,000
3번째 -> 125,000
4번째 -> 62,500
...
20번째 -> 찾음
연산횟수 : O(log n) = 20
예시 2: 건물에서 사람 찾기
상황: 회사 건물에 1,000,000명 직원이 있다.
O(n)방식 : 사람이 무작위로 흩어져 있다.
찾는 방법 : 1. 한명 씩 얼굴 확인.. 2. 계속 확인...
최악의 경우 1,000,000명 확인, 연산횟수 O(n) = 1,000,000
O(log n)방식 :사람이 사번 순서로 정렬된 방에 배치
찾는 방법: 1.건물 가운데 층 확인 2.사번이 더 크면 위로 3.더 작으면 아래로
1층 ~ 1,000,000층
1 -> 500,000층
2 -> 250,000층
3 -> 125,000층
...
20 -> 찾음
연산횟수: O(log n) = 20
<참조: https://www.geeksforgeeks.org/java/internal-working-of-sethashset-in-java/>