ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 문자열
    알고리즘 2025. 4. 17. 22:57

     

     

     

     

    문자열 찾기 

     

    1) longest-substring-without-repeating-characters

    문제 : https://leetcode.com/problems/longest-substring-without-repeating-characters/

     

    1. 슬라이딩 윈도우 

    반복되지 않는 가장 긴 문자열의 길이를 반환하는 문제다. 

    (풀고 보니 슬라이딩 윈도우와 비슷한 방식으로 풀었다.)

    n = 문자열길이, k = 현재 윈도우 길이 (최대 n)이라고 가정할때, 

    temp.contains(), temp.indexOf(), temp.substring(...) + s[i] 모두 O(k)의 시간복잡도를 갖고, 이 연산이 최대 n번 반복될 수 있다. 

    시간복잡도 : O(n^2) 

    문자열 연산이 계속 일어나므로 새로운 문자열이 계속 생성될 수 있다. 

    공간복잡도 : O(n) 

    class Solution {
        fun lengthOfLongestSubstring(s: String): Int {
            if(s.length == 0 || s.length ==1) return s.length
            var temp = ""
            var answer = 0
            for(i in 0 until s.length) {
                if(temp.contains(s[i])) {
                    answer = max(temp.length, answer)
                    val idx = temp.indexOf(s[i])
                    temp = temp.substring(idx +1) + s[i]
                } else {
                    temp += s[i]
                }
            }
            return max(answer, temp.length) 
        }
    }

     

     

    2. 슬라이딩 윈도우 + HashSet 

    HashMap (or Set) + Two Pointer로 O(n)의 풀이도 가능하다. 

     

    각 문자는 최대 두번만 확인되므로 

    시간복잡도 : O(n) 

    문자 Set에 최대 n개를 저장하므로 

    공간복잡도 : O(n)

    class Solution {
        fun lengthOfLongestSubstring(s: String): Int {
            val set = mutableSetOf<Char>()
            var left = 0 
            var right = 0
            var answer = 0 
    
            while (right < s.length) {
                if(set.contains(s[right]).not()) {
                    set.add(s[right])
                    answer = max(answer, right-left +1)
                    right ++
                }else {
                    set.remove(s[left])
                    left ++
                }
            }
            return answer 
        }
    }

     

     

     

    2) longest-repeating-character-replacement

    k개의 문자열을 대체했을때 가장 긴 중복 문자열의 길이를 찾는 문제다. 

    문제 : https://leetcode.com/problems/longest-repeating-character-replacement

    슬라이딩 윈도우를 하면서 문자열 내에서 교체 가능한 윈도우를 찾고, 그 길이를 측정하는 방식으로 진행할 수 있다. 

     

    시간 복잡도 : O(n) -> right 포인터는 한번씩, left 포인터도 한번씩만 이동 

    공간 복잡도 : O(1) -> 고정 배열 크기 26개(알파벳 대문자)만 사용 

    class Solution {
        fun characterReplacement(s: String, k: Int): Int {
        
            // 슬라이딩 윈도우 + 문자 카운팅 
            val count = IntArray(26)
            var left = 0 
            var maxLen = 0
            var maxFreq = 0 
            for( right in s.indices) {
                val idx = s[right] - 'A'
                count[idx] ++ 
                maxFreq = max(maxFreq, count[idx])
                val windowSize = right-left +1 
                if(windowSize - maxFreq >k) {
                    count[s[left] - 'A'] --
                    left ++
                }
                maxLen = max(maxLen, right-left + 1)
            }
            return maxLen 
        }

     

    3) minimum-window-substring

    문제 : https://leetcode.com/problems/minimum-window-substring/

    s, t 두개의 문자열이 주어졌을때, s가 t의 알파벳을 모두 포함할 수 있는 최소 문자열을 구하는 문제다. 

     

    슬라이딩 윈도우 + 해시맵 비교를 통해서 해결할 수 있다. 

     

    시간 복잡도 : O(n) -> 문자열 s의 left, right 포인터가 한번씩 이동(O(n)) + 맵에 데이터를 넣고 비교/ 카운트 하는 작업(O(1))

    공간 복잡도 : O(1) -> s, t에 대한 카운트 저장 맵 2개, 가능한 문자열은 최대 128가지로 고정상수이므로 O(1) 

     

    class Solution {
        fun minWindow(s: String, t: String): String {
            
            val tCount = mutableMapOf<Char, Int>()
            val windowCount = mutableMapOf<Char, Int>()
            for(c in t) {
                tCount[c] = tCount.getOrDefault(c, 0) +1
            }
    
            var left = 0 
            var have = 0 
            val need = tCount.size 
            var minLen = Int.MAX_VALUE
            var answer = ""
            for (right in s.indices) {
                val c = s[right]
                windowCount[c] = windowCount.getOrDefault(c, 0) + 1
                
                if(tCount.containsKey(c) && windowCount[c] == tCount[c]) {
                    have ++
                }
                while(have == need) {
                    if(right-left + 1<minLen) {
                        minLen = right - left + 1
                        answer = s.substring(left, right+1)
                    }
                    val leftChar = s[left]
                    windowCount[leftChar] = windowCount[leftChar]!! -1
                    if(tCount.containsKey(leftChar) && windowCount[leftChar]!! < tCount[leftChar]!!) {
                        have -- 
                    }
                    left ++ 
                }
            }
            return answer 
    
        }
    }

     

     

     

    4) Valid Anagram 

    문제 : https://leetcode.com/problems/valid-anagram/

    두개의 문자열이 주어지면 같은 알파벳으로 이루어져있는지 찾는 문제다.

    class Solution {
        fun isAnagram(s: String, t: String): Boolean {
            val alpha = IntArray(26)
    
            for(char in s) {
                alpha[char-'a'] ++
            }
    
            for(char in t) {
                val idx = char-'a'
                alpha[idx] --
                if(alpha[idx]<0) {
                    return false 
                } 
            }
    
            if(alpha.sum()>0) {
                return false 
            }
            return true 
        }
    }

     

     

    5) Group Anagrams 

    문제 : https://leetcode.com/problems/group-anagrams/

    여러개의 문자열이 주어지면, 같은 알파벳으로 이루어진 문자열들을 리스트로 묶어 반환하는 문제다. 

    n = 문자열 개수, k = 각 문자열의 길이일때, 

    sorted()를 사용하므로 시간 복잡도는 O(n * klogk)로 리스트가 길어질수록 병목이 될 수 있다 .

    공간복잡도는 O(n*k) 다. (모든 문자열 저장 + 그룹화 저장) 

    class Solution {
                fun groupAnagrams(strs: Array<String>): List<List<String>> {
                    val map = mutableMapOf<String, MutableList<String>>()
                    for(str in strs) {
                        val sorted = str.toCharArray().sorted().joinToString("") 
                        if(map.contains(sorted)) {
                            map[sorted]!!.add(str)
                        }else {
                            map[sorted] = mutableListOf(str)
                        }
                    }
                    val answer = mutableListOf<List<String>>()
                    for(result in map.values) {
                        answer.add(result.toList())
                    }
                    return answer
                }
    }

     

     

    정렬을 사용하지 않고, 갯수 자체를 기록하는 방법을 사용하면 시간을 단축할 수 있다. 

    시간 복잡도 = O(n*k) 

    공간 복잡도 = O(n*k) 

    class Solution {
        fun groupAnagrams(strs: Array<String>): List<List<String>> {
            val map = mutableMapOf<String, MutableList<String>>()
    
            for (str in strs) {
                val count = IntArray(26)
                for (c in str) {
                    count[c - 'a']++
                }
    
                // 배열을 문자열로 변환 (ex: "1#0#0#..." 식으로 고유 키 생성)
                val key = count.joinToString("#")
    
                map.getOrPut(key) { mutableListOf() }.add(str)
            }
    
            return map.values.toList()
        }
    }

     

     

    6) Valid Parentheses 

    문제 : https://leetcode.com/problems/valid-parentheses/

    올바른 괄호 형식을 가지는지 찾는 전형적인 스택 문제다. 

    class Solution {
        fun isValid(s: String): Boolean {
            if(s.length %2 != 0) {
                return false 
            }
            val input = listOf('(', '{', '[')
            val queue = Stack<Char>()
            for(char in s) {
                if(input.contains(char)) {
                    queue.push(char)
                }else if(queue.isEmpty()){
                    return false 
                }else {
                    val pop = queue.pop() 
                    if((pop == '(' && char != ')') || 
                        (pop == '{' && char != '}') ||
                        (pop == '[' && char != ']')
                    ) {
                        return false 
                    }
                }
            }
            if(queue.isNotEmpty()) {
                return false 
            }
            return true 
        }
    }

     

     

    맵을 활용하면 더 간결하게도 가능하다. 

    class Solution {
        fun isValid(s: String): Boolean {
            val stack = Stack<Char>()
            val pairs = mapOf(')' to '(', '}' to '{', ']' to '[')
    
            for (char in s) {
                if (char in pairs.values) {
                    stack.push(char)
                } else {
                    if (stack.isEmpty() || stack.pop() != pairs[char]) return false
                }
            }
    
            return stack.isEmpty()
        }
    }

     

     

    Palindrome

     

    1) valid-palindrome

    문제 : https://leetcode.com/problems/valid-palindrome

    주어진 문자열이 역순으로도 동일한 단어임을 구분한다. 

    먼저 단순하게 최종 문자열을 구한뒤 역순으로 정렬한 값과 비교했다. 

    시간복잡도 : O(n) 

    공간복잡도 : O(n)

    class Solution {
        fun isPalindrome(s: String): Boolean {
            var str = StringBuilder()
            for (c in s) {
                if(c.isLetterOrDigit()) {
                    str.append(c.lowercaseChar())
                }
            }
            var result = str.toString()
            return result.reversed() == result
        }
    }

     

     

    미리 문자열을 모두 순회하지 않고, 투포인터 기법으로도 문제를 해결할 수 있다. 

     

    시간복잡도 : O(1)

    공간복잡도 : O(1)

    class Solution {
        fun isPalindrome(s: String): Boolean {
            var left = 0 
            var right = s.length - 1
            while(left < right) {
                while(left < right && !s[left].isLetterOrDigit()) left++
                while(left < right && !s[right].isLetterOrDigit()) right--
                if(s[left].lowercaseChar() != s[right].lowercaseChar()) {
                    return false
                }
                left++
                right--
            }
            return true 
        }
    }

     

     

    2) Longest Palindromic Substring 

    문제 : https://leetcode.com/problems/longest-palindromic-substring/

     

    문자열을 순회하면서 중심 지점을 선정하고 최대 길이를 가질 수 있는 문자열을 출력한다. 

    시간복잡도 : O(n^2)

    공간복잡도 : O(1)

    class Solution {
        fun longestPalindrome(s: String): String {
            if(s.length < 2) return s
    
            var start = 0
            var end = 0 
    
            for(i in s.indices) {
                // 현재 시작점 i 
                val len1 = expand(s, i, i) // 홀수 개라고 가정하고 최댓값 계산 
                val len2 = expand(s, i, i+1) // 짝수 개라고 가정하고 최댓값 계산
    
                val len = maxOf(len1, len2)
    
                if(len>end-start) { // 최댓값 발생으로 정답 갱신 (홀수와 짝수에서 모두 정답을 낼 수 있는 인덱스 값 확인) 
                    start = i - (len-1)/2 
                    end = i + len/2
                }
            }
            return s.substring(start, end+1)
        }
        
    
        private fun expand(s:String, left:Int, right:Int): Int{
            var l = left 
            var r = right 
    
            while(l>=0 && r<s.length && s[l]==s[r]) {
                l--
                r++
            }
    
            return r-l-1
        }
    }

     

     

    3) palindromic-substrings

    문제 : https://leetcode.com/problems/palindromic-substrings/submissions/1628517953/

    palindromic의 갯수를 세는 문제로 상단의 문제를 응용해서 풀 수 있다. 

    시간복잡도 : O(n^2)

    공간복잡도 : O(1)

    class Solution {
        fun countSubstrings(s: String): Int {
            var count = 0 
    
            for(i in s.indices) {
                count += expand(s, i, i)
                count += expand(s, i, i+1)
            }
            return count 
        }
    
        private fun expand(s: String, left: Int, right:Int): Int {
            var cnt = 0 
            var l = left 
            var r = right 
            while(l >=0 && r <s.length && s[l] == s[r]) {
                l--
                r++ 
                cnt++
            }
            return cnt 
        }
    
    }

     

     

    4) string-encode-and-decode

    문제 : https://neetcode.io/problems/string-encode-and-decode

     

    문자열을 encode하고 decode 하는 함수를 만든다. 

    이때, 문자열을 누락하지 않고 그대로 encode, decode할 기준점을 잡아야 한다. 

     

    class Solution {
    
        public String encode(List<String> strs) {
            StringBuilder sb = new StringBuilder();
            for(int i =0; i < strs.size() ; i++) {
                sb.append(strs.get(i).length()).append("#").append(strs.get(i));
            }
            return sb.toString();
        }
    
        public List<String> decode(String str) {
            List<String> result = new ArrayList<String>();
            int i = 0 ;
            while(i < str.length()) {
                int j = str.indexOf('#', i); // i 부터 찾는다 
                int len = Integer.parseInt(str.substring(i, j)); // 문자열 사이즈 
                String s = str.substring(j+1, j+1 + len); // 문자열 
                result.add(s);
                i = j+1 + len ;
            }
            return result ;
        }
    }

    '알고리즘' 카테고리의 다른 글

    트리  (1) 2025.05.14
    행렬 문제  (0) 2025.04.11
    연결리스트 문제  (0) 2025.04.10
    그래프(DFS, BFS) 문제  (0) 2025.03.11
    백준 14891) 톱니바퀴 (골드.5)  (0) 2022.08.02
Designed by Tistory.