Saltar al contenido
ES EN
Tema: automático

Ejercicio resuelto: evaluar expresiones con pila y recursión en Python

Evalúa expresiones con paréntesis anidados y operaciones apiladas usando una pila explícita y una cola, y compara con la solución recursiva en Python.

Actualizado el

Código de Examen Python Estructuras de datos Nivel examen Código probado

Código de Examen: Ejercicio resuelto: evaluar expresiones con pila y recursión en Python
Estructuras de Datos · Examen parcialEjercicio 2 · 3,0 puntos · 30 min

Una calculadora de laboratorio recibe expresiones aritméticas enteras escritas en una línea, con los operadores +, -, *, /, paréntesis y espacios opcionales entre símbolos. Los números son enteros no negativos de una o más cifras. La multiplicación y la división tienen mayor precedencia que la suma y la resta, y todos los operadores se asocian por la izquierda.

Debes escribir un programa que lea de la entrada estándar un entero n en la primera línea y, a continuación, n líneas, cada una con una expresión. Para cada expresión, el programa imprime en una línea la notación posfija (tokens separados por un espacio) seguida de = y el valor calculado. El formato es exactamente posfija = valor.

Reglas del enunciado: (1) la división es entera y trunca hacia cero, como en C (por tanto 7 / 2 vale 3 y (0 - 7) / 2 vale -3); (2) si la expresión divide entre cero, en lugar del valor se escribe ERROR, pero la posfija se imprime igualmente; (3) si la línea está vacía o solo contiene espacios, se imprime VACIA; (4) puedes suponer que las expresiones no vacías están bien formadas, y que no existen operadores unarios (el negativo se obtiene con 0 - x).

Debes usar una cola (collections.deque) para la lista de tokens, una pila (lista de Python) para los operadores en la conversión y otra pila para los operandos en la evaluación. Además, implementa una segunda función recursiva que evalúe la misma expresión por descenso recursivo; el programa debe comprobar que ambos resultados coinciden y, si no, abortar con una excepción (no debería ocurrir).

Entrada de ejemplo

3
3 + 4 * 2
(3 + 4) * 2
10 - 4 - 3

Salida esperada

3 4 2 * + = 11
3 4 + 2 * = 14
10 4 - 3 - = 3
Tokenización correcta de la entrada (números de varias cifras, operadores y paréntesis) con una cola
0,5
Conversión a posfija con pila de operadores respetando precedencia y asociatividad por la izquierda
1,0
Evaluación de la posfija con pila de operandos, incluida la división entera truncada
0,75
Versión recursiva equivalente (descenso recursivo) con el mismo resultado
0,5
Tratamiento de casos límite: entrada vacía, un solo número y división por cero
0,25

Pistas

Pista 1 · ¿Cómo separo los números de varias cifras de los operadores?

Recorre la cadena con un índice. Si el carácter es un dígito, sigue avanzando mientras sigan siendo dígitos y acumula el número como un único token. Cualquier otro carácter no blanco es un token de un solo símbolo. Guarda los tokens en un deque para ir consumiéndolos por la izquierda con popleft.

Pista 2 · ¿Cuándo se desapila un operador al convertir a posfija?

Al leer un operador, mientras el tope de la pila sea otro operador (no un paréntesis abierto) con precedencia mayor o igual que el actual, sácalo y envíalo a la salida. Como la asociatividad es por la izquierda, la igualdad también desapila. Después apila el operador actual. El paréntesis cerrado desapila hasta encontrar el abierto.

Pista 3 · ¿Cómo estructuro la versión recursiva?

Piensa en tres funciones que se llaman entre sí: expresión (sumas y restas), término (productos y cocientes) y factor (un número o una expresión entre paréntesis). La gramática refleja la precedencia: cada nivel llama al nivel inmediatamente más fuerte. Cuidado con la división truncada hacia cero: // de Python redondea hacia menos infinito.

Solución

Solución explicada

La solución separa el problema en cuatro piezas pequeñas: tokenizar produce la cola de tokens, a_posfija convierte con la pila de operadores, evaluar_posfija calcula con la pila de operandos y evaluar_recursivo resuelve lo mismo con tres funciones mutuamente recursivas. El main lee, imprime y verifica que las dos vías dan el mismo valor.

El detalle más traicionero es la división: Python usa // con redondeo hacia menos infinito, así que -7 // 2 da -4, mientras que el enunciado exige -3. La función auxiliar dividir calcula el cociente con valores absolutos y le restituye el signo. La división por cero se señala con una excepción propia que el main captura para escribir ERROR.

import sys
from collections import deque

PREC = {'+': 1, '-': 1, '*': 2, '/': 2}


class DivisionPorCero(Exception):
    pass


