본문 바로가기

코딩테스트/알고리즘

예제로 배우는 알고리즘 - 해싱

카운팅

 

예제 - LeetCode 383. Ransom Note

https://leetcode.com/problems/ransom-note/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  magazine 알파벳을 이용하여 ransomNote를 만들 수 있는지 확인
  •  magazine의 각 문자는 한 번만 사용할 수 있음

 

2. 요구사항 분석

  • 입력
    • 1 <= ransomNote.length, magazine.length <= 100,000
    • 모든 문자는 소문자 알파벳
  • 출력
    • 가능하면 true, 불가능하면 false

 

 

3. 해결 아이디어

  • magazine의 알파벳 갯수를 배열에 저장
  • ransomNote 알파벳을 하나씩 확인하면 갯수를 줄여 나가기
  • 중간에 사용가능한 알파벳이 없다면 false 반환

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • magazine, ransomNote를 한번씩 반복하므로 O(n + m)
  • 공간 복잡도
    • 고정된 크기의 배열만 사용하므로 O(1)

 

5. 구현

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        // 1. 주어진 알파벳을 저장할 배열 할당
        int[] count = new int[26];

        // 2. 주어진 알파벳의 갯수를 계산
        for (char c : magazine.toCharArray()) {
            count[c - 'a']++;
        }

        // 3. 알파벳을 하나씩 사용
        for (char c : ransomNote.toCharArray()) {
            // 사용 가능한 알파벳이 없다면 false 반환
            if (count[c - 'a'] <= 0) {
                return false;
            }
            count[c - 'a']--;
        }

        return true;
    }
}

 

예제 - LeetCode 242. Valid Anagram

https://leetcode.com/problems/valid-anagram/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  두 문자열이 아나그램(문자 구성은 같고 순서만 다름)인지 확인

 

2. 요구사항 분석

  • 입력
    • 1 <= s.length, t.length <= 50,000
    • Follow up 조건에 맞춰 s,t는 유니코드 문자가 포함
  • 출력
    • 아나그램이면 true, 아니면 false

 

 

3. 해결 아이디어

  • s 문자열의 문자 갯수만큼 맵을 활용해 카운팅
  • t 문자열의 문자 갯수만큼 빼기
  • 남은 문자 갯수가 있는 경우 아나그램이 아님

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • 문자열을 하나씩 순회하므로 O(n)
  • 공간 복잡도
    • 문자열 크기의 맵을 사용하므로 O(n)

 

5. 구현

class Solution {
    public boolean isAnagram(String s, String t) {
        // 두 문자열의 길이가 다르면 불가능
        if (s.length() != t.length()) return false;

        // 1. 카운팅 맵 할당
        Map<Character, Integer> count = new HashMap<>();

        // 2. 문자열을 순회하며 s문자열의 문자는 1 더하고 t문자열의 문자는 1 빼기
        for (int i = 0; i < s.length(); i++) {
            count.put(s.charAt(i), count.getOrDefault(s.charAt(i), 0) + 1);
            count.put(t.charAt(i), count.getOrDefault(t.charAt(i), 0) - 1);
        }

        // 3. 값이 0이 아닌 문자가 존재하면 불가능
        for (int value : count.values()) {
            if (value != 0) return false;
        }

        return true;
    }
}

 

문자열 패턴 매칭

 

예제 - LeetCode 205. Isomorphic Strings

https://leetcode.com/problems/isomorphic-strings/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  • s의 문자들을 다른 문자로 치환하여 t를 만들 수 있는지 확인
  • 각 문자는 다른 단일 문자(자기 자신 포함)로만 치환 가능하고 서로 다른 두 문자는 같은 문자로 치환 불가능
  • 순서는 유지되어야 함

 

2. 요구사항 분석

  • 입력
    • 1 <= s.length <= 50,000
    • s.length = t.length
    • s, t는 아스키 문자
  • 출력
    • 가능하면 true, 불가능하면 false

 

 

3. 해결 아이디어

  • 문자를 순회하며 매핑
  • 다른 단어가 같은 문자로 치환될 수 없으므로 양방향 매핑으로 확인
  • 이미 매핑된 단어와 다른 문자와 매핑할 필요가 있다면 불가능

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • 두 문자열을 동시에 한번 순회하면 되므로 O(n)
  • 공간 복잡도
    • 최대 아스키코드 크기(256)의 맵을 사용하므로 O(1)

 

