해시 알고리즘 쉽게 배우기
배열 사용
배열의 좋은 점 : 인덱스의 위치를 사용해서 데이터를 찾을 때 O(1)로 매우 빠른 특징을 가지고 있다. O(1)
배열의 안좋은 점: 배열을 검색할 때 배열에 있는 데이터를 모두 비교해야 함 O(n)
Index 사용
데이터의 값을 인덱스 번호로 검색을 함으로써 O(n)
의 검색 연산을 O(1)
의 검색 연산으로 바꾼다
문제
만약에 데이터가 0~99까지 늘어난다면 인덱스도 0~99까지 사용해야한다
크기 100의 배열이 필요함
예를들어 [1, 2, 6, 99] 의 값을 넣으면
[null, 1, 2, null, null, null, 6, 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,
null, null, null, null, null, null, null, null, 99]
한계
매우 빠른 속도로 검색은 가능하나 O(1), 메모리 공간이 너무 많이 낭비된다
⟪나머지 연산 (Hash Index)⟫
배열의 크기 (CAPACITY) = 10
- 1 % 10 = 1
- 2 % 10 = 2
- 5 % 10 = 5
- 14 % 10 = 4
- 99 % 10 = 9
데이터 저장할 때
- 해시 인덱스를 먼저 구한 후, 해시 인덱스 위치에 데이터 저장
- arr[hashIndex] = value
- 값을 저장하는데 O(1)로 빠른 성능 → 해시 인덱스 생성 O(1) + 해시 인덱스를 사용해 배열에 값 저장 O(1)
데이터 조회할 때
- 해시 인덱스를 구하고, 배열에 해시 인덱스를 대입해서 값을 조회
- int value = arr[hashIndex]
-
배열의 인덱스를 사용해서 하나의 값을 찾는데 O(1)로 빠른 성능 제공
-
*코드 구현*
public class Set { 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(3) = " + hashIndex(3)); System.out.println("hashIndex(5) = " + hashIndex(5)); System.out.println("hashIndex(14) = " + hashIndex(14)); System.out.println("hashIndex(99) = " + hashIndex(99)); Integer[] inputArray = new Integer[CAPACITY]; add(inputArray, 1); add(inputArray, 3); add(inputArray, 5); add(inputArray, 16); add(inputArray, 99); System.out.println("inputArray = " + Arrays.toString(inputArray)); //검색 int searchValue = 99; int hashIndex = hashIndex(searchValue); System.out.println("searchValue hashIndex = " + hashIndex); Integer result = inputArray[hashIndex]; System.out.println(result); } 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) = 3 hashIndex(5) = 5 hashIndex(14) = 4 hashIndex(99) = 9 inputArray = [null, 1, null, 3, null, 5, 16, null, null, 99] searchValue hashIndex = 9 99
-
문제
만약 같은 해시 인덱스가 나오면?
1 % 10 = 1
11 % 10 = 1
해시 충돌
을 방지 해야함
⟪해시 충돌 방지⟫
CAPACITY를 값의 입력 범위만큼 키우면 안되나?
→ 충돌은 발생 안하겠지만, 메모리 낭비 겁나 심함
해결
-
설명
데이터를 등록할 때 먼저 해시 인덱스(hashIndex)를 구한다. 해시 인덱스로 배열의 인덱스를 찾는다. 배열에는 연결 리스트가 들어있다.
해시 인덱스를 통해 바구니들 사이에서 바구니인 연결 리스트를 하나 찾은 것이다. 셋은 중복된 값을 저장하지 않는다. 따라서 바구니에 값을 저장하기 전에
contains()
를 사용해서 중복 여부를 확인한다. 만약 바구니에 같은 데이터가 없다면 데이터를 저장한다.
-
설명
해시 인덱스로 배열의 인덱스를 찾는다. 여기에는 연결 리스트가 들어있다. 연결 리스트의
bucket.contains(searchValue)
메서드를 사용해서 찾는 데이터가 있는지 확인한다.연결 리스트의
contains()
는 모든 항목을 다 순회하기 때문에 O(n)의 성능이다. 하지만 해시 충돌이 발생하지 않으면 데이터가 1개만 들어있기 때문에 O(1)의 성능을 제공한다. -
9번 인덱스를 찾는다 → 값을 하나씩 비교한다
😵💫BUT, 최악의 경우
- 9, 19, 29, 99를 저장할 때 → 모두 해시 인덱스 9가 됨
- 저장한 데이터의 수 만큼 값을 반복해서 비교 → O(n)의 성능
-
*코드 예시*
public class Set { static final int CAPACITY = 10; public static void main(String[] args) { //{1, 2, 5, 99 ,9} // 배열안에 LinkedList를 넣어준다 -> LinkedList는 가끔 충돌남 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, 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); } public 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); } } public 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], [], [], [5], [], [], [], [99, 9]] buckets.contains(9) = true
-
충돌이 발생하면 데이터를 추가하거나 조회할 때, 연결 리스트 내부에서 O(n)의 추가 연산을 해야 해서 성능이 떨어짐.
🤗GOOD, 현실
-
정리
입력 값의 범위가 넓어도 실제 모든 값이 들어오지는 않기 때문에 배열의 크기를 제한하고, 나머지 연산을 통해 메모리가 낭비되는 문제도 해결할 수 있다. 해시 인덱스를 사용해서 O(1)의 성능으로 데이터를 저장하고, O(1)의 성능으로 데이터를 조회할 수 있게 되었다. 덕분에 자료 구조의 조회 속도를 비약적으로 향상할 수 있게 되었다.
HashCode()
HashCode() 메서드를 통해서 문자를 기반으로 고유한 숫자를 만들 수 있다 →
해시 코드
- 문자열을 해시 코드로 변경 → 고유한 정수 값 =
해시 코드
- 해시 코드를 사용해서 해시 인덱스 생성
-
정리
문자 데이터를 사용할 때도, 해시 함수를 사용해서 정수 기반의 해시 코드로 변환한 덕분에, 해시 인덱스를 사용할 수 있게 되었다. 따라서 문자의 경우에도 해시 인덱스를 통해 빠르게 저장하고 조회할 수 있다.
참고
https://night-knight.tistory.com/entry/🚀-파이썬의-set-함수와-list-함수의-in-함수-사용-시-시간복잡도
https://velog.io/@bada308/알고리즘-해시Hash
https://adjh54.tistory.com/490
https://velog.io/@mooh2jj/Hash의-개념-및-설명