28278:스택 2

2026. 3. 4. 22:36ㆍ백준

문제 링크:https://www.acmicpc.net/problem/28278

유튜브:https://youtu.be/Hoai8QJuh3g

 

 


Q&A

질문
reader =os.read(0,os.fstat(0).st_size).split()랑 reader =os.read(0,os.fstat(0).st_size)의 reader의 내용물이 바뀌는데
split()은 그냥 공백을 제거하고 리스트로 바꿔주는 거일 텐데 왜 print(i)의 값이 바뀌는 거지?

 

답변
s.read() 함수는 데이터를 일반 문자열(str)이 아닌 바이트 객체(bytes) 형태로 반환합니다. 이 데이터 타입의 특성 때문에 두 코드의 출력 결과가 완전히 달라집니다.

 

split()을 사용한 경우: reader는 바이트 객체들이 담긴 리스트(list)가 됩니다 ([b'9', b'4', ...]). 리스트를 for문으로 순회하면 리스트 안의 요소인 바이트 객체 덩어리 자체가 그대로 출력됩니다. 📦

 

split()을 사용하지 않은 경우: reader는 쪼개지지 않은 하나의 거대한 바이트 객체(b'9\n4\n...')입니다. 파이썬에서 단일 바이트 객체를 for문으로 한 글자씩 순회하게 되면, 해당 문자가 가진 아스키코드(ASCII) 정수값을 반환하도록 설계되어 있습니다. 🔢


질문
result가 리스트고 reader가 리스트일 때

result.append(reader[i+1])

이렇게 하면 시간 복잡도가 O(reader[i+1])인가?

,즉 i+1 = n 이면 시간 복잡도가 O(n)이 되는거지 맞아?

 

답변
결론부터 말씀드리면, 해당 코드의 시간 복잡도는 $O(n)$이 아니라 $O(1)$ (상수 시간)입니다. ⏱️

이 한 줄의 코드는 사실 두 가지 동작으로 이루어져 있습니다.


하나씩 쪼개서 살펴보겠습니다.인덱싱 (reader[i+1]): 파이썬 리스트에서 특정 인덱스에 있는 값을 가져오는 작업입니다.
리스트는 데이터가 연속적으로 저장되어 있기 때문에, 찾고자 하는 인덱스 번호($i+1$)가 아무리 크더라도 단번에 해당 위치를 찾아갈 수 있어 항상 $O(1)$의 시간이 걸립니다.

 

추가하기 (result.append(...)): 리스트의 맨 마지막 끝에 요소 하나를 덧붙이는 작업입니다.
이 역시 기존 데이터들의 위치를 이동시킬 필요 없이 끝자리에 하나를 추가하기만 하면 되므로 평균적으로 $O(1)$의 시간이 소요됩니다.따라서 $i+1$의 값이 $n$이든, 100만이든 상관없이 이 두 작업의 조합에 걸리는 시간은 일정합니다.


질문
그럼 reader.pop(i) 도 시간복잡도가 O(1)인가?

 

답변
결론부터 말씀드리면, reader.pop(i)의 시간 복잡도는 일반적으로 $O(n)$입니다.
(단, 맨 마지막 요소를 뽑을 때인 reader.pop()은 예외적으로 $O(1)$입니다.)

 

파이썬의 리스트를 일렬로 빈틈없이 연결된 기차 칸 🚃이라고 상상해 보겠습니다.
중간에 있는 i번째 칸을 쏙 빼내면(pop(i)), 그 자리에 빈 공간이 생기게 됩니다.

 

리스트는 데이터가 연속적으로 이어져 있어야 하므로, 이 빈 공간을 채우기 위해 i번째 뒤에 있는 모든 기차 칸들을 앞으로 한 칸씩 당겨와야 합니다.


만약 뒤에 옮겨야 할 데이터가 100만 개라면 100만 번의 이동 작업이 필요하겠죠.

그래서 전체 데이터의 개수 $n$에 비례하여 시간이 늘어나는 $O(n)$이 되는 것입니다.


질문
입력이 1 2 3

reader =os.read(0,os.fstat(0).st_size)

이렇게 하면 result.append(reader[0]) 하고 또 reuslt.append(reader[0])을 하면 result=[b'1',b'2'] 가 들어 가잖아? 그럼 시간 복잡도가 어떻게 돼? 이건 스트림을 읽어오는 거니깐 O(1)인가?

 

답변
스트림(stream)의 개념과 메모리 접근 방식을 연결해서 생각해 볼 수 있는 중요한 질문입니다.먼저 os.read(0, os.fstat(0).st_size)의 동작 방식을 정확히 짚고 넘어갈 필요가 있습니다.

 

