파이썬 스택으로 배우는 괄호 검사 완전정복

기초 → 응용 → 심화 예제 + 자동채점 30문항 + 상세 해설 + 결과 TXT 저장

1. 괄호 검사란?

괄호 검사는 여는 괄호와 닫는 괄호가 종류와 순서에 맞게 짝을 이루는지 확인하는 문제입니다. 대표적으로 (), [], {}를 검사합니다.

문자열판정이유
{[()]}올바름가장 나중에 연 괄호부터 먼저 닫힘
([)]잘못됨[를 연 뒤 )가 먼저 등장하여 종류 불일치
((()))올바름모든 여는 괄호가 역순으로 닫힘
(()잘못됨검사 종료 후 여는 괄호가 스택에 남음
())잘못됨스택이 빈 상태에서 닫는 괄호가 등장
핵심: 괄호는 LIFO(Last In, First Out), 즉 가장 나중에 들어온 것이 가장 먼저 나오는 스택 구조와 정확히 맞습니다.

주어진 기본 코드 상세 설명

def valid_brackets(text):
    stack = []
    pair = {")": "(", "]": "[", "}": "{"}

    for ch in text:
        if ch in "([{":
            stack.append(ch)
        elif ch in ")]}":
            if not stack or stack.pop() != pair[ch]:
                return False

    return not stack
코드의미
stack = []아직 닫히지 않은 여는 괄호를 저장할 스택
pair = {...}각 닫는 괄호에 대응하는 올바른 여는 괄호를 딕셔너리로 정의
for ch in text문자열을 왼쪽에서 오른쪽으로 한 문자씩 검사
stack.append(ch)여는 괄호를 만나면 스택의 맨 뒤에 삽입
not stack스택이 비어 있으면 True. 닫는 괄호가 너무 일찍 등장했는지 검사
stack.pop()가장 최근에 저장한 여는 괄호를 꺼냄
pair[ch]현재 닫는 괄호와 짝이어야 할 여는 괄호를 얻음
return not stack모든 검사가 끝난 후 스택까지 비어 있어야 올바른 문자열

2. 기초부터 응용까지 예시 코드

예제 1. 소괄호만 검사

def check_parentheses(text):
    count = 0

    for ch in text:
        if ch == "(":
            count += 1
        elif ch == ")":
            count -= 1

        if count < 0:
            return False

    return count == 0

print(check_parentheses("(())"))   # True
print(check_parentheses("())("))   # False

한 종류의 괄호만 검사할 때는 정수 카운터도 사용할 수 있습니다. 단, 여러 종류의 괄호가 섞이면 종류와 중첩 순서를 기억해야 하므로 스택이 필요합니다.

예제 2. 여러 종류의 괄호 검사

def valid_brackets(text):
    stack = []
    pair = {")": "(", "]": "[", "}": "{"}

    for ch in text:
        if ch in "([{":
            stack.append(ch)

        elif ch in ")]}":
            if not stack:
                return False

            if stack.pop() != pair[ch]:
                return False

    return not stack

print(valid_brackets("{[()]}"))  # True
print(valid_brackets("([)]"))    # False

예제 3. 검사 과정을 한 단계씩 출력

def trace_brackets(text):
    stack = []
    pair = {")": "(", "]": "[", "}": "{"}

    for i, ch in enumerate(text):
        if ch in "([{":
            stack.append(ch)
            print(i, ch, "push ->", stack)

        elif ch in ")]}":
            if not stack:
                print(i, ch, "오류: 대응할 여는 괄호 없음")
                return False

            top = stack.pop()
            print(i, ch, "pop", top, "->", stack)

            if top != pair[ch]:
                print("오류: 괄호 종류 불일치")
                return False

    if stack:
        print("오류: 닫히지 않은 괄호", stack)
        return False

    return True

예제 4. 일반 문자가 섞인 수식 검사

def check_expression(expr):
    stack = []
    pair = {")": "(", "]": "[", "}": "{"}

    for ch in expr:
        if ch in "([{":
            stack.append(ch)
        elif ch in ")]}":
            if not stack or stack.pop() != pair[ch]:
                return False

    return not stack

print(check_expression("a * (b + c[2])"))       # True
print(check_expression("func(a[2} + b)"))        # False

괄호가 아닌 문자는 아무 동작 없이 건너뜁니다.

