lunes, 30 de septiembre de 2019

2.2.1 Notación Polaca

 Notación Polaca

La notación polaca, también conocida como notación de prefijo o notación prefija, es una forma de notación para la lógica, la aritmética, el álgebra y la computación. Su característica distintiva es que coloca los operadores a la izquierda de sus operandos. Si la aridad de los operadores es fija, el resultado es una sintaxis que carece de paréntesis u otros signos de agrupación, y todavía puede ser analizada sin ambigüedad. 



La notación polaca es la originada por un Autómata con pila, en la que los operadores siempre preceden a los operandos sobre los que actúan, y que tiene la ventaja de no necesitar paréntesis:
Estándar
      Ejemplo 1: 2 * ( 3 + 5 )
      Ejemplo 2: 2 * 3 + 5
Polaca
      Ejemplo 1: * 2 + 3 5
      Ejemplo 2: + * 2 3 5

2.2 Representaciones de código Intermedio

Representaciones de código Intermedio

En el proceso de traducir un programa fuente a código destino, un compilador puede construir una o más representaciones intermedias, las cuales pueden tener una variedad de formas. Los árboles sintácticos son una forma de representación intermedia; por lo general, se utilizan durante el análisis sintáctico y semántico.
Después del análisis sintáctico y semántico del programa fuente, muchos compiladores generan un nivel bajo explícito, o una representación intermedia similar al código máquina, que podemos considerar como un programa para una máquina abstracta. Esta representación intermedia debe tener dos propiedades importantes: debe ser fácil de producir y fácil de traducir en la máquina destino.
Existe una forma intermedia llamada código de tres direcciones, que consiste en una secuencia de instrucciones similares a ensamblador, con tres operandos por instrucción. Cada operando puede actuar como un registro. La salida del generador de código intermedio consiste en la secuencia de código de tres direcciones.

t1 = inttofloat(60)
t2 = id3 * t1                                
t3 = id2 + t2
id1 = t3

Hay varios puntos que vale la pena mencionar sobre las instrucciones de tres direcciones. En primer lugar, cada instrucción de asignación de tres direcciones tiene, por lo menos, un operador del lado derecho. Por ende, estas instrucciones corrigen el orden en el que se van a realizar las operaciones; la multiplicación va antes que la suma en el programa fuente. En segundo lugar, el compilador debe generar un nombre temporal para guardar el valor calculado por una instrucción de tres direcciones. En tercer lugar, algunas "instrucciones de tres direcciones" como la primera y la última en la secuencia anterior, tienen menos de tres operandos.

Unidad II. Notaciones Prefija,Infija y Posfija

Notaciones Prefija,Infija y Posfija.


Prefija:

Nos indica que el operador va antes de los operandos sus características principales son:
Los operandos conservan el mismo orden que la notación infija
equivalente.
-No requiere de paréntesis para indicar el orden de precedencia de
operadores ya que el es una operación.
-Se evalúa de izquierda a derecha hasta que encontrémosle primer
operador seguido inmediatamente de un par de operandos.
-Se evalúa la expresión binaria y el resultado se cambia como un nuevo
operando.
Se repite este hasta que nos quede un solo resultado.

Notación prefija: El orden es operador, primer operando, segundo.
Infija:

Es la forma más común que utilizamos para escribir expresiones
matemáticas, estas notaciones se refiere a que el operador esta entre
los operandos. La notación infija puede estar completamente
parentizada o puede basarse en un esquema de precedencia de
operadores así como el uso de paréntesis para invalidar los arreglos al
expresar el orden de evaluación de una expresión:
3*4=12
3*4+2=14
3*(4+2)=18
[size=12]

Notación infija: La notación habitual. El orden es primer operando, operador, segundo operando.
Posfija

Como su nombre lo indica se refiere a que el operador ocupa la posición
después de los operandos sus características principales son:
-El orden de los operandos se conserva igual que la expresión infija
equivalente no utiliza paréntesis ya que no es una operación ambigua.
La operación posfija no es exactamente lo inverso a la operación prefija
equivalente:
(A+B)*C AB+C*

Notación postfija: El orden es primer operando, segundo operando, operador.

EJEMPLO:

Si deseamos representar las expresiones (2+(3*4)) = x y ((2+3)*4)= x en las tres notaciones mencionadas, el resultado sería:



(2+(3*4)) = x
((2+3)*4) = x

Notación prefija
= + 2 * 3 4 x
= * + 2 3 4 x