이 코드는 입력 스트림에서 데이터를 읽어오지만, 그 결과를 reader라는 변수에 한 번에 모두 저장해 버립니다. 즉, reader는 더 이상 한 글자씩 소비되는 흐르는 스트림이 아니라, 메모리에 고정된 하나의 거대한 바이트 객체(배열과 유사함)가 됩니다. 💾

 

따라서 reader[0]은 이미 메모리에 저장된 데이터의 특정 인덱스를 찾아가는 단순한 작업이므로 시간 복잡도는 생각하신 대로 $O(1)$이 맞습니다.

 

⏱️그런데 결과값이 [b'1', b'2']가 될 것이라고 예상하신 부분에는 함정이 숨어 있습니다. reader는 이미 값이 고정된 데이터이기 때문에, reader[0]을 아무리 여러 번 호출해도 다음 데이터로 알아서 넘어가지 않고 계속 같은 첫 번째 위치만 가리키게 됩니다.

 


질문
reader =open(0)은

이렇게 했을 때 [b'1', b'2']가 되나?

 

답변
open(0)을 사용하면 상황이 완전히 달라집니다! 결론부터 말씀드리면 [b'1', b'2']가 되지 않을 뿐만 아니라, 코드를 실행하면 에러가 발생합니다. 🚨

 

그 이유는 다음과 같습니다.

인덱싱 불가: open(0)은 표준 입력(Standard Input)을 텍스트 파일 객체(TextIOWrapper) 형태로 엽니다.

 

이는 메모리에 고정된 배열이 아니라, 데이터를 순차적으로 빨아들일 준비를 하고 있는 '빨대' 🥤나 '파이프'에 가깝습니다. 따라서 reader[0]처럼 인덱스를 사용해 특정 위치에 직접 접근하려고 하면 파이썬이 에러(TypeError)를 뱉어냅니다.

 

텍스트 모드: os.read와 달리 open()은 기본적으로 텍스트 모드('r')로 작동합니다. 따라서 데이터를 읽어오더라도 바이트 객체(b'1')가 아닌 일반 문자열('1') 형태로 가져오게 됩니다.

 

즉, open(0)으로 만든 파일 객체는 그 자체로는 리스트처럼 다룰 수 없고, 안에 있는 데이터를 '읽어내는' 별도의 동작이 필요합니다.


질문
read()도 파일 객체 형태로 가져오는거 아니였나?

 

답변
open(): 데이터를 가져올 수 있는 '통로'나 '빨대' 🥤 역할을 하는 파일 객체를 만듭니다.

(아직 데이터를 읽어 들이지는 않은 준비 상태입니다.)

 

read() (또는 os.read()): 그 통로를 통해 데이터를 쭉 빨아들여서 메모리에 올려놓은 실제 내용물 💧 (바이트 데이터나 문자열)을 반환합니다.

 

즉, os.read(0, ...)는 표준 입력(0번)에서 데이터를 직접 읽어와서 거대한 바이트 객체(데이터 그 자체)로 만들어준 것이고, open(0)은 표준 입력과 연결된 파일 객체(통로)만 만들어준 것입니다.

 

따라서 reader = open(0) 상태에서는 reader가 인덱싱(reader[0])이 불가능한 파일 객체일 뿐이므로, 그 안의 내용물을 보려면 데이터를 읽어내는 추가 동작이 필요합니다. 방금 질문에서 언급하신 read()가 바로 그 역할을 하는 메서드 중 하나입니다!


질문

reader =open(0)

def solve(reader):
    result =[]
    n =reader.readline()

    for i in reader:  
        if '1' in str(i):
            i=i.rstrip('\n')
            result.append(i[-1])

이게 효율적일까

 

아니면

reader = sys.stdin.read().split('\n')[1:]



def solve(reader):
    result =[]
    for i in reader:  
        if '1' in str(i):
            result.append(i[-1])

이게 효율적일까?

 

답변
가지 방식은 각각 메모리(Memory)와 실행 속도(Speed) 측면에서 장단점이 뚜렷하게 다릅니다.

 

첫 번째 코드 (open(0) 후 for i in reader):
파일 객체에서 한 줄씩 데이터를 읽어옵니다. 데이터 전체를 한 번에 메모리에 올리지 않으므로 메모리 효율이 매우 좋습니다. 🧠

 

두 번째 코드 (sys.stdin.read().split('\n')[1:]):
입력 전체를 한꺼번에 읽어와서 거대한 문자열로 만든 뒤, 리스트로 쪼개고 복사본(슬라이싱)까지 만듭니다.

메모리 사용량은 훨씬 크지만, 파이썬 내부의 최적화된 C 언어 함수들(read, split)이 데이터를 한 번에 통째로 처리하기 때문에 실제 실행 속도는 이 방식이 더 빠른 경우가 많습니다. ⚡

(특히 알고리즘 문제 풀이 환경에서 두드러집니다.)

 