def tokenizar(linea):
    cola = deque()
    i = 0
    n = len(linea)
    while i < n:
        c = linea[i]
        if c.isspace():
            i += 1
        elif c.isdigit():
            j = i
            while j < n and linea[j].isdigit():
                j += 1
            cola.append(linea[i:j])
            i = j
        else:
            cola.append(c)
            i += 1
    return cola


def dividir(a, b):
    if b == 0:
        raise DivisionPorCero()
    q = abs(a) // abs(b)
    return q if (a >= 0) == (b >= 0) else -q


def aplicar(op, a, b):
    if op == '+':
        return a + b
    if op == '-':
        return a - b
    if op == '*':
        return a * b
    return dividir(a, b)


def a_posfija(tokens):
    cola = deque(tokens)
    salida = []
    pila = []
    while cola:
        t = cola.popleft()
        if t.isdigit():
            salida.append(t)
        elif t == '(':
            pila.append(t)
        elif t == ')':
            while pila[-1] != '(':
                salida.append(pila.pop())
            pila.pop()
        else:
            while pila and pila[-1] != '(' and PREC[pila[-1]] >= PREC[t]:
                salida.append(pila.pop())
            pila.append(t)
    while pila:
        salida.append(pila.pop())
    return salida


def evaluar_posfija(posfija):
    pila = []
    for t in posfija:
        if t.isdigit():
            pila.append(int(t))
        else:
            b = pila.pop()
            a = pila.pop()
            pila.append(aplicar(t, a, b))
    return pila[0]


def evaluar_recursivo(tokens):
    cola = deque(tokens)

    def expresion():
        valor = termino()
        while cola and cola[0] in ('+', '-'):
            op = cola.popleft()
            valor = aplicar(op, valor, termino())
        return valor

    def termino():
        valor = factor()
        while cola and cola[0] in ('*', '/'):
            op = cola.popleft()
            valor = aplicar(op, valor, factor())
        return valor

    def factor():
        t = cola.popleft()
        if t == '(':
            valor = expresion()
            cola.popleft()
            return valor
        return int(t)

    return expresion()


def main():
    lineas = sys.stdin.read().split('\n')
    n = int(lineas[0].strip())
    salida = []
    for k in range(1, n + 1):
        linea = lineas[k] if k < len(lineas) else ''
        tokens = list(tokenizar(linea))
        if not tokens:
            salida.append('VACIA')
            continue
        posfija = a_posfija(tokens)
        texto = ' '.join(posfija)
        try:
            valor = evaluar_posfija(posfija)
            if valor != evaluar_recursivo(tokens):
                raise RuntimeError('las dos evaluaciones no coinciden')
            salida.append(texto + ' = ' + str(valor))
        except DivisionPorCero:
            salida.append(texto + ' = ERROR')
    print('\n'.join(salida))


main()

tokenizar recorre la línea con el índice i. Los espacios se saltan; si encuentra un dígito, avanza j mientras siga habiendo dígitos y añade el trozo completo a la cola, de modo que 12 es un único token. Cualquier otro carácter (operador o paréntesis) es un token de un símbolo. Si la lista de tokens queda vacía, el main imprime VACIA, que es el caso de la prueba con línea en blanco.

a_posfija es el algoritmo clásico de la pila de operadores. Los números pasan directos a salida. Un ( se apila; un ) desapila hacia la salida hasta encontrar su pareja, que se descarta. Para un operador, el bucle while desapila mientras el tope tenga precedencia mayor o igual: esa igualdad es lo que da asociatividad por la izquierda. Por eso 8 - 3 - 2 se convierte en 8 3 - 2 - y vale 3, y no 7.

evaluar_posfija usa una segunda pila con los operandos. Cada operador saca primero b y después a (el orden importa en - y /) y apila aplicar(t, a, b). Al final queda un solo valor. La función dividir resuelve el truncamiento hacia cero y lanza DivisionPorCero, que main captura: en la prueba 4 / (2 - 2) se imprime la posfija 4 2 2 - / con ERROR.

evaluar_recursivo reproduce la precedencia con la estructura de las llamadas: expresion llama a termino, y este a factor. Un factor o es un número o abre un paréntesis y vuelve a llamar a expresion; ahí está la recursividad. Las funciones internas comparten la cola por cierre léxico, y consumir con popleft hace que cada llamada avance por la entrada sin necesidad de índices. El main compara ambos resultados como comprobación interna.

Los casos de prueba cubren: el ejemplo mixto del enunciado, un único número (posfija igual al propio número), paréntesis anidados con números de varias cifras, la asociatividad izquierda, la división truncada con un valor negativo obtenido mediante 0 - 7, la división por cero y la línea vacía.

Traza paso a paso · generada ejecutando el código

