해시 테이블

5 minute read

들어가기 전에

  1. 해시: 해시 테이블에서 입력값을 고정 크기 값으로 매핑하는 과정을 해싱이라고 한다.
  2. 해시 함수: 해시를 하는데 사용되는 함수. 성능이 좋은 해시 함수들의 특징은 다음과 같다.
    • 해시 함수 값 충돌의 최소화
    • 쉽고 빠른 연산
    • 해시 테이블 전체에 해시 값이 균일하게 분포
    • 사용할 키의 모든 정보를 이용하여 해싱
    • 해시 테이블 사용 효율이 높을 것
  3. 로드 팩터: 해시 테이블에 저장된 데이터 개수 n을 버킷의 개수 k로 나눈 것이다.
  4. 충돌: 해시 값이 같아져 동일한 공간에 같은 값이 선언되는 것이다. 충돌을 해결하기 위한 방법으로 두가지가 있다.
    • 개별 체이닝: 키의 해시 값을 계산해 배열의 인덱스를 구한다. 만약 인덱스가 같다면 연결 리스트로 연결한다.
  • 오픈 어드레싱: 충돌 발생시 탐사를 위해 빈 공간을 찾아나서는 방식이다. 개별 체이닝과는 다르게 모든 원소를 무한정 저장할 수 없으므로 모든 원소가 반드시 자신의 해시값과 일치하는 주소에 저장된다는 보장은 없다.

Question 28: 해시맵 디자인

put, get, remove 기능을 제공하는 해시맵을 디자인 하라

입력:
[“MyHashMap”, “put”, “put”, “get”, “get”, “put”, “get”, “remove”, “get”]
[[], [1, 1], [2, 2], [1], [3], [2, 1], [2], [2], [2]]

출력:
[null, null, null, 1, -1, null, 1, null, -1]

내 풀이

해시맵에 대한 걔념이 확실하게 잡혀있지 않은 상태에서 일단 풀이를 보류하였다.


풀이 1: 개별 체이닝 방식을 이용한 해시 테이블 구현

class ListNode:
    def __init__(self, key=None, value=None):
        self.key = key
        self.value = value
        self.next = None


class MyHashMap:
    # 초기화
    def __init__(self):
        self.size = 1000
        self.table = collections.defaultdict(ListNode)

    # 삽입
    def put(self, key: int, value: int) -> None:
        index = key % self.size
        # 인덱스에 노드가 없다면 삽입 후 종료
        if self.table[index].value is None:
            self.table[index] = ListNode(key, value)
            return

        # 인덱스에 노드가 존재하는 경우 연결 리스트 처리
        p = self.table[index]
        while p:
            if p.key == key:
                p.value = value
                return
            if p.next is None:
                break
            p = p.next
        p.next = ListNode(key, value)

    # 조회
    def get(self, key: int) -> int:
        index = key % self.size
        if self.table[index].value is None:
            return -1

        # 노드가 존재할때 일치하는 키 탐색
        p = self.table[index]
        while p:
            if p.key == key:
                return p.value
            p = p.next
        return -1

    # 삭제
    def remove(self, key: int) -> None:
        index = key % self.size
        if self.table[index].value is None:
            return

        # 인덱스의 첫 번째 노드일때 삭제 처리
        p = self.table[index]
        if p.key == key:
            self.table[index] = ListNode() if p.next is None else p.next
            return

        # 연결 리스트 노드 삭제
        prev = p
        while p:
            if p.key == key:
                prev.next = p.next
                return
            prev, p = p, p.next

기억해야 할 함수

  1. collections.defaultdict(): 존재하지 않는 키를 조회할 경우 자동으로 디폴트를 생성해주는 함수이다.

중요 내용

  1. 파이썬은 오픈 어드레싱 방법을 사용하나 이 풀이 방법은 개별 체이닝 방법으로 풀었다.
  2. 처음에 해시맵을 만들때 선언하는 해시맵의 크기(버켓의 크기)는 중요하지 않다. 만약 데이터를 넣을 공간이 부족하더라도 같은 인덱스 자리에 연결 리스트로 연결하여 무제한으로 넣을 수 있기 때문이다.
  3. 만약 연결 리스트를 사용하지 않는다면 넣을 수 있는 데이터의 갯수가 한정되기 때문에 처음에 사이즈 선언을 충분하게 하는 것이 중요하다.

Question 29: 보석과 돌

J는 보석이며, S는 갖고 있는 돌이다. S에 보석이 몇 개가 들어있을까?

입력: jewels = “aA”, stones = “aAAbbbb”

출력: 3


내 풀이

class Solution:
    def numJewelsInStones(self, jewels: str, stones: str) -> int:
        jewel_list = [jewel for jewel in jewels]
        stones_list: Deque = collections.deque([stone for stone in stones])
        jewels_count = 0    
        
        while len(stones_list) != 0:
            if stones_list.popleft() in jewel_list:
                jewels_count += 1
        return jewels_count

  • 보석과 돌을 리스트로 변환한 뒤 큐 방법을 이용해서 풀었다.
  • 돌 리스트는 데크를 선언하여 큐 방법을 이용하여 풀 수 있도록 하였다.
  • 해시 테이블 방법은 사용하지 않았다.

결과

Runtime: 58 ms, faster than 14.29% of Python3 online submissions for Jewels and Stones. Memory Usage: 13.9 MB, less than 80.19% of Python3 online submissions for Jewels and Stones.

