HashSet의 내부 동작 방식과 중복 제거 메커니즘을 설명하고, HashSet이 효율적인 중복 체크를 할 수 있는 이유를 설명해주세요.
- HashSet이란?
- Java의 java.util 패키지에 있는 Set 인터페이스의 구현체 중 하나로, 중복을 허용하지 않고, 순서를 보장하지 않는 컬렉션이다.
HashSet의 핵심 특징
| 중복허용 X | 같은 값이 두 번 저장되지 않음 (equals() & hashCode()로 판단) |
| 순서 보장 X | 저장한 순서를 유지하지 않음 |
| null 허용 O | null 값을 1개 저장 가능 |
| 검색 속도 빠름 | 내부적으로 HashMap 기반으로 구현되어 탐색/삽입 속도가 빠름 |
| 정렬 안됨 | 정렬된 상태가 필요하면 TreeSet이나 LinkedHashSet 사용 |
1. HashSet의 내부 구조
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
}
- HashSet은 내부적으로 HashMap을 이용한다.
- 실제로 값을 key로 넣고, value는 의미 없는 더미 객체 PRESENT이다.
- 따라서, HashSet.add(E e) → 내부적으로 map.put(e, PRESENT) 호출한다.
2. 저장 시 내부 동작 과정
예 : set.add("apple") 호출 시
- "apple" 객체의 hashCode() 값 계산
- 해당 해시코드로 해시 버킷(bucket) 결정
- 같은 버킷에 이미 있는 값들과 equals() 비교
- 이미 동일한 값이 있으면 추가하지 않음
- 없다면 HashMap에 저장 → put("apple", PRESENT)

3. 중복 제거 메커니즘
중복 여부를 다음 두 메서드로 판단한다.
| hashCode() | 같은 해시값인지 확인 (빠른 후보 필터링) |
| equals() | 실제 값이 같은지 확인 (정밀 검사) |
- 중복 제거 조건
- 둘 다 같아야 동일 객체(중복)로 보고 추가하지 않음.
A.hashCode() == B.hashCode()
A.equals(B) == true
4. HashSet이 효율적인 중복 체크를 할 수 있는 이유
- HashSet이 효율적인 중복 체크를 할 수 있는 이유는 내부적으로 HashMap을 기반으로 구현되어 있고, 해시 함수를 활용하여 빠르게 데이터의 존재 여부를 판단하기 때문이다.
1. 해시 함수의 활용
- hashCode()를 통해 객체를 해시값으로 변환
- 해시값으로 배열의 특정 인덱스(버킷)를 빠르게 결정
- 시간복잡도: O(1) - 배열 인덱스 접근
2. 분산 저장 구조
- 해시값에 따라 데이터를 여러 버킷에 분산 저장
- 전체 데이터를 순회할 필요 없이 특정 버킷만 확인
- 충돌이 적을수록 더 효율적
3. 2단계 검증 시스템
- 1단계: hashCode() - 빠른 버킷 탐색
- 2단계: equals() - 정확한 동등성 비교
- 해시 충돌 시에만 equals() 비교 수행
4. 리스트 vs HashSet 비교
- ArrayList: 중복 체크 시 전체 순회 필요 → O(n)
- HashSet: 해시 기반 직접 접근 → O(1) 평균
| 요소 | 설명 |
| 해시 함수 사용 | hashCode()를 이용해 위치를 빠르게 계산함으로써 탐색 시간을 단축 |
| 버킷 구조(Hash Bucket) | 충돌이 발생해도 연결 리스트나 트리 구조로 빠르게 비교 가능 |
| equals() 비교 최소화 | 같은 해시값만 비교하므로 전체 요소와 비교하지 않음 |
| 시간 복잡도 평균 O(1) | add(), contains() 모두 평균적으로 O(1)의 성능 |