Pasotokencolapilasalida
1343
2+3+3
342+3 4
4*1+*3 4
520+*3 4 2
6(6(
735(3
8+4(+3
943(+3 4
10)23 4 +
11*1*3 4 +
1220*3 4 + 2
1310410
14-3-10
1542-10 4
16-1-10 4 -
1730-10 4 - 3

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo mixto3 3 + 4 * 2 (3 + 4) * 2 10 - 4 - 33 4 2 * + = 11 3 4 + 2 * = 14 10 4 - 3 - = 33 4 2 * + = 11 3 4 + 2 * = 14 10 4 - 3 - = 3OK
Un solo número2 42 (7)42 = 42 7 = 742 = 42 7 = 7OK
Paréntesis anidados y varias cifras2 ((12 + 8) * (3 + 2)) - 100 / 4 2 * (3 + (4 * (5 - 1)))12 8 + 3 2 + * 100 4 / - = 75 2 3 4 5 1 - * + * = 3812 8 + 3 2 + * 100 4 / - = 75 2 3 4 5 1 - * + * = 38OK
Asociatividad izquierda y división truncada3 8 / 2 / 2 (0 - 7) / 2 7 / 2 * 28 2 / 2 / = 2 0 7 - 2 / = -3 7 2 / 2 * = 68 2 / 2 / = 2 0 7 - 2 / = -3 7 2 / 2 * = 6OK
División por cero2 4 / (2 - 2) 5 + 14 2 2 - / = ERROR 5 1 + = 64 2 2 - / = ERROR 5 1 + = 6OK
Línea vacía y espacios3 1+2VACIA VACIA 1 2 + = 3VACIA VACIA 1 2 + = 3OK

Salidas reales: código compilado con Python 3.13.16 y ejecutado en un contenedor aislado el 5 de noviembre de 2026.

Complejidad

Sea m el número de tokens de una expresión. Tokenizar recorre cada carácter una vez: O(m). La conversión a posfija apila y desapila cada token como mucho una vez, así que es O(m). La evaluación de la posfija es otra pasada O(m). La versión recursiva consume cada token exactamente una vez con popleft, que en deque es O(1), luego también es O(m).

En memoria, la cola, la salida y las pilas son O(m) en el peor caso. La versión recursiva añade la profundidad de la pila de llamadas, proporcional al anidamiento de paréntesis, que en el peor caso es O(m) (por ejemplo, ((((1))))). Con las expresiones de examen no hay riesgo de superar el límite de recursión de Python, pero conviene saber que existe.

Errores típicos

  • Desapilar solo con precedencia estrictamente mayor: 8 - 3 - 2 se evalúa como 8 - (3 - 2) y da 7 en lugar de 3, porque se pierde la asociatividad por la izquierda.
  • Usar // de Python directamente para la división: con operandos negativos redondea hacia menos infinito y (0 - 7) / 2 daría -4 en lugar de -3.
  • Sacar los operandos en el orden equivocado al evaluar: el primer pop es el operando derecho b; intercambiarlos rompe la resta y la división.
  • Tratar cada carácter como un token y partir 12 en 1 y 2, o usar list.pop(0) como cola y degradar el coste a O(m²).
  • Olvidar vaciar la pila de operadores al final de la conversión, de modo que los últimos operadores nunca llegan a la salida.
  • No descartar el paréntesis abierto al cerrar, dejándolo en la salida o en la pila y corrompiendo la posfija.

Variantes

Añadir potencia y menos unario

Introduce el operador ^ con la precedencia más alta y asociatividad por la derecha: en la conversión, al comparar, desapila solo con precedencia estrictamente mayor cuando el operador actual es ^. En la versión recursiva, añade un nivel potencia entre termino y factor que llame a sí mismo para el exponente. El menos unario se resuelve en factor: si el token es -, devuelve -factor().

Evaluar sin construir la posfija

Usa dos pilas a la vez, una de operandos y otra de operadores, y aplica un operador en cuanto llega otro de precedencia menor o igual (algoritmo de las dos pilas). Se obtiene el valor en una sola pasada, pero se pierde la notación posfija intermedia, que aquí es parte de la salida pedida.

En los temarios

El tema de este ejercicio (pilas, colas y recursión) aparece como bloques independientes en el temario de Estructuras de Datos de la Facultad de Ciencias de la UNAM, que lista Recursión, Pilas y Colas como unidades propias: temario de la UNAM. El programa de Algoritmos y Estructuras de Datos del IPN (ESCOM) menciona soluciones con pilas y soluciones con colas: programa de ESCOM-IPN.

En Colombia, el curso ISIS1206 de la Universidad de los Andes incluye pilas y colas en su calendario de estructuras de datos: página del curso. Esta referencia solo indica que el tema forma parte de esos programas; el ejercicio de esta página es original.

Historial de cambios

  1. Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
  2. Publicación original.

Artículo generado mediante AI.larebelion · Revisado por Kernel

Kernel

· Dirección de tecnología · España

«Una pila explícita y una recursión resuelven lo mismo; el examen premia al que sabe decir cuál de las dos está usando.»