러스트 입출력 방법 총정리 - Rust로 알고리즘 풀기

이승규·2022년 12월 27일
1
post-thumbnail

썸네일 출처: https://external-preview.redd.it/7pJXcEbRlyjmS4R7KNT0Rx-1PECe-b78NKnZbGlUm1g.jpg?auto=webp&s=f9d3581014288f8ee24e4ad869f4d47aa4cad176


💁 서론

러스트로 알고리즘 풀기를 처음 시작하면서 가장 문제가 됐던 부분이 바로 어떻게 입출력을 해야 하나? 였습니다. 파이썬처럼 함수 하나로 입출력이 다 되는 것에 너무 익숙해졌던 나머지 입출력에 두 줄 이상 쓰이면 곧바로 몸에서부터 거부반응이 올라오더군요.💦 러스트에도 println!()같은 매크로가 있긴 하지만, 기본적으로 인풋을 받을 때는 버퍼를 만들고 Stdin인스턴스를 만든 뒤에 read_line()같은 메소드를 호출해야 하는 다소 귀찮은 과정이 필요합니다.

가뜩이나 입출력 방식도 어려운데, 국내에서는 별로 잘 쓰지도 않아서 관련된 정보 찾기가 어렵더군요. 그래서, 혹시나 있을 새로운 Rustacean 분들, 특히 저처럼 코딩테스트에도 못 쓰는 러스트로 굳이 알고리즘을 풀어야겠다는 분들을 위해서 지금까지 제가 배운 러스트 입출력 방식을 정리해놓으려고 합니다.

알고리즘 문제는 인풋과 아웃풋이 아주 긴 문제들이 많아서 입출력 방식을 잘못 쓰면 알고리즘을 잘 짰어도 입출력에서 시간이 초과되어 버리는 경우가 생깁니다. 가뜩이나 문제푸느라 짜증나 죽겠는데 입출력때문에 실패가 뜨는 것을 보면 아주 미치고 환장할 노릇이죠. 저도 처음에는 100,000줄이 넘어가는 아웃풋을 죄다 println!() 매크로로 프린트해놓고 뭣때문에 시간초과가 나는지 몰라서 정신이 반쯤 나가버렸던 적이 있습니다. 저같이 미련하게 시간 버리는 사람이 또 나오지 않았으면 좋겠네요.

아래 정리한 내용들은 표준 입출력을 기준으로 합니다.

아래 내용들은 백준에서 다른 분들이 코딩한 것을 참고하거나 스택오버플로우 등 사이트에서 질문한 내용들을 바탕으로 작성되었습니다.
잘못된 부분이나 더 효율적인 방법 등, 알고계신 것들이 있다면 댓글로 알려주세요!

개인적으로 자주 쓰는 것들을 ⭐️로 표시해 놓았습니다.


📕 정리

⌨️ 입력

1. 한 줄 입력받기

use std::io::stdin