풀이 1: 해시 테이블을 이용한 풀이

class Solution:
    def numJewelsInStones(self, J: str, S: str) -> int:
        freqs = {}
        count = 0

        # 돌(S)의 빈도 수 계산
        for char in S:
            if char not in freqs:
                freqs[char] = 1
            else:
                freqs[char] += 1

        # 보석(J)의 빈도 수 합산
        for char in J:
            if char in freqs:
                count += freqs[char]

        return count

중요 내용

  1. 돌 문자열에 들어있는 문자의 횟수를 나타낸 딕셔너리에서 보석 문자열에 나타나는 문자가 몇번 나타나는지 카운팅 하는 방법으로 풀었다.
  2. 딕셔너리 또한 가장 기본적인 해시 테이블의 형태라고 할 수 있다.

풀이 2: defaultdict를 이용한 비교 생략

class Solution:
    def numJewelsInStones(self, J: str, S: str) -> int:
        freqs = collections.defaultdict(int)
        count = 0

        # 비교 없이 돌(S) 빈도 수 계산
        for char in S:
            freqs[char] += 1

        # 비교 없이 보석(J) 빈도 수 합산
        for char in J:
            count += freqs[char]

        return count

중요 내용

  1. defaultdict를 사용하면 비교 없이 바로 딕셔너리를 생성할 수 있다.

풀이 3: counter로 계산 생략

class Solution:
    def numJewelsInStones(self, J: str, S: str) -> int:
        freqs = collections.Counter(S)  # 돌(S) 빈도 수 계산
        count = 0

        # 비교 없이 보석(J) 빈도 수 합산
        for char in J:
            count += freqs[char]

        return count

중요 내용

  1. defaultdict에 반복문을 써서 돌 문자열에 대한 딕셔너리를 직접 만드는 방법이 아닌 counter함수를 사용하여 단 한 줄로 만들 수 있었다.

풀이 4: 파이썬다운 방식

class Solution:
    def numJewelsInStones(self, J: str, S: str) -> int:
        return sum(s in J for s in S)

중요 내용

  1. 리스트 컴프리헨션을 이용한 반복문에 함수를 적용할 때는 괄호[]를 생략할 수 있다.

Question 30: 중복 문자 없는 가장 긴 부분 문자열 (재풀이)

중복 문자가 없는 가장 긴 부분 문자열의 길이를 리턴하라.

입력: s = “abcabcbb”

출력: 3


내 풀이

결국 풀지 못하였다.

풀이 1: 슬라이딩 윈도우오 투 포인터로 사이즈 조절

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
    
        # 키가 char, 값이 value 가 되는 딕셔너리를 생성함
        used = {}
        max_length = start = 0
        for index, char in enumerate(s):
            # 이미 등장했던 문자라면 `start` 위치 갱신
            if char in used and start <= used[char]:
                start = used[char] + 1
            else:  # 최대 부분 문자열 길이 갱신
                max_length = max(max_length, index - start + 1)

            # 현재 문자의 위치 삽입
            used[char] = index

        return max_length

중요 내용

  • 슬라이딩 윈도우를 옮기면서 이미 등장했던 문자라면 start 위치를 갱신하며 푸는 방식이다.
  • 배열 챕터에 나왔던 슬라이딩 윈도우와 투 포인터를 응용한 방법이다.

Question 31: 상위 K 빈도 요소 (재풀이)

상위 k번 이상 등장하는 요소를 추출하라

입력: nums = [1,1,1,2,2,3], k = 2

출력: [1,2]


내 풀이

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        nums_count = collections.Counter(nums)
        return sorted(nums_count.keys(), key=lambda x:nums_count[x])[-k:]

  • 이 문제는 상당히 쉽게 접근하였다.
  • counter() 함수를 사용하여 각 숫자가 나타나는 횟수를 나타내는 딕셔너리를 만들었다.
  • sorted() 에 람다 함수를 사용하여 숫자가 나타나는 횟수를 기준으로 숫자를 정렬하였다.

결과

Runtime: 145 ms, faster than 46.25% of Python3 online submissions for Top K Frequent Elements. Memory Usage: 18.6 MB, less than 90.78% of Python3 online submissions for Top K Frequent Elements.

풀이 1: 해시 테이블을 이용한 풀이

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        freqs = collections.Counter(nums)
        freqs_heap = []
        for f in freqs:
            heapq.heappush(freqs_heap, (-freqs[f], f))
        
        topk = list()
        for _ in range(k):
            topk.append(heapq.heappop(freqs_heap)[1])
            
        return topk

중요 내용

  1. 힙에 값을 삽입하는 방식은 다음과 같이 크게 두가지가 있다.
  • 리스트를 만든 후 heapify()를 적용한다.
  • 값을 삽입할 때 마다 매번 heappush()를 사용한다.
  1. 힙은 우선순위 큐를 구현할때 사용된다. 따라서 작은 값부터 차례로 정렬되서 값들이 저장되게 된다.

풀이 2: 파이썬다운 방식

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        return list(zip(*collections.Counter(nums).most_common(k)))[0]

중요 내용

  1. zip() 함수는 2개 이상의 시퀀스를 일대일 대응하는 새로운 튜플 시퀀스로 만드는 역할을 한다.
  2. *은 시퀀스 언패킹 연산자로, 시퀀스를 풀어 해친다. 반대로 언패킹 된 시퀀스에 사용하면 다시 패킹할 수 있다.

Comments