Notación infija
2+3*4 = x
(2+3)*4 = x

Notación postfija
2 3 4 * + x =
2 3 + 4 * x =

jueves, 5 de septiembre de 2019

Pila semántica de un analizador sintáctico


Pila semántica en un analizador sintáctico

Las pilas y colas son estructuras de datos que se utilizan generalmente para simplificar ciertas operaciones de programación. Estas estructuras pueden implementarse mediante arrays o listas enlazadas.

Pila: colección de datos a los cuales se les puede acceder mediante un extremo, que se conoce generalmente como tope. Las pilas tienen dos operaciones básicas:
      Push (para introducir un elemento)
      Pop (para extraer un elemento)


Sus características fundamentales es que al extraer se obtiene siempre el último elemento que acabe de insertarse. Por esta razón también se conoce como estructuras de datos LIFO, una posible implementación mediante listas enlazadas seria insertando y extrayendo siempre por el principio de la lista.
Las pilas se utilizan en muchas aplicaciones que utilizamos con frecuencia. Las pilas y colas son estructuras de datos que se utilizan generalmente para simplificar ciertas operaciones de programación. Estas estructuras pueden implementarse mediante arrays o listas enlazadas.
Un analizador sintáctico es un autómata de pila que reconoce la estructura de una cadena de componentes léxicos.
En general, el analizador sintáctico inicializa el compilador y para cada símbolo de entrada llama al analizador morfológico y proporciona el siguiente símbolo de entrada.
Al decir pila semántica no se refiere a que hay varios tipos de pila, hace referencia a que se debe programar única y exclusivamente en un solo lenguaje, es decir, no podemos mezclar código de C++ con Visual Basic.

Ventajas

      Los problemas de integración entre los subsistemas son sumamente costosos y muchos de ellos no se solucionan hasta que la programación alcanza la fecha límite para la integración total del sistema.
      Se necesita una memoria auxiliar que nos permita guardar los datos para poder hacer la comparación.
Objetivo teórico
Es construir un árbol de análisis sintáctico, este raramente se construye como tal, sino que las rutinas semánticas integradas van generando el árbol de Sintaxis abstracta. Se especifica mediante una gramática libre de contexto.
El análisis semántico detecta la validez semántica de las sentencias aceptadas por el analizador sintáctico. El analizador semántico suele trabajar simultáneamente al analizador sintáctico y en estrecha cooperación. Se entiende por semántica como el conjunto de reglas que especifican el significado de cualquier sentencia sintácticamente correcta y escrita en un determinado lenguaje.
Las rutinas semánticas deben realizar la evaluación de los atributos de las gramáticas siguiendo las reglas semánticas asociadas a cada producción de la gramática.
El análisis sintáctico es la fase en la que se trata de determinar el tipo de los resultados intermedios, comprobar que los argumentos que tiene un operador pertenecen al conjunto de los operadores posibles, y si son compatibles entre sí, etc.
En definitiva, comprobará que el significado de la que se va leyendo es válido. La salida teórica de la fase de análisis semántico sería un árbol semántico. Consiste en un árbol sintáctico en el que cada una de sus ramas ha adquirido el significado que debe tener.
Se compone de un conjunto de rutinas independientes, llamadas por los analizadores morfológico y sintáctico. El análisis semántico utiliza como entrada el árbol sintáctico detectado por el análisis sintáctico para comprobar restricciones de tipo y otras limitaciones semánticas y preparar la generación de código.
Las rutinas semánticas suelen hacer uso de una pila que contiene la información semántica asociada a los operadores en forma de registros semánticos.

Reglas semánticas

Son el conjunto de normas y especificaciones que definen al lenguaje de programación y están dadas por la sintaxis del lenguaje, las reglas semánticas asignan un significado lógico a ciertas expresiones definidas en la sintaxis del lenguaje.
La evaluación de las reglas semánticas define los valores de los atributos en los nodos del árbol de análisis sintáctico para la cadena de entrada. Una regla semántica también puede tener efectos colaterales, por ejemplo, imprimir un valor o actualizar una variable global.

Compatibilidad de tipos

Durante la fase de análisis semántico, el compilador debe verificar que los tipos y valores asociados a los objetos de un programa se utilizan de acuerdo con la especificación del lenguaje.
Además debe detectar conversiones implícitas de tipos para efectuarlas o insertar el código apropiado para efectuarlas así como almacenar información relativa a los tipos de los objetos y aplicar las reglas de verificación de tipos.
Analizadores descendentes:
Parten del axioma inicial de la gramática, se va descendiendo utilizando las derivaciones izquierdas, hasta llegar a construir la cadena analizada.
Se va construyendo el árbol desde sus nodos terminales. Es decir, se construye desde los símbolos de cadena hasta llegar al axioma de la gramática.

