
오늘은 백준 2075번 N번째 큰 수 문제를 풀다가
최소 힙(Min Heap) 을 이용해 잘 구현했다고 생각했는데…
계속 시간초과(TLE) 가 발생했습니다. 🤯
코드 로직은 분명 맞는 것 같았는데 왜 자꾸 시간초과가 뜰까?
구글링을 해보니 문제는 readLine()의 속도에 있더군요.
Swift의 readLine()은 편하긴 하지만,
입력 데이터가 많아질 경우 굉장히 느리다는 치명적인 단점이 있습니다.
특히 이 문제처럼 최대 1,500 × 1,500개의 수를 읽어야 할 땐 더 심각하죠.
그래서 이번 포스팅에서는
👉 시간초과를 해결할 수 있는 빠른 입력 방식,
즉 FileIO를 이용한 극한의 빠른 입력 처리 방법을 소개해보려고 합니다.
📉 왜 readLine()은 느릴까?
Swift에서 가장 기본적으로 사용하는 입력 함수는 바로 readLine()입니다.
하지만 알고리즘 문제에서 입력이 많아지면, 이 함수는 눈에 띄게 느려지고 결국 시간초과가 나게 됩니다.
그 이유는 다음과 같습니다:
1. readLine()은 문자열 단위로 동작한다!
readLine()은 한 줄을 String 타입으로 읽은 뒤, 우리가 다시 split, map, Int() 등으로 변환해서 써야 합니다.
즉, 한 줄을 읽을 때 이런 과정이 생깁니다:
입력 버퍼 → 문자열 파싱 → 문자열 배열 → Int 배열 변환
이러한 추가적인 처리 비용이 많아질수록 성능이 나빠집니다.
2. 표준 입력을 한 글자씩 처리한다!
Swift의 readLine()은 내부적으로 입력 스트림을 한 글자씩 확인하고 개행을 만날 때까지 읽습니다.
대량의 입력을 처리할 때 이건 엄청난 오버헤드가 됩니다.
C에서는 scanf처럼 버퍼 단위로 입력을 처리하는 방식이 훨씬 빠른데,
Swift는 기본적으로 안전한 방식을 택하다 보니 속도에서는 손해를 보게 됩니다.
3. 대량 입력에 적합하지 않다!
예를 들어, N이 1500일 때 2차원 배열을 입력받는다면 2,250,000개의 숫자를 읽어야 합니다.
그걸 readLine()으로 하나씩 읽고 split하고 map하고… 하면?
❌ Swift에겐 너무 큰 부담입니다.
그래서 우리는 더 빠른 방법인 FileIO 클래스를 직접 구현해서 사용하게 되는 것입니다.
⚡️ 그럼 FileIO는 어떻게 동작해서 빠를까?
Swift 기본 입력인 readLine()이 느리다는 건 앞에서 설명드렸습니다.
그렇다면 우리가 직접 만든 FileIO 클래스는 어떻게 그렇게 빠른 걸까요?
핵심은 딱 하나입니다:
✅ 입력을 한 글자씩 읽지 않고, 전체 입력을 한 번에 읽은 다음, 필요한 만큼만 직접 파싱해서 쓰기 때문입니다.
🔍 동작 원리 요약
- FileIO는 FileHandle.standardInput을 사용해서
- 전체 입력을 통째로 readToEnd()로 읽습니다.
- 그 입력을 [UInt8] 타입의 바이트 배열로 저장해둡니다.
- 이후 정수를 읽고 싶을 때는,
- 우리가 직접 while 루프를 돌면서 숫자인 문자만 골라서 계산합니다.
→ 즉, 문자열로 바꿨다가 다시 숫자로 바꾸는 번거로운 과정이 없습니다.
→ 한 글자씩 검사하면서도 map, split 없이 순수하게 숫자만 처리하니 훨씬 빠른 거죠.
🔹 1단계. 입력을 한 번에 읽는다!
init(fileHandle: FileHandle = FileHandle.standardInput) {
buffer = Array(try! fileHandle.readToEnd()!) + [UInt8(0)]
}
- readToEnd()는 표준 입력을 끝까지 한 번에 읽고 Data로 반환 합니다.
- readToEnd()는 내부적으로[UInt8]를 담고 있습니다. UInt8 배열로 변환해 buffer에 저장합니다.
- 마지막에 + [UInt8(0)]을 붙여 EOF 처리를 안전하게 합니다.
💡 이렇게 하면 입출력 호출을 단 한 번만 하므로 매우 빠릅니다.
🔹 2단계. 한 글자씩 직접 읽는 메서드 read()
@inline(__always) private func read() -> UInt8 {
defer { index += 1 }
return buffer[index]
}
- 현재 위치의 바이트(UInt8)를 반환하고, 다음 글자로 넘어갑니다.
- index는 읽기 포인터처럼 사용됩니다.
예: "123 456" → [49, 50, 51, 32, 52, 53, 54](49는 ASCII ‘1’, 32는 공백)
🔹 3단계. 숫자만 추출하는 readInt()
@inline(__always) func readInt() -> Int {
var num = 0
var isNegative = false
var byte = read()
while byte == 10 || byte == 32 { byte = read() } // 공백, 개행 제거
if byte == 45 { isNegative = true; byte = read() } // 음수 처리
while byte >= 48, byte <= 57 {
num = num * 10 + Int(byte - 48)
byte = read()
}
return num * (isNegative ? -1 : 1)
}
- 공백(32), 줄바꿈(10)은 건너뜀
- 숫자(ASCII 48~57)를 만나면 그때부터 수를 계산
- num = num * 10 + (현재 자리수) 방식으로 자릿수를 쌓음
- -가 있으면 isNegative = true 설정 후 마지막에 음수 처리
🔹 4단계. 실제 사용 흐름
let file = FileIO()
let n = file.readInt()
let m = file.readInt()
- readInt()를 반복 호출하면 입력 데이터를 순서대로 하나씩 읽을 수 있음
- 사용자는 Int 값만 얻으면 되므로 간단하고 빠름
💻 전체 코드
final class FileIO {
private var buffer: [UInt8]
private var index: Int = 0
init(fileHandle: FileHandle = FileHandle.standardInput) {
buffer = Array(try! fileHandle.readToEnd()!) + [UInt8(0)]
}
@inline(__always) private func read() -> UInt8 {
defer { index += 1 }
return buffer[index]
}
@inline(__always) func readInt() -> Int {
var num = 0
var isNegative = false
var byte = read()
while byte == 10 || byte == 32 { byte = read() }
if byte == 45 { isNegative = true; byte = read() }
while byte >= 48, byte <= 57 {
num = num * 10 + Int(byte - 48)
byte = read()
}
return num * (isNegative ? -1 : 1)
}
}
🧩 마무리
이번 포스팅에서는 Swift로 알고리즘 문제를 풀 때 자주 마주치는 시간초과 문제를
FileIO 클래스를 활용해 빠른 입력 처리 방식으로 해결하는 방법을 소개해드렸습니다.
이 방법은 특히 백준(BOJ)에서 입력이 많은 문제를 풀 때 정말 강력한 무기이니,
꼭 기억해두셨다가 활용해보시길 추천드립니다! 💪
앞으로도 알고리즘을 공부하면서 마주치는 유용한 팁이나 개념들을
이렇게 정리해보려고 합니다.
꾸준히 기록하며 성장하는 개발자가 되기 위해 계속 달려보겠습니다! 🚀
읽어주셔서 감사합니다 😊
