Greatest Common Divisor of Strings — `GCD`로 `O(n + m)` 문자열 최대 공약 패턴
전체 보기접기
- 01Merge Strings Alternately — 단일 루프로 `O(n + m)` 교차 병합
- 02Increasing Triplet Subsequence — `O(n)` 시간, `O(1)` 공간으로 푸는 그리디 패턴
- 03Move Zeroes — 쓰기 포인터 하나로 `O(n)`/`O(1)` 제자리 재배치
- 04String Compression — 읽기/쓰기 포인터로 `O(n)`/`O(1)` 제자리 압축
- 05Product of Array Except Self — 나눗셈 없이 `O(n)`/`O(1)`로 나머지 곱 구하기
- 06Reverse Vowels of a String — 투 포인터 `O(n)` 스왑으로 모음만 뒤집기
- 07Can Place Flowers — `O(n)` 그리디 스캔으로 인접 금지 배치 판정
- 08시간 복잡도와 공간 복잡도 완전 정복 — Big-O 표기법부터 실전 분석까지
- 09Greatest Common Divisor of Strings — `GCD`로 `O(n + m)` 문자열 최대 공약 패턴읽는 중
- 10Container With Most Water — 투 포인터 `O(n)`로 최대 넓이 한 번에 찾기
- 11Is Subsequence — 투 포인터 `O(|s| + |t|)`로 부분 수열 판정
- 12Richest Customer Wealth — 2차원 배열에서 행 합의 최댓값 찾기
- 13Running Sum of 1d Array — 누적합의 가장 기본 형태
- 14Roman to Integer — 감산 규칙을 한 번의 스캔으로 처리하기
- 15Palindrome Number — 문자열 없이 절반만 뒤집는 숫자 팰린드롬 판정
- 16Max Number of K-Sum Pairs — 정렬 `O(n log n)`에서 해시 `O(n)`까지 최대 페어 수 세기
- 17Ransom Note — 문자 개수로 문자열 구성 가능 여부 판정하기
- 18Middle of the Linked List — slow/fast 포인터로 중간 노드 찾기
- 19Max Consecutive Ones — 한 번의 스캔으로 가장 긴 1 구간 찾기
- 20Find Numbers with Even Number of Digits — 자릿수 개수의 짝수 여부 세기
- 21Merge Sorted Array — 뒤에서부터 채우는 제자리 병합
- 22Duplicate Zeros — 고정 길이 배열을 오른쪽에서부터 제자리 갱신하기
- 23Squares of a Sorted Array — 절댓값 기준 투 포인터로 `O(n)` 정렬 유지하기
- 24Valid Mountain Array — 올라갔다가 내려오는 배열 판별하기
- 25Check If N and Its Double Exist — 어떤 값과 그 두 배가 함께 있는지 확인하기
- 26Remove Duplicates from Sorted Array — 정렬 배열을 고유 값 구간으로 압축하기
- 27Remove Element — 앞쪽 유효 구간으로 압축하는 제자리 제거
- 28Replace Elements with Greatest Element on Right Side — 오른쪽 최댓값으로 바꾸기
- 29Sort Array By Parity — 짝수를 앞쪽으로 모으기
- 30Height Checker — 기대 순서와 다른 위치 세기
- 31Third Maximum Number — 세 변수로 상위 3개를 추적해 찾기
- 32Reorder Data in Log Files — 문자 로그만 정렬하고 숫자 로그는 순서 유지하기
- 33Valid Palindrome — 투 포인터로 영숫자만 비교하는 팰린드롬 판정
- 34Find All Numbers Disappeared in an Array — 음수 마킹으로 `O(n)`/`O(1)` 누락 숫자 찾기
- 35Most Common Word — 금지어를 제외하고 가장 자주 나온 단어 찾기
- 36Group Anagrams — 정렬 키와 문자 개수 키로 애너그램 묶기
문자열의 최대 공약수, 어떤 문제인가요?
LeetCode 1071번 문제는 이렇게 주어집니다.
두 문자열
str1과str2가 주어졌을 때, 두 문자열을 모두 나누는 가장 긴 문자열x를 반환하세요. 문자열t가 문자열s를 "나눈다"는 것은t를 한 번 이상 반복해서s를 만들 수 있다는 뜻입니다.
예시는 이렇습니다.
str1 = "ABCABC",str2 = "ABC"→"ABC"("ABC"× 2 ="ABCABC","ABC"× 1 ="ABC")str1 = "ABABAB",str2 = "ABAB"→"AB"("AB"× 3 ="ABABAB","AB"× 2 ="ABAB")str1 = "LEET",str2 = "CODE"→""(공통으로 나눌 수 있는 문자열이 없음)
제약은 간단합니다.
1 <= str1.length, str2.length <= 1000- 두 문자열 모두 대문자 영문으로만 구성
"문자열을 나눈다"는 개념이 정수의 약수와 닮아 있습니다. 정수에서 최대공약수(GCD)를 구하듯, 문자열에서도 최대 공약 문자열을 찾는 것이 핵심입니다. 이 글에서는 후보를 하나씩 시도하는 브루트포스부터, GCD를 활용한 O(n + m) 풀이까지 단계적으로 정리해 보겠습니다. 시간·공간 복잡도 표기가 익숙하지 않다면 시간 복잡도와 공간 복잡도 완전 정복을 먼저 읽어 보시는 것을 권합니다.
Phase 1. 브루트포스 — 가능한 길이를 모두 시도
가장 직관적인 접근은 가능한 길이를 큰 것부터 하나씩 시도하는 것입니다. 공약 문자열이 존재한다면 그 길이는 str1.length와 str2.length 모두의 약수여야 합니다. 가장 긴 것을 찾아야 하므로, 큰 길이부터 내려갑니다.
fun gcdOfStrings(str1: String, str2: String): String {
val len1 = str1.length
val len2 = str2.length
for (k in minOf(len1, len2) downTo 1) {
if (len1 % k != 0 || len2 % k != 0) continue
val candidate = str1.substring(0, k)
if (candidate.repeat(len1 / k) == str1 &&
candidate.repeat(len2 / k) == str2) {
return candidate
}
}
return ""
}
동작을 따라가 봅시다. str1 = "ABABAB", str2 = "ABAB"인 경우:
k = 4:len1(6) % 4 != 0→ 건너뜀k = 3:len2(4) % 3 != 0→ 건너뜀k = 2:"AB".repeat(3) == "ABABAB"✓,"AB".repeat(2) == "ABAB"✓ →"AB"반환
정답은 맞지만, 최악의 경우 minOf(len1, len2)개의 후보를 시도하고, 각 후보마다 repeat + 비교에 O(n + m) 비용이 듭니다. 전체 시간 복잡도는 약 O(min(n, m) * (n + m))입니다. 후보 수를 줄이는 것이 아니라, 후보를 하나로 확정하는 방법이 있다면 더 빠를 수 있습니다.
Phase 2. GCD 기반 풀이 — O(n + m)
핵심 관찰은 이렇습니다. 공약 문자열이 존재한다면 그 길이는 gcd(len1, len2)입니다.
왜 그런지 따져 봅시다. 문자열 t가 str1과 str2를 모두 나누면, t의 길이는 len1과 len2의 공약수입니다. 가장 긴 공약 문자열을 찾으므로, 그 길이는 최대공약수가 됩니다. 그보다 긴 후보는 두 길이의 공약수가 아니고, 그보다 짧은 공약 문자열은 gcd 길이의 약수이므로 gcd 길이보다 길 수 없습니다.
그런데 한 가지 더 확인해야 합니다. 길이가 gcd라고 해서 반드시 공약 문자열이 존재하는 것은 아닙니다. "LEET"과 "CODE"의 길이는 모두 4이고 gcd(4, 4) = 4이지만, 공약 문자열은 없습니다.
존재 여부를 한 번에 판별하는 방법
공약 문자열이 존재하는 필요충분조건은 다음과 같습니다.
str1 + str2 == str2 + str1
이유를 짚어 봅시다. 두 문자열이 같은 패턴 t의 반복으로 구성된다면, str1 = t × a, str2 = t × b입니다. 이때:
str1 + str2=t를a + b번 반복str2 + str1=t를b + a번 반복
둘은 동일합니다. 반대로, 같은 반복 단위가 아닌 두 문자열을 이어 붙이면 순서에 따라 결과가 달라집니다.
"ABCABC" + "ABC" = "ABCABCABC"
"ABC" + "ABCABC" = "ABCABCABC" ← 같음 → 공약 문자열 존재
"LEET" + "CODE" = "LEETCODE"
"CODE" + "LEET" = "CODELEET" ← 다름 → 공약 문자열 없음
이 두 가지를 합치면 풀이가 됩니다.
fun gcdOfStrings(str1: String, str2: String): String {
if (str1 + str2 != str2 + str1) return ""
val gcdLen = gcd(str1.length, str2.length)
return str1.substring(0, gcdLen)
}
private fun gcd(a: Int, b: Int): Int =
if (b == 0) a else gcd(b, a % b)
같은 입력 str1 = "ABABAB", str2 = "ABAB"을 따라가 봅시다.
"ABABAB" + "ABAB"="ABABABABAB","ABAB" + "ABABAB"="ABABABABAB"→ 같음gcd(6, 4)=gcd(4, 2)=gcd(2, 0)=2str1.substring(0, 2)="AB"→ 정답
gcd 함수의 시간 복잡도
유클리드 호제법의 시간 복잡도는 O(log(min(a, b)))입니다. 매 재귀마다 a % b가 최소 절반 이하로 줄기 때문입니다. 문자열 비교 str1 + str2 == str2 + str1이 O(n + m)이므로, 전체 시간 복잡도는 O(n + m)입니다.
두 풀이를 다시 비교
| 풀이 | 시간 | 공간 | 특징 |
|---|---|---|---|
| 브루트포스 (모든 약수 길이 시도) | O(min(n, m) * (n + m)) |
O(n + m) |
직관적이지만 후보마다 repeat + 비교 필요 |
| GCD + 교환 법칙 검증 | O(n + m) |
O(n + m) |
문자열 이어 붙이기 한 번으로 존재 여부 확인 |
두 풀이 모두 공간은 O(n + m)입니다. 문자열 이어 붙이기(str1 + str2)와 substring 결과가 새 문자열을 만들기 때문입니다.
마무리
- "문자열을 나눈다"는 정의를 정수의 약수 관계로 치환하면, 최대 공약 문자열의 길이는
gcd(len1, len2)— 후보 길이를 하나로 좁혀 줍니다 str1 + str2 == str2 + str1은 공약 문자열 존재의 필요충분조건 — 같은 반복 단위로 구성된 두 문자열은 이어 붙이는 순서에 영향을 받지 않습니다- 유클리드 호제법은
O(log(min(a, b)))이므로 병목은 문자열 비교O(n + m)— GCD 계산 자체는 거의 무시할 수 있는 비용입니다
시간 복잡도와 공간 복잡도 완전 정복 — Big-O 표기법부터 실전 분석까지
다음 편Container With Most Water — 투 포인터 `O(n)`로 최대 넓이 한 번에 찾기
다음으로 읽어볼 글
Group Anagrams — 정렬 키와 문자 개수 키로 애너그램 묶기
애너그램 문자열을 같은 그룹으로 묶는 LeetCode 49번, 정렬된 문자열을 키로 쓰는 풀이부터 문자 개수 배열을 해시 키로 바꾸는 `O(n * k)` 풀이까지 정리합니다.
Most Common Word — 금지어를 제외하고 가장 자주 나온 단어 찾기
문단에서 금지어를 제외하고 가장 많이 등장한 단어를 찾는 LeetCode 819번, 문장 부호 제거와 소문자 정규화, 해시 카운팅 풀이를 정리합니다.
Find All Numbers Disappeared in an Array — 음수 마킹으로 `O(n)`/`O(1)` 누락 숫자 찾기
1부터 n까지의 숫자 중 배열에 나타나지 않은 값을 찾는 LeetCode 448번, 완전 탐색부터 존재 배열, 음수 제자리 마킹 풀이까지 정리합니다.