Bottom up

Es un principio de muchos años del estilo de programación que los elementos funcionales de un programa no deben ser demasiado grandes. Si un cierto componente de un programa crece más allá de la etapa donde está fácilmente comprensible, se convierte en una masa de la complejidad que encubre errores tan fácilmente como una ciudad grande encubre a fugitivos.

Top-down

Este método consiste en dividir los problemas en subproblemas más sencillos para conseguir una solución más rápida. El diseño descendente es un método para resolver el problema que posteriormente se traducirá a un lenguaje compresible por la computadora.
Un parser ascendente utiliza durante el análisis una pila. En esta va guardando datos que le permiten ir haciendo las operaciones de reducción que necesita.
Para incorporar acciones semánticas como lo es construir el árbol sintáctico, es necesario incorporar a la pila del parser otra columna que guarde los atributos de los símbolos que se van analizando. Estos atributos estarían ligados a la correspondiente producción en la tabla de parsing.
La pila juega un papel fundamental en el desarrollo de cualquier analizador semántico. Dentro de cada elemento de la pila se guardan los valores que pueden tener una expresión.



                                       




lunes, 2 de septiembre de 2019

Recorridos de arboles binarios

Preorden:
In-Orden

Post-Orden


Recorrido y búsqueda de arboles

Recorrido y búsqueda en arboles

Según el libro de T. Parajan nos dice que:
El recorrido de un árbol es el proceso para recorrer (desplazarse a lo largo) un árbol de manera sistemática a fin de que cada vértice se visite y procese exactamente una vez .Hay tres métodos para recorrer un árbol binario a saber recorridos de preorden , de inorden y de posorden.
Según el libro de JOHNSONBAUGH Richard pag. 415 nos dice que :
La búsqueda a lo ancho y la búsqueda a profundidad proporcionan formas de recorrer un arbol , es decir de recorrerlo de manera sistemática de modo que cada vértice sea visitado exactamente una vez.
Recorrido preorden :
Para recorrer un árbol binario no vació en preorden, hay que realizar las siguientes operaciones recursivamente en cada nodo, comenzando con el nodo de raiz:
1.Visite la raíz
2.Atraviese el sub-arbol izquierdo
3.Atraviese el sub-arbol derecho

Preorden: ABDGEHICFJK

Recorrido inorden :
Para recorrer un arbol binario no vació en inorden (simétrico), hay que realizar las siguientes operaciones recursivamente en cada nodo:
1.Atraviese el sub-arbol izquierdo
2.Visite la raíz
3.Atraviese el sub-arbol derecho

Inorden: GDBHEIACJKF

Recorrido posorden :
Para recorrer un árbol binario no vació en postorden, hay que realizar las siguientes operaciones recursivamente en cada nodo:
1.Atraviese el sub-arbol izquierdo
2.Atraviese el sub-arbol derecho
3.Visite la raíz

Postorden: GDHIEBKJFCA

Búsqueda en Arboles

Esto implica examinar cada parte del árbol hasta que el vértice o la arista deseada sea encontrada. Podríamos profundizar moviéndonos a un vértice siempre que sea posible o podríamos des plegarnos comprobando todos los vertices en un nivel antes de pasar al siguiente.
Búsqueda en profundidad:
La idea básica de la búsqueda en profundidad es penetrar tan profundamente como sea posible antes de desplegarse a otros vértices .Esto se consigue al tomar el nuevo vértice adyacente al ultimo de los posibles vértices anteriores.

Búsqueda en anchura:
La idea básica de la búsqueda en anchura es desplegarse a tantos vértices como sea posible antes de penetrar en profundidad dentro de un árbol. Esto significa que visitaremos todos los vértices adyacentes a uno dado antes de cambiar de nivel.

Algoritmos de recorridos preorden, inorden y postorden

Arboles en Java: Recorrido Preorden, Inorden y Postorden


El recorrido de árboles refiere al proceso de visitar de una manera sistemática, exactamente una vez, cada nodo en una estructura de datos de árbol (examinando y/o actualizando los datos en los nodos).