예제 5. 오류 위치와 이유 반환

def check_with_reason(text):
    stack = []
    pair = {")": "(", "]": "[", "}": "{"}

    for i, ch in enumerate(text):
        if ch in "([{":
            stack.append((ch, i))

        elif ch in ")]}":
            if not stack:
                return False, f"{i}번 위치: 여는 괄호가 없습니다."

            open_ch, open_i = stack.pop()

            if open_ch != pair[ch]:
                return False, (
                    f"{i}번 위치: {open_ch}와 {ch}는 짝이 아닙니다."
                )

    if stack:
        ch, i = stack[-1]
        return False, f"{i}번 위치의 {ch}가 닫히지 않았습니다."

    return True, "모든 괄호가 올바르게 짝지어졌습니다."

예제 6. 괄호의 최대 중첩 깊이 구하기

def max_depth(text):
    stack = []
    pair = {")": "(", "]": "[", "}": "{"}
    maximum = 0

    for ch in text:
        if ch in "([{":
            stack.append(ch)
            maximum = max(maximum, len(stack))

        elif ch in ")]}":
            if not stack or stack.pop() != pair[ch]:
                return -1

    if stack:
        return -1

    return maximum

print(max_depth("{[()]}"))   # 3

정상 문자열이면 최대 스택 크기가 곧 최대 괄호 중첩 깊이입니다.

3. 응용·심화 개념

시간 복잡도와 공간 복잡도

문자열 길이를 n이라 하면 각 문자를 한 번씩 확인하므로 시간 복잡도는 O(n)입니다. 최악의 경우 문자열 전체가 여는 괄호일 수 있으므로 스택 공간은 O(n)까지 필요합니다.

왜 단순 개수 비교만으로는 부족한가?

([)]는 소괄호와 대괄호의 개수는 각각 맞지만 순서가 틀렸습니다. 따라서 여러 종류를 검사하려면 개수뿐 아니라 가장 최근의 여는 괄호 종류를 기억해야 합니다.

스택 불변식

문자열의 앞부분을 어느 지점까지 처리했을 때 스택에는 정확히 아직 닫히지 않은 여는 괄호들이 등장 순서대로 들어 있습니다. 이 성질이 알고리즘의 정확성을 설명하는 핵심입니다.

심화 1. 사용자 정의 괄호 추가

def valid_custom(text):
    stack = []
    pair = {
        ")": "(",
        "]": "[",
        "}": "{",
        ">": "<"
    }
    opens = set(pair.values())

    for ch in text:
        if ch in opens:
            stack.append(ch)
        elif ch in pair:
            if not stack or stack.pop() != pair[ch]:
                return False

    return not stack

심화 2. 모든 오류를 한꺼번에 찾는 방식의 한계

첫 번째 짝 불일치 이후에는 어느 괄호를 기준으로 계속 검사할지 모호해질 수 있습니다. 따라서 문법 검사기는 보통 첫 오류를 보고하거나, 오류 복구 규칙을 별도로 설계합니다.

심화 3. 문자열 리터럴 속 괄호 무시하기

def check_code_line(text):
    stack = []
    pair = {")": "(", "]": "[", "}": "{"}
    quote = None

    for ch in text:
        if ch in "'\"":
            if quote is None:
                quote = ch
            elif quote == ch:
                quote = None
            continue

        if quote is not None:
            continue

        if ch in "([{":
            stack.append(ch)
        elif ch in ")]}":
            if not stack or stack.pop() != pair[ch]:
                return False

    return not stack and quote is None
실제 Python 소스 전체를 정확하게 분석하려면 이 단순 예제보다 복잡합니다. 이스케이프 문자, 삼중 따옴표, 주석 등을 함께 처리해야 하므로 실제 컴파일러·파서는 토큰화 단계를 사용합니다.

4. 5지선다형 10문항 — 옳은 것 3개 고르기

각 문항에서 반드시 3개를 선택하세요. 문항당 4점, 총 40점.

5. 개념·코드 OX 퀴즈 10문항

문항당 3점, 총 30점.

6. 답이 분명한 단답형 10문항

문항당 3점, 총 30점. 영문은 대소문자를 구분하지 않습니다.

7. 채점 및 결과 저장

아직 채점하지 않았습니다.