5. 구현

class Solution {
    public boolean isIsomorphic(String s, String t) {
        // 1. 양방향 매핑에 필요한 맵 할당, 두 문자가 같은 문자로 할당될 수 없으므로 양방향 확인 필요
        Map<Character, Character> StoT = new HashMap<>();
        Map<Character, Character> TtoS = new HashMap<>();

        // 2. 각 문자를 순환하면서 매핑
        for (int i = 0; i < s.length(); i++) {
            // 2-1) 현재 문자
            char c1 = s.charAt(i);
            char c2 = t.charAt(i);

            // 2-2) c1을 키로 c2를 매핑
            if (StoT.containsKey(c1)) {
                // 이미 매핑 되어 있는데 값이 다르다면 불가능
                if (StoT.get(c1) != c2) return false;
            } else {
                StoT.put(c1, c2);
            }

            // 2-3) c2를 키로 c1을 매핑
            if (TtoS.containsKey(c2)) {
                // 이미 매핑 되어 있는데 값이 다르다면 불가능
                if (TtoS.get(c2) != c1) return false;
            } else {
                TtoS.put(c2, c1);
            }
        }

        return true;
    }
}

 

예제 - LeetCode 290. Word Pattern

 

https://leetcode.com/problems/word-pattern/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  • pattern과 문자열 s가 주어졌을 때 문자열이 pattern을 따르는지 확인
  • 모든 문자는 하나의 고유 단어와 모든 단어는 하나의 고유 문자와 대응(일대일 대응)

 

2. 요구사항 분석

  • 입력
    • 1 <= pattern.length <= 300
    • 1 <= s.length <= 3,000
    • pattern은 소문자로만 이루어짐
    • s는 소문자로와 공백으로만 이루어짐
    • 공백으로 시작하거나 끝나지 않고 공백은 하나씩만 존재
  • 출력
    • pattern과 s가 일대일 대응이면 true, 아니면 false

 

 

3. 해결 아이디어

  • 문자열을 공백을 기준으로 문자로 나눔
  • pattern과 문자 배열을 양방향 매핑하면서 확인

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • 문자열을 한번 순회하면 되므로 O(n)
  • 공간 복잡도
    • 패턴과 문자열 길이만큼의 두가지 맵을 사용하므로 O(n)

 

5. 구현

class Solution {
    public boolean wordPattern(String pattern, String s) {
        // 1. 문자열을 공백을 기준으로 나누기
        String[] words = s.split(" ");

        // 2. 문자열과 패턴의 길이가 다르면 불가능
        if (pattern.length() != words.length) return false;

        // 3. 패턴의 글자와 문자를 매핑항 맵 할당
        Map<Character, String> charToWord = new HashMap<>();
        Map<String, Character> wordToChar = new HashMap<>();

        // 4. 패턴 및 문자열의 길이만큼 순회하며 양방향 매핑
        for (int i = 0; i < pattern.length(); i++) {
            // 4-1) 현재 글자 및 문자
            char c = pattern.charAt(i);
            String word = words[i];

            // 4-2) 글자를 문자로 매핑
            if (charToWord.containsKey(c)) {
                // 이미 매핑되어 있는데 다른 값이라면 불가능
                if (!charToWord.get(c).equals(word)) return false;
            } else {
                charToWord.put(c, word);
            }

            // 4-3) 문자를 글자로 매핑
            if (wordToChar.containsKey(word)) {
                // 이미 매핑되어 있는데 다른 값이라면 불가능
                if (!wordToChar.get(word).equals(c)) return false;
            } else {
                wordToChar.put(word, c);
            }
        }

        return true;
    }
}

 

그룹화

 

예제 - LeetCode 49. Group Anagrams

https://leetcode.com/problems/group-anagrams/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  • 아나그램끼리 그룹화하기

 

2. 요구사항 분석

  • 입력
    • 1 <= strs.length <= 10,000
    • 0 <= strs[i].length <= 100
    • strs[i]는 소문자 알파벳만 포함
  • 출력
    • 아나그램끼리 묶인 리스트

 

 