fn main() {
	let mut buffer = String::new();
    stdin().read_line(&mut buffer).unwrap();
    
    ...

가장 기본적인 입력방식입니다. 입력이 문자열 딱 한 줄만 주어질 때 쓰면 좋습니다.

2. 여러 줄 순차적으로 입력받기

use std::io::stdin

fn main() {
	let mut buffer = String::new();
    stdin().read_line(&mut buffer).unwrap();
    
    let n = buffer.trim().parse::<usize>().unwrap();
    
    for _ in 0..n {
    	buffer.clear();
        stdin().read_line(&mut buffer).unwrap();
        
        ...
    }

여러 줄에 걸쳐서 문자열이 입력으로 주어질 때 쓸 수 있는 방식입니다.
보시다시피 매번 새 라인을 입력받을 때마다 버퍼를 클리어해줘야 하기 때문에 그닥 효율적인 방식은 아닙니다.
대신 메모리가 많이 들지 않기 때문에 메모리가 중요한 경우에는 충분히 사용할 만한 방식입니다.

3. 숫자 여러개 입력받기 ⭐️

use std::io::{stdin, Read}

fn main() {
	let mut input = String::new();
    stdin().read_to_string(&mut input).unwrap();
    let mut input = input.split_ascii_whitespace().flat_map(str::parse::<usize>);
    
    let n = input.next().unwrap();
    
    ... 필요할 때 마다 input.next().unwrap() 호출

제가 문제 풀때 가장 애용하는 방식입니다. 알고리즘 문제 특성상 정수 숫자를 띄어쓰기나 줄바꿈으로 구분해서 여러개 입력받는 경우가 많은데, 그럴때 라인별로 입력받고 스플릿하고 파싱하고 하는게 아니라 아얘 통째로 입력받아서 통째로 스플릿하고 통째로 파싱해서 저장해둡니다. 그리고 필요할 때 마다 input.next().unwrap()으로 하나씩 뽑아쓰면 됩니다.

단점을 꼽자면 입력을 한 번에 버퍼에 저장하기 때문에 메모리 이슈가 발생할 수 있다는 겁니다. 하지만 입력이 여간 큰게 아니면 보통의 경우에는 그럴 일이 거의 없습니다.

또 하나의 단점이라면 커맨드라인에서 직접 입력 데이터를 줄 수가 없다는 것인데, 인풋 데이터는 거의 파일에 써서 사용하기 때문에 굳이 입력 데이터를 커맨드라인에서 쓸 필요가 없죠. 그래서 특별히 단점이라고 보기는 어렵네요.

파싱을 usize타입으로 하는 이유는 제가 푼 대부분의 문제가 입력으로 자연수를 줬고, 입력 데이터를 배열의 인덱스로 사용할 일이 많기 때문입니다. i32타입으로 파싱하시면 usize가 아니라고 불평하는 린터 메세지를 아주 많이 보게 되실 거에요. 🤮

또 하나 주목할 부분은 인풋 데이터를 스플릿할때 split_whitespace() 대신 split_ascii_whitespace()를 사용했다는 겁니다. 둘다 거의 같은 효과가 있지만 후자가 좀더 빠릅니다. 알고리즘 문제에서는 거의 공백문자와 '\n' 만으로 데이터가 구분되기 때문에 굳이 전자를 사용할 이유가 없습니다.

4. 문자열 어러개 입력받기 ⭐️

use std::io::{stdin, Read}

fn main() {
	let mut input = String::new();
    stdin().read_to_string(&mut input).unwrap();
    let mut input = input.split_ascii_whitespace();
    
    let n = input.next().unwrap().parse::<usize>().unwrap();
    let line = input.next().unwrap();
    
    ... 필요할 때 마다 input.next().unwrap() 호출

3번에서 파싱하는 부분만 뗀 버전입니다. 공백문자로 구분된 데이터들이 숫자가 아니라 문자열이 포함되어있는 경우에 사용합니다.

5. 입력 데이터 파일로 받기(표준 입력 리디렉션)

커맨드라인에서 실행시킬 때 파일을 따로 만들어놓고 거기에 인풋 데이터를 적은 다음에 다음과 같은 커맨드를 실행시키면 파일에서 입력을 받을 수 있습니다.

cargo run < input.txt

부등호 방향을 거꾸로 하면 파일에 출력도 됩니다.

VSCode, CLion같은 IDE를 사용하신다면 실행 설정에서 입력 리디렉션을 설정하면 됩니다.

정말 기본적인건데 제가 몰라서 초반에 고생했기 때문에 적습니다.

🖨️ 출력

1. 한 줄 출력하기

let data = 32;
println!("{data}");

가장 쉬운 출력방식입니다.
내부적으로 Stdout에 매번 락을 걸고 해제하기 때문에 여러 줄을 반복적으로 출력하는 경우에 매우 비효율적입니다.

2. BufWriter를 이용해 여러줄 출력하기

use std::io::{stdout, Write, BufWriter}

fn main() {
	let stdout = stdout();
    let mut writer = BufWriter::new(stdout);
    
    for i in 0..100_000 {
    	writeln!(writer, "{i}").unwrap();
    }
}

출력해야 하는 양이 많을 때 사용하면 좋습니다.
일단 버퍼를 사용하기 때문에 출력속도가 좋아지는것 외에도 BufWriter가 생성될때 락을 걸고 생명주기가 끝날때 까지 유지되기 때문에 println!() 매크로를 사용해 출력할때처럼 매번 락을 걸었다 풀었다 하지 않습니다.

3. String 인스턴스를 버퍼로 활용하기 ⭐️

use std::fmt::Write

fn main() {
	let mut output = String::new();
    
    for i in 0..100_000 {
    	writeln!(output, "{i}").unwrap();
    }
    
    println!("{output}");
}

제가 가장 많이 쓰는 방식입니다.
출력이 한 줄 이상이면 무조건 이 방식을 사용합니다. 2번 방식과 비슷하지만, String 인스턴스를 버퍼로 활용한다는 점이 차이점입니다. BufWriter 처럼 버퍼가 다 차면 쓰는 것이 아니라 출력해야할 것들을 일단 String에 다 쓰고 한 번에 Stdout으로 출력하기 때문에 메모리가 얼마나 필요할지 가늠할 수 없다는 단점이 있습니다. 하지만 대부분의 경우에 메모리는 별 문제가 안되기 때문에 2번 방식보다 좀더 속도가 빠른 이 방식을 많이 사용하게 되는 것 같습니다.


일단 이 정도인 것 같습니다. 나중에 더 추가하겠습니다.

profile
웹 풀스택 개발 공부중입니다.

0개의 댓글