Preorden: (raíz, izquierdo, derecho). Para recorrer un árbol binario no vacío en preorden, hay que realizar las siguientes operaciones recursivamente en cada nodo, comenzando con el nodo de raíz:
  1. Visite la raíz
  2. Atraviese el sub-árbol izquierdo
  3. Atraviese el sub-árbol derecho
Inorden: (izquierdo, raíz, derecho). Para recorrer un árbol binario no vacío en inorden (simétrico), hay que realizar las siguientes operaciones recursivamente en cada nodo:
  1. Atraviese el sub-árbol izquierdo
  2. Visite la raíz
  3. Atraviese el sub-árbol derecho
Postorden: (izquierdo, derecho, raíz). Para recorrer un árbol binario no vacío en postorden, hay que realizar las siguientes operaciones recursivamente en cada nodo:
  1. Atraviese el sub-árbol izquierdo
  2. Atraviese el sub-árbol derecho
  3. Visite la raíz
En general, la diferencia entre preorden, inorden y postorden es cuándo se recorre la raíz. En los tres, se recorre primero el sub-árbol izquierdo y luego el derecho.
  • En preorden, la raíz se recorre antes que los recorridos de los subárboles izquierdo y derecho
  • En inorden, la raíz se recorre entre los recorridos de los árboles izquierdo y derecho, y
  • En postorden, la raíz se recorre después de los recorridos por el subárbol izquierdo y el derecho
public class NodoArbol
{
    //miembros de acceso
    NodoArbol nodoizquierdo;
    int datos;
    NodoArbol nododerecho;
    
    //iniciar dato y hacer de este nodo un nodo hoja
    public NodoArbol(int datosNodo)
    {
        datos = datosNodo;
        nodoizquierdo = nododerecho = null; //el nodo no tiene hijos
    }
    
    //buscar punto de insercion e inserter nodo nuevo
    public synchronized void insertar(int valorInsertar)
    {
        //insertar en subarbol izquierdo
        if(valorInsertar < datos)
        {
            //insertar en subarbol izquierdo
            if(nodoizquierdo == null)
                nodoizquierdo = new NodoArbol(valorInsertar);
            else    //continua recorriendo subarbol izquierdo
                nodoizquierdo.insertar(valorInsertar);
        }
        
        //insertar nodo derecho
        else if(valorInsertar > datos)
        {
            //insertar nuevo nodoArbol
            if(nododerecho == null)
                nododerecho = new NodoArbol(valorInsertar);
            else
                nododerecho.insertar(valorInsertar);
        }
    } // fin del metodo insertar
}

class Arbol
{
    private NodoArbol raiz;
    
    //construir un arbol vacio
    public Arbol()
    {
        raiz = null;
    }
    
    //insertar un nuevo ndo en el arbol de busqueda binaria
    public synchronized void insertarNodo(int valorInsertar)
    {
        if(raiz == null)
            raiz = new NodoArbol(valorInsertar); //crea nodo raiz
        else
            raiz.insertar(valorInsertar); //llama al metodo insertar        
    }
    
    // EMPIEZA EL RECORRIDO EN PREORDEN
    public synchronized void recorridoPreorden()
    {
        ayudantePreorden(raiz);
    }
    //meoto recursivo para recorrido en preorden
    
    private void ayudantePreorden(NodoArbol nodo)
    {
        if(nodo == null)
            return;
        
        System.out.print(nodo.datos + " ");     //mostrar datos del nodo
        ayudantePreorden(nodo.nodoizquierdo);   //recorre subarbol izquierdo
        ayudantePreorden(nodo.nododerecho);     //recorre subarbol derecho
    }
    
    //EMPEZAR RECORRIDO INORDEN
    public synchronized void recorridoInorden()
    {
        ayudanteInorden(raiz);
    }
    
    //meoto recursivo para recorrido inorden
    private void ayudanteInorden( NodoArbol nodo)
    {
        if(nodo == null)
            return;
        
        ayudanteInorden(nodo.nodoizquierdo);
        System.out.print(nodo.datos + " ");
        ayudanteInorden(nodo.nododerecho);
    }
    
    //EMPEZAR RECORRIDO PORORDEN
    public synchronized void recorridoPosorden()
    {
        ayudantePosorden(raiz);        
    }
    
    //meotod recursivo para recorrido posorden
    private void ayudantePosorden(NodoArbol nodo)
    {
        if( nodo == null )
            return;
        
        ayudantePosorden(nodo.nodoizquierdo);
        ayudantePosorden(nodo.nododerecho);
        System.out.print(nodo.datos + " ");
    }
}