3. 해결 아이디어

  • 각각의 문자열을 정렬하면 같은 아나그램끼리 같은 값이 나옴
  • 해당 값을 키로 하는 맵에 문자열들을 저장 후 리스트로 반환

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • 문자 갯수만큼 정렬을 진행하므로 O(n * klogk)
  • 공간 복잡도
    • 문자열의 길이만큼의 맵과 그 값으로 리스트를 사용하므로 O(n * k)

 

5. 구현

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // 1. 그룹화 할 맵 할당
        Map<String, List<String>> map = new HashMap<>();

        // 2. 문자열을 순회하면서 맵에 그룹화
        for (String s : strs) {
            // 2-1) 문자를 정렬하여 키 찾기
            char[] chars = s.toCharArray();
            Arrays.sort(chars);
            String key = new String(chars);

            // 2-2) 키에 문자를 값으로 넣기
            map.putIfAbsent(key, new ArrayList<>());
            map.get(key).add(s);
        }

        // 3. 맵에 값들을 리스트로 반환
        return new ArrayList<>(map.values());
    }
}

 

인덱스 매핑

 

예제 - LeetCode 1. Two Sum

https://leetcode.com/problems/two-sum/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  배열에서 두 수를 합해 target이 되는 두 인덱스 구하기
  • 같은 요소를 두 번 사용할 수 없으며, 정확히 하나의 해만 존재

 

2. 요구사항 분석

  • 입력
    • 2 <= nums.length <= 10,000
    • -1,000,000,000 <= nums[i], target <= 1,000,000,000
  • 출력
    • 인덱스 배열

 

3. 해결 아이디어

  • 배열의 값을 키로 인덱스를 값으로 맵에 해싱
  • target - 현재값이 맵에 키로 있으면 두 인덱스를 출력

 

4. 시간 복잡도 및 공간 복잡도 확인

  • 시간 복잡도
    • 배열을 한번 순회하므로 O(n)
  • 공간 복잡도
    • 배열 길이의 맵을 사용하므로 O(n)

 

5. 구현

class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 1. 인덱스를 매핑할 맵 할당
        Map<Integer, Integer> map = new HashMap<>();

        // 2. 배열을 순회하며 인덱스 매핑 + 답 찾기
        for (int i = 0; i < nums.length; i++) {
            // 2-1) target을 만들기 위해 필요한 값
            int complement = target - nums[i];

            // 2-2) 맵에 해당 값이 있다면 두 인덱스를 출력
            if (map.containsKey(complement)) {
                return new int[] { map.get(complement), i };
            }

            // 2-3) 현재 값 인덱스 매핑
            map.put(nums[i], i);
        }

        // 정답이 없는 경우 -> 현재 문제에는 답이 존재
        return new int[]{-1, -1};
    }
}

 

특정 값 조회

 

예제 - LeetCode 202.  Happy Number

https://leetcode.com/problems/happy-number/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  행복한 수 찾기
  • 행복한 수는 양의 정수에서 시작해서 각 자릿수의 제곱합을 구해가며 결과가 1인 수

 

2. 요구사항 분석

  • 입력
    • 1 <= n <= int의 양의 범위
  • 출력
    • 행복한 수이면 true, 아니면 false

 

3. 해결 아이디어

  • 제곱합을 하면서 각 수를 저장
  • 중복된 수(이미 제곱합을 구했던)가 다시 나오면 사이클이므로 불가능

4. 시간 복잡도 및 공간 복잡도 확인

  • 시간 복잡도
    • 반복되는 수가 일정 크기(약 243) 이하이므로 O(1)
  • 공간 복잡도
    • Set의 최대 크기(약 243)가 제한적이므로 O(1)

 

5. 구현

class Solution {
    public boolean isHappy(int n) {
        // 1. 중복을 확인할 셋 할당
        Set<Integer> seen = new HashSet<>();

        // 2. n이 1이 될때까지 확인
        while (n != 1) {
            // 2-1) 이미 확인한 값이 다시 나오면(사이클) 행복한 수가 아님
            if (seen.contains(n)) return false;
            // 2-2) 현재 값을 셋에 넣고 제곱합 구하기
            seen.add(n);
            n = getDigitSquareSum(n);
        }

        // 3. n이 1이 됐으므로 행복한 수
        return true;
    }

