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
        }
    }
    
    
    
    
    
    
        var start = 0 
        var answer = 0 
        fun longestPalindrome(s: String): String {
            if(s.length <2) return s
    
            for(i in 0..s.length) {
                isPalindrome(s, i, i+1)
                isPalindrome(s, i, i+2)
            }
            return s.substring(start, start+answer)
        }
    
        fun isPalindrome(s: String, left: Int, right: Int) {
            var l = left 
            var r = right 
            while(l>=0 && r <s.length && s[l] == s[r]) {
                l -- 
                r ++
            }     
            if(answer < r-l-1) {
                answer = r-l-1
                start = 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 ;
        }
    }

     

     

     

    5) valid palindrome 2 

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

     

     

    class Solution {
        fun validPalindrome(s: String): Boolean {
            var left = 0 
            var right = s.length -1
            while (left < right) {
                if(s[left] != s[right]) {
                   return isPalindrome(s, left+1, right) || isPalindrome(s, left, right-1)
                }
                left ++ 
                right --
            }
            return true
        }
    
        private fun isPalindrome(s: String, l: Int, r:Int): Boolean{
            var left = l
            var right = r
            var moved = false
            while (left < right) {
                if(s[left] != s[right]) {
                   return false 
                }
                left ++ 
                right --
            }
            return true
        }
    
    }

     

     

     

     

     

    reverse string 

    문제 : https://leetcode.com/problems/reverse-string/

     

    class Solution {
        fun reverseString(s: CharArray): Unit {
            var left = 0 
            var right = s.size - 1
            while (left < right) {
                val temp  = s[left]
                s[left] = s[right]
                s[right] = temp 
                left ++ 
                right -- 
            }
        }
    }

     

     

     

    Reorder data log 

     

    문제 : https://leetcode.com/problems/reorder-data-in-log-files/

     

    1. 로그 분리 (digit/letter):

    • for 루프에서 각 로그에 대해 split(" ")[1][0].isDigit() 평가
    • split은 최대 한 번만 하므로, O(n * m)
      • n = 로그 개수
      • m = 평균 로그 길이

    2. 문자로그 정렬:

    • sortWith는 일반적인 비교 기반 정렬이므로 O(k log k)
      • k = 문자 로그 개수 (k ≤ n)
    • 비교 시마다 split(" ", limit = 2)를 호출 → 문자열 복사 연산 포함
      • 각 비교는 O(m)
      • 전체 정렬: O(k log k * m)

    3. 리스트 합치기 (addAll, toTypedArray):

    • addAll: O(d) (숫자로그 개수 d ≤ n)
    • toTypedArray: O(n)

    총 시간 복잡도 

    O(n * m) + O(k log k * m) + O(n)
    = O(n * m + k log k * m) = 최악의 경우 (k = n)O(nlogn*m) 

    공간복잡도 

    = O(n) 

     

    class Solution {
        fun reorderLogFiles(logs: Array<String>): Array<String> {
            val digitLogs = mutableListOf<String>()
            val letterLogs = mutableListOf<String>()
    
            for(log in logs) {
                if(log.split(" ")[1][0].isDigit()) {
                    digitLogs.add(log)
                }else {
                    letterLogs.add(log)
                }
            }
    
            letterLogs.sortWith(Comparator {s1: String, s2:String -> 
                val s1Log = s1.split(" ", limit = 2)
                val s2Log = s2.split(" ", limit = 2)
                val compared = s1Log[1].compareTo(s2Log[1])
                if(compared == 0) {
                    s1Log[0].compareTo(s2Log[0])
                } else {
                    compared 
                }
            })
    
            letterLogs.addAll(digitLogs)
            return letterLogs.toTypedArray()
    
        }
    }

     

     

    Most common word 

    문제 : https://leetcode.com/problems/most-common-word/

    class Solution {
        fun mostCommonWord(paragraph: String, banned: Array<String>): String {
            val countWord = mutableMapOf<String, Int>()
    
            val originalWords = paragraph.replace("\\W+".toRegex(), " ").lowercase()
            val words = originalWords.trim().split(" ")
            for(word in words) {
                if(banned.contains(word)) {
                    continue
                }
                countWord[word] = countWord.getOrPut(word, {0}) + 1
    
            }
            return countWord.maxBy {it.value}.key
        }
    }

     

     

     

    지그재그 문자열 만들기 

     

    문제 : https://leetcode.com/problems/zigzag-conversion/?envType=problem-list-v2&envId=string

     

    간단하게 위 아래 방향성으로만 생각하면 된다. 

    class Solution {
        fun convert(s: String, numRows: Int): String {
            if(s.length <= numRows  || numRows == 1) {
                return s
            }
            var answer = Array(minOf(numRows, s.length)) {StringBuilder()}
            var curRow = 0 
            var isDowning = false 
            for (c in s) {
                answer[curRow].append(c)
                if(curRow == 0 || curRow == numRows -1) {
                    isDowning = !isDowning
                }
                curRow += if(isDowning) 1 else -1
            }
    
            return answer.joinToString("") {it.toString()}
    
        }
    }

     

     

     

    logest palindrome by concatenating two letter words

    문제 : https://leetcode.com/problems/longest-palindrome-by-concatenating-two-letter-words

     

    두글자로 이루어진 문자열 배열에서 가장 긴 palindrome을 찾는 문제다. 

    여기서 구분해야 하는 것은 같은 문자로 이루어진 단어와, 다른 문자로 이루어진 단어가 다르게 취급되어야 한다는 것이다. 

     

    하나의 문자로 이루어진 경우 -> 짝수개라면 모두다 사용이 가능, 홀수개로 이루어지면 가운데에만 위치 가능 

    두개의 문자로 이루어진 경우 -> 반대 문자열이 존재하는 경우 사용이 가능 

     

    시간 복잡도 : O(N) (처음 배열 루프) + O(1) (문자기준으로 두글자로 이루어진 map순회 = 26*26) = O(N)

    공간 복잡도 : O(1) (단어길이가 두글자고, 맵의 최대 크기는 26*26)

    class Solution {
        fun longestPalindrome(words: Array<String>): Int {
            val cnt = mutableMapOf<String, Int>()
            var result = 0
            var hasCenter = false 
            for(word in words) {
                cnt[word] = cnt.getOrDefault(word, 0) + 1   
            }
    
            for((word, freq) in cnt) {
                val reversed = word.reversed()
                if(word == reversed) {
                    // 같은 문자 두글자는 짝수이면 두개 다 넣기 가능 
                    result += (freq/2) * 4
                    // 만약 홀수개가 있다면 센터에 위치시킬 수 있음 
                    if(freq % 2 == 1) {
                        hasCenter = true 
                    }
                    // 중복 계산 방지 
                } else if (word<reversed) {
                    val pairCnt = minOf(cnt.getOrDefault(word, 0), cnt.getOrDefault(reversed, 0))
                    result += pairCnt *4
                }
            }
    
            if(hasCenter) result += 2
    
            return result 
        }
    }

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

    힙 (Heap)  (1) 2025.05.20
    트리  (1) 2025.05.14
    행렬 문제  (0) 2025.04.11
    연결리스트 문제  (0) 2025.04.10
    그래프(DFS, BFS) 문제  (0) 2025.03.11
Designed by Tistory.