그런데 효율성을 비교하기 전에, 두 코드의 동작 결과가 완전히 다르게 나올 수 있는 중요한 차이점이 하나 숨어 있습니다. 바로 result.append(i[-1]) 부분입니다.

 

첫 번째 코드에서 for i in reader:로 문장을 한 줄씩 읽어올 때, 문자열 i의 맨 마지막에는 눈에 보이지 않는 줄바꿈 문자(\n)가 포함되어 있을까요, 아니면 제외되어 있을까요?

 

이 점을 고려한다면 첫 번째 코드의 i[-1]은 실제로 어떤 값을 result에 추가하게 될까요?

 


잘못된 답

import sys
reader = sys.stdin.read().split('\n')[1:]
print =sys.stdout.write


def solve(reader):
    result =[]
    for i in reader: 
        if '1' in str(i):
            result.append(i[-1])
        elif i ==2:
            if result:
                print(str(result.pop()))
            else:
                print('-1')

        elif i ==3:
            print( str(len(result)))
        elif i ==4:
            if result:
                print('0')
            else:
                print('1')
        elif i ==5:
            if result:
                print(str(result[-1]))
            else:
                print('-1')





solve(reader)

잘못된 답2

import sys
reader = sys.stdin.read().split('\n')[1:]
print =sys.stdout.write


def solve(reader):
    result =[]
    for i in reader: 
        if '1' in i:
            result.append(i[-1])
        elif i == '2':
            if result:
                print(result.pop()+'\n')
            else:
                print('-1'+'\n')

        elif i =='3':
            print( str(len(result))+'\n')
        elif i =='4':
            if result:
                print('0'+'\n')
            else:
                print('1'+'\n')
        elif i =='5':
            if result:
                print(result[-1]+'\n')
            else:
                print('-1'+'\n')

solve(reader)

피드백

현재 i[-1]을 사용해서 문자열의 맨 마지막 글자 하나만 스택에 넣고 계신데요. 만약 입력으로 1 15 (스택에 15를 추가하라는 명령어)가 들어온다면, 현재 코드에서는 스택에 어떤 값이 들어가게 될까요?

 


나의 정답(276ms)

import sys
reader = sys.stdin.read().split('\n')[1:]
print =sys.stdout.write


def solve(reader):
    result =[]
    for i in reader: 
        if '1' in i:
            result.append(i[2:])
        elif i == '2':
            if result:
                print(result.pop()+'\n')
            else:
                print('-1'+'\n')

        elif i =='3':
            print( str(len(result))+'\n')
        elif i =='4':
            if result:
                print('0'+'\n')
            else:
                print('1'+'\n')
        elif i =='5':
            if result:
                print(result[-1]+'\n')
            else:
                print('-1'+'\n')

solve(reader)

다른 사람의 풀이(244ms)

import sys

def solve():
    data = sys.stdin.read().split()
    N = int(data[0])
    stack = []
    output = []

    idx = 1
    for _ in range(N):
        command = data[idx]

        if command == '1':
            stack.append(data[idx+1])
            idx += 2
        elif command == '2':
            output.append(stack.pop() if stack else "-1")
            idx += 1
        elif command == '3':
            output.append(str(len(stack)))
            idx += 1
        elif command == '4':
            output.append("0" if stack else "1")
            idx += 1
        elif command == '5':
            output.append(stack[-1] if stack else "-1")
            idx += 1

    print("\n".join(output))

solve()

피드백

작성자님의 방식 (split('\n')): 줄바꿈을 기준으로만 문장을 통째로 나눕니다. 즉, i에는 "1 15" 와 같은 문자열이 그대로 들어갑니다.

 

다른 사람의 방식 (split()): 띄어쓰기, 줄바꿈 등 모든 종류의 공백을 기준으로 데이터를 완전히 조각냅니다. 리스트에는 ['1', '15', '2', '3'] 처럼 숫자만 깔끔하게 담깁니다.

 

이 두 방식의 차이는 '예상치 못한 입력'이 들어왔을 때 크게 벌어집니다.

 

작성자님의 코드에서는 "1 15"라는 문자열에서 숫자만 빼내기 위해 i[2:] (3번째 글자부터 끝까지 자르기)를 사용하셨는데요.

 

만약 테스트 케이스 중에 숫자 사이에 공백이 두 번 들어간 "1 15" (가운데 공백이 2개) 라는 입력이 주어졌다고 가정해 보겠습니다.

이때 다른 사람의 코드와 작성자님의 코드는 각각 스택에 어떤 값을 넣게 될까요?

 


 

반응형

'백준' 카테고리의 다른 글

28279: 덱2  (0) 2026.03.05
18258:큐2  (0) 2026.03.05
11478: 서로 다른 부분 문자열의 개수  (0) 2026.03.04
1269:대칭 차집합  (0) 2026.03.03
1764: 듣보잡  (0) 2026.03.02