    // 제곱합
    private int getDigitSquareSum(int n) {
        // 1. 합을 저장할 변수 할당
        int sum = 0;
        
        // 2. 각 자릿수를 하나씩 제곱하면서 더하기
        while (n > 0) {
            // 2-1) 현재 자릿수(오른쪽 끝)
            int digit = n % 10;
            // 2-2) 자릿수를 제곱해서 더하기
            sum += digit * digit;
            // 2-3) 현재 자릿수 제거
            n /= 10;
        }

        return sum;
    }
}

 

예제 - LeetCode 219.  Contains Duplicate 2

https://leetcode.com/problems/contains-duplicate-ii/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  배열 내에서 nums[i] == nums[j] 이면서 abs(i - j) <= k인 두 인덱스 찾기

 

2. 요구사항 분석

  • 입력
    • 1 <= nums.length <= 100,000
    • -1,000,000,000 <= nums[i], target <= 1,000,000,000
    • 0 <= k <= 100,000
  • 출력
    • 조건을 만족하는 두 인덱스가 있으면 true, 없으면 false

 

3. 해결 아이디어

  • 배열의 일정 범위(윈도우) 내에서 중복된 값을 HashSet을 사용하여 찾기
  • 범위를 초과하면 이전 값들을 하나씩 줄여가며 범위 유지하기

4. 시간 복잡도 및 공간 복잡도 확인

  • 시간 복잡도
    • 배열을 한 번 순회하므로 O(n)
  • 공간 복잡도
    • 최대 k 크기로 set을 유지하므로 O(k)

 

5. 구현

class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        // 1. 중복된 값이 존재하는지 확인할 set 할당
        Set<Integer> window = new HashSet<>();

        // 2. 배열을 순회하면서 중복인 값 찾기
        for (int i = 0; i < nums.length; i++) {
            // 2-1) 중복된 값이 존재하면 true 반환
            if (window.contains(nums[i])) {
                return true;
            }
            // 2-2) 현재 값 셋에 넣기
            window.add(nums[i]);

            // 2-3 윈도우 크기 유지
            if (window.size() > k) {
                window.remove(nums[i - k]);
            }
        }

        return false;
    }
}

 

예제 - LeetCode 128.  Longest Consecutive Sequence

https://leetcode.com/problems/longest-consecutive-sequence/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  • 주어진 배열에서 연속된 정수로 이루어진 최장 수열 구하기
  • 시간복잡도 O(n)으로 해결해야 함 -> 정렬 불가

 

2. 요구사항 분석

  • 입력
    • 0 <= nums.length <= 100,000
    • -1,000,000,000 <= nums[i] <= 1,000,000,000
  • 출력
    • 최장 수열의 길이

 

3. 해결 아이디어

  • 모든 값을 HashSet에 넣기
  • x - 1이 있는지 확인하여 시작점 체크
  • 시작점부터 x + 1이 있는지 확인하며 길이 계산

4. 시간 복잡도 및 공간 복잡도 확인

  • 시간 복잡도
    • 배열을 한 번 순회하므로 O(n)
  • 공간 복잡도
    • n크기의 HashSet이 필요하므로 O(k)

 

5. 구현

class Solution {
    public int longestConsecutive(int[] nums) {
        // 1. 배열의 수들을 저장할 HashSet 할당
        Set<Integer> set = new HashSet<>();
        // 2. 배열의 수를 저장
        for (int num : nums) {
            set.add(num);
        }

        int longest = 0; // 최장 수열 길이

        // 3. set을 순회하며 최장 길이 구하기
        for (int num : set) {
            // 3-1) 시작 지점인 경우만 수열 길이 계산
            if (!set.contains(num - 1)) {
                int currentNum = num; // 현재 수
                int length = 1; // 현재 길이

                // 3-2) 1씩 큰 수가 있는지 확인
                while (set.contains(currentNum + 1)) {
                    currentNum++;
                    length++;
                }

                // 3-3) 최장 길이 갱신
                longest = Math.max(longest, length);
            }
        }

        return longest;
    }
}