From 40099a2b59577170c8e9c8cd40dd0e3489d9674d Mon Sep 17 00:00:00 2001 From: gyusuk Date: Sat, 3 Aug 2019 15:22:17 +0900 Subject: [PATCH] add Hash Table --- ...54\355\230\204\355\225\230\352\270\260.md" | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 "Algorithm/Hash Table \352\265\254\355\230\204\355\225\230\352\270\260.md" diff --git "a/Algorithm/Hash Table \352\265\254\355\230\204\355\225\230\352\270\260.md" "b/Algorithm/Hash Table \352\265\254\355\230\204\355\225\230\352\270\260.md" new file mode 100644 index 00000000..8a6f00dc --- /dev/null +++ "b/Algorithm/Hash Table \352\265\254\355\230\204\355\225\230\352\270\260.md" @@ -0,0 +1,332 @@ +# Hash Table 구현하기 + +> 알고리즘 문제를 풀기위해 필수적으로 알아야 할 개념 + +브루트 포스(완전 탐색)으로는 시간초과에 빠지게 되는 문제에서는 해시를 적용시켜야 한다. + +
+ +[연습 문제 링크]() + +
+ +N(1~100000)의 값만큼 문자열이 입력된다. + +처음 입력되는 문자열은 "OK", 들어온 적이 있던 문자열은 "문자열+index"로 출력하면 된다. + +ex) + +##### Input + +``` +5 +abcd +abc +abcd +abcd +ab +``` + +##### Output + +``` +OK +OK +abcd1 +abcd2 +OK +``` + +
+ +문제를 이해하는 건 쉽다. 똑같은 문자열이 들어왔는지 체크해보고, 들어온 문자열은 인덱스 번호를 부여해서 출력해주면 된다. + +
+ +하지만, 현재 N값은 최대 10만이다. 브루트 포스로 접근하면 N^2이 되므로 100억번의 연산이 필요해서 시간초과에 빠질 것이다. 따라서 **'해시 테이블'**을 이용해 해결해야 한다. + +
+ +입력된 문자열 값을 해시 키로 변환시켜 저장하면서 최대한 시간을 줄여나가도록 구현해야 한다. + +이 문제는 해시 테이블을 알고리즘에서 적용시켜보기 위해 연습하기에 아주 좋은 문제 같다. 특히 삼성 상시 SW역량테스트 B형을 준비하는 사람들에게 추천하고 싶은 문제다. 해시 테이블 구현을 연습하기 딱 좋다. + +
+ +
+ +#### **해시 테이블 구현** + +해시 테이블은 탐색을 최대한 줄여주기 위해, input에 대한 key 값을 얻어내서 관리하는 방식이다. + +현재 최대 N 값은 10만이다. 이차원 배열로 1000/100으로 나누어 관리하면, 더 효율적일 것이다. + +충돌 값이 들어오는 것을 감안해 최대한 고려해서, 나는 두번째 배열 값에 4를 곱해서 선언한다. + +
+ +``` + +key 값을 얻어서 저장할 때, 서로다른 문자열이라도 같은 key 값으로 들어올 수 있다. +(이것이 해시에 대한 이론을 배울 때 나오는 충돌 현상이다.) + +충돌이 일어나는 것을 최대한 피해야하지만, 무조건 피할 수 있다는 보장은 없다. 그래서 두번째 배열 값을 조금 넉넉히 선언해두는 것이다. + +``` + +이를 고려해 final 값으로 선언한 해시 값은 아래와 같다. + +```java +static final int HASH_SIZE = 1000; +static final int HASH_LEN = 400; +static final int HASH_VAL = 17; // 소수로 할 것 +``` + +HASH_VAL 값은 우리가 input 값을 받았을 때 해당하는 key 값을 얻을 때 활용한다. + +최대한 input 값들마다 key 값이 겹치지 않기 위해 하기 위해서는 소수로 선언해야한다. (그래서 보통 17, 19, 23으로 선언하는 것 같다.) + +
+ +key 값을 얻는 메소드 구현 방법은 아래와 같다. ( 각자 사람마다 다르므로 꼭 이게 정답은 아니다 ) + +```java +public static int getHashKey(String str) { + + int key = 0; + + for (int i = 0; i < str.length(); i++) { + key = (key * HASH_VAL) + str.charAt(i); + } + + if(key < 0) key = -key; // 만약 key 값이 음수면 양수로 바꿔주기 + + return key % HASH_SIZE; + +} +``` + +input 값을 매개변수로 받는다. 만약 string 값으로 들어온다고 가정해보자. + +string 값의 한글자(character)마다 int형 값을 얻어낼 수 있다. 이를 활용해 string 값의 length만큼 반복문을 돌면서, 그 문자열만의 key 값을 만들어내는 것이 가능하다. + +우리는 이 key 값을 배열 인덱스로 활용할 것이기 때문에 음수면 안된다. 만약 key 값의 결과가 음수면 양수로 바꿔주는 조건문이 필요하다. + +
+ +마지막으로 return 값으로 key를 우리가 선언한 HASH_SIZE로 나눈 나머지 값을 얻도록 한다. + +현재 계산된 key 값은 매우 크기 때문에 HASH_SIZE로 나눈 나머지 값으로 key를 활용할 것이다. (이 때문에 데이터가 많으면 많을수록 충돌되는 key값이 존재할 수 밖에 없다. - 우리는 최대한 충돌을 줄이면서 최적화시키기 위한 것..!) + +
+ +이제 우리는 input으로 받은 string 값의 key 값을 얻었다. + +해당 key 값의 배열에서 이 값이 들어온 적이 있는지 확인하는 과정이 필요하다. + +
+ +이제 우리는 모든 곳을 탐색할 필요없이, 이 key에 해당하는 배열에서만 확인하면 되므로 시간이 엄~~청 절약된다. + +
+ +```java +static int[][] data = new int[HASH_SIZE][HASH_LEN]; + +string str = "apple"; + +int key = getHashKey(str); // apple에 대한 key 값 얻음 + +data[key][index]; // 이곳에 apple을 저장해서 관리하면 찾는 시간을 줄일 수 있는 것 +``` + +여기서 HASH_SIZE가 1000이었고, 우리가 key 값을 리턴할 때 1000으로 나눈 나머지로 저장했으므로 이 안에서만 key 값이 들어오게 된다는 것을 이해할 수 있다. + +
+ +ArrayList로 2차원배열을 관리하면, 그냥 계속 추가해주면 되므로 구현이 간편하다. + +하지만 삼성 sw 역량테스트 B형처럼 라이브러리를 사용하지 못하는 경우에는, 배열로 선언해서 추가해나가야 한다. 또한 ArrayList 활용보다 배열이 훨씬 시간을 줄일 수 있기 때문에 되도록이면 배열을 이용하도록 하자 + +
+ +여기서 끝은 아니다. 이제 우리는 단순히 key 값만 받아온 것 뿐이다. + +해당 key 배열에서, apple이 들어온적이 있는지 없는지 체크해야한다. (문제에서 들어온적 있는건 숫자를 붙여서 출력해야 했기 때문이다.) + +
+ +데이터의 수가 많으면 key 배열 안에서 다른 문자열이라도 같은 key로 저장되는 값들이 존재할 것이기 때문에 해당 key 배열을 돌면서 apple과 일치하는 문자열이 있는지 확인하는 과정이 필요하다. + +
+ +따라서 key 값을 매개변수로 넣고 문자열이 들어왔던 적이 있는지 체크하는 함수를 구현하자 + +```java +public static int checking(int key) { + + int len = length[key]; // 현재 key에 담긴 수 (같은 key 값으로 들어오는 문자열이 있을 수 있다) + + if(len != 0) { // 이미 들어온 적 있음 + + for (int i = 0; i < len; i++) { // 이미 들어온 문자열이 해당 key 배열에 있는지 확인 + if(str.equals(s_data[key][i])) { + data[key][i]++; + return data[key][i]; + } + } + + } + + // 들어온 적이 없었으면 해당 key배열에서 문자열을 저장하고 길이 1 늘리기 + s_data[key][length[key]++] = str; + + return -1; // 처음으로 들어가는 경우 -1 리턴 +} +``` + +length[] 배열은 HASH_SIZE만큼 선언된 것으로, key 값을 얻은 후, 처음 들어온 문자열일 때마다 숫자를 1씩 늘려서 해당 key 배열에 몇개의 데이터가 저장되어있는지 확인하는 공간이다. + +
+ +**우리가 출력해야하는 조건은 처음 들어온건 "OK" 다시 또 들어온건 "data + 들어온 수"였다.** + +
+ +- "OK"로 출력해야 하는 조건 + + > 해당 key의 배열 length가 0일 때는 무조건 처음 들어오는 데이터다. + > + > 또한 1이상일 때, 그 key 배열 안에서 만약 apple을 찾지 못했다면 이 또한 처음 들어오는 데이터다. + +
+ +- "data + 들어온 수"로 출력해야 하는 조건 + + > 만약 1이상일 때 key 배열에서 apple 값을 찾았다면 이제 'apple+들어온 수'를 출력하도록 구현해야한다. + +
+ +그래서 나는 3개의 배열을 선언해서 활용했다. + +```java +static int[][] data = new int[HASH_SIZE][HASH_LEN]; +static int[] length = new int[HASH_SIZE]; +static String[][] s_data = new String[HASH_SIZE][HASH_LEN]; +``` + +data[][] 배열 : input으로 받는 문자열이 들어온 수를 저장하는 곳 + +length[] 배열 : key 값마다 들어온 수를 저장하는 곳 + +s_data[][] 배열 : input으로 받은 문자열을 저장하는 곳 + +
+ +진행 과정을 설명하면 아래와 같다. (apple - banana - abc - abc 순으로 입력되고, apple과 abc의 key값은 5로 같다고 가정하겠다.) + +
+ +``` +1. apple이 들어옴. key 값을 얻으니 5가 나옴. length[5]는 0이므로 처음 들어온 데이터임. length[5]++하고 "OK"출력 + +2. banana가 들어옴. key 값을 얻으니 3이 나옴. length[3]은 0이므로 처음 들어온 데이터임. length[3]++하고 "OK"출력 + +<< 중요 >> +3. abc가 들어옴. key 값을 얻으니 5가 나옴. length[5]는 0이 아님. 해당 key 값에 누가 들어온적이 있음. +length[5]만큼 반복문을 돌면서 s_data[key]의 배열과 abc가 일치하는 값이 있는지 확인함. 현재 length[5]는 1이고, s_data[key][0] = "apple"이므로 일치하는 값이 없기 때문에 length[5]를 1 증가시키고 s_data[key][length[5]]에 abc를 넣고 "OK"출력 + +<< 중요 >> +4. abc가 들어옴. key 값을 얻으니 5가 나옴. length[5] = 2임. +s_data[key]를 2만큼 반복문을 돌면서 abc가 있는지 찾음. 1번째 인덱스 값에는 apple이 저장되어 있고 2번째 인덱스 값에서 abc가 일치함을 찾았음!! +따라서 해당 data[key][index] 값을 1 증가시키고 이 값을 return 해주면서 메소드를 끝냄 +→ 메인함수에서 input으로 들어온 abc 값과 리턴값으로 나온 1을 붙여서 출력해주면 됨 (abc1) +``` + +
+ +진행과정을 통해 어떤 방식으로 구현되는지 충분히 이해할 수 있을 것이다. + +
+ +#### 전체 소스코드 + +```java +package CodeForces; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +public class Solution { + + static final int HASH_SIZE = 1000; + static final int HASH_LEN = 400; + static final int HASH_VAL = 17; // 소수로 할 것 + + static int[][] data = new int[HASH_SIZE][HASH_LEN]; + static int[] length = new int[HASH_SIZE]; + static String[][] s_data = new String[HASH_SIZE][HASH_LEN]; + static String str; + static int N; + + public static void main(String[] args) throws Exception { + + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + StringBuilder sb = new StringBuilder(); + + N = Integer.parseInt(br.readLine()); // 입력 수 (1~100000) + + for (int i = 0; i < N; i++) { + + str = br.readLine(); + + int key = getHashKey(str); + int cnt = checking(key); + + if(cnt != -1) { // 이미 들어왔던 문자열 + sb.append(str).append(cnt).append("\n"); + } + else sb.append("OK").append("\n"); + } + + System.out.println(sb.toString()); + } + + public static int getHashKey(String str) { + + int key = 0; + + for (int i = 0; i < str.length(); i++) { + key = (key * HASH_VAL) + str.charAt(i) + HASH_VAL; + } + + if(key < 0) key = -key; // 만약 key 값이 음수면 양수로 바꿔주기 + + return key % HASH_SIZE; + + } + + public static int checking(int key) { + + int len = length[key]; // 현재 key에 담긴 수 (같은 key 값으로 들어오는 문자열이 있을 수 있다) + + if(len != 0) { // 이미 들어온 적 있음 + + for (int i = 0; i < len; i++) { // 이미 들어온 문자열이 해당 key 배열에 있는지 확인 + if(str.equals(s_data[key][i])) { + data[key][i]++; + return data[key][i]; + } + } + + } + + // 들어온 적이 없었으면 해당 key배열에서 문자열을 저장하고 길이 1 늘리기 + s_data[key][length[key]++] = str; + + return -1; // 처음으로 들어가는 경우 -1 리턴 + } + +} +``` +