나만의 언어로 정리해보기
HashSet이란?
중복X, 순서X 컬렉션
중복 제거 메커니즘
해시 코드로 같은해시 값인지 확인, equals로 실제 값이 같은지 확인. 해시코드와 equals 둘 다 같아야 중복으로 보고 추가하지 않음.
내부적으로 HashMap을 기반으로 구현되어 있고 해시 함수를 활용하여 빠르게 데이터의 존재 여부를 판단하기 때문에 중복 체크를 효율적으로 할 수 있다.
O(n)과 O(log n)의 성능 차이를 실생활 예시를 들어 설명하고, 데이터의 크기가 1백만 개일 때 각각 대략 몇 번의 연산이 필요한지 비교해주세요.
O(n)과 O(log n)은 알고리즘의 시간 복잡도(Time Complexity)를 나타내는 표현이다. 이걸 이해하면 코드가 얼마나 빠르게 실행되는지, 데이터가 커졌을 때 얼마나 느려질지를 예측할 수 있다.
- 시간 복잡도(Time Complexity)란?
- 입력 크기(n)가 커질수록 실행 시간이 어떻게 증가하는지를 수학적으로 표현한 것이다.
- 정확한 시간(초)을 말하지 않고, 성장 속도(크게 늘어나는 정도)를 말한다.
- O(n) - 선형 시간
- 의미: 입력이 n개라면, n번 작업이 필요
- 예시
for (int i = 0; i < n; i++) {
System.out.println(i);
}
- 입력이 10이면 10번, 100이면 100번 반복
- 시간 증가 = 데이터 증가에 정비례
- 예) 리스트 전체를 순서대로 탐색하는 경우 (선형 탐색)
- O(log n) - 로그 시간
- 의미: 입력이 n개라면, 로그(log n) 만큼만 작업이 필요
- 보통 로그는 2를 밑으로 하는 로그(log₂ n) 를 의미
- 예시
int left = 0, right = n - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (target == arr[mid]) break;
else if (target < arr[mid]) right = mid - 1;
else left = mid + 1;
}
- 입력이 1,000개여도 약 10번만 비교하면 끝난다.
- 예) 정렬된 배열에서 이진 탐색(Binary Search)
O(n)과 O(log n) 비교
| n(입력 크기) | O(n) (선형 시간) | O(log n) (로그 시간) |
| 10 | 10번 | 4번 |
| 1,000 | 1,000번 | 10번 |
| 1,000,000 | 1,000,000번 | 20번 |
- O(n) : 전체 데이터를 하나씩 확인 (선형 탐색 등)
- O(log n) : 매번 절반씩 나누며 탐색 (이진 탐색 등)
예시: 물건에서 특정 상품 찾기
1) O(n): 선형 탐색 - 마트에서 무작위로 진열된 물건 찾기
- 상황: 마트 진열대에 100개의 상품이 무작위로 진열되어 있고, 특정한 물건(예: “꿀사과”)을 찾는다고 가정.
- 방법: 첫 번째 상품부터 하나씩 모두 확인하면서 찾기
- 최악의 경우: 100개 전부 확인 → 100번 비교 → O(n)
→ 물건이 10배 많아지면 탐색 시간도 10배 걸림
2) O(log n): 이진 탐색 - 정렬된 도서관에서 책 찾기
- 상황: 도서관 책장이 가나다순으로 정렬되어 있고, “이순신 전기” 책을 찾는다고 가정
- 방법: 중간 책을 펼쳐서 앞인지 뒤인지 판단 → 절반 제거 → 반복
- 총 1024권이 있다면, 최대 10번(=log₂1024)만에 찾을 수 있음
→ 책이 10배 늘어나도 (10240권) → 14번 정도 비교 → O(log n)
나만의 언어로 정리해보기
O(n)과 O(log n)의 성능 차이
O(n), O(log n) : 알고리즘의 시간 복잡도(Time Complexity)
O(n) : 입력이 n개면 n번의 작업 ex)선형 탐색
O(log n) : 입력이 n개면 log n번의 작업 ex)이진 탐색
데이터 크기가 1백만 개일 때
O(n) : 1백만 번 탐색
O(log n) : 20번 탐색
'코드잇 BE스프린트' 카테고리의 다른 글
| 위클리 페이퍼 - 5주차 (0) | 2026.04.01 |
|---|---|
| 위클리 페이퍼 - 4주차 (0) | 2026.03.31 |
| 위클리 페이퍼 - 2주차 (0) | 2026.03.17 |
| 위클리 페이퍼 - 1주차 (1) | 2026.03.16 |
| 코드잇 스프린트 고급 프로젝트(Mopl) 개발 회고록 (0) | 2025.12.17 |