Datos Estructuras y Algoritmos
Datos Estructuras y Algoritmos
DSA - Inicio
DSA - Descripción general
DSA - Configuración del entorno
Algoritmo
DSA - Conceptos básicos de algoritmos
DSA - Análisis asintótico
DSA - Algoritmos codiciosos
DSA - Divide y vencerás
DSA - Programación dinámica
Estructuras de datos
DSA - Conceptos básicos de estructura de datos
DSA - Estructura de datos de matriz
Listas vinculadas
DSA - Conceptos básicos de la lista vinculada
DSA - Lista doblemente vinculada
DSA - Lista circular vinculada
Pila y cola
DSA - Pila
DSA - Análisis de expresiones
DSA - Cola
Técnicas de búsqueda
DSA - Búsqueda lineal
DSA - Búsqueda binaria
DSA - Búsqueda de interpolación
DSA - Tabla hash
Técnicas de clasificación
DSA - Algoritmos de clasificación
DSA - Clasificación de burbujas
DSA - Clasificación de inserción
DSA - Ordenar selección
DSA - Ordenar fusión
DSA - Clasificación de shell
DSA - Clasificación rápida
Estructura de datos del gráfico
DSA - Estructura de datos del gráfico
DSA - Profundidad del primer recorrido
DSA: primer recorrido transversal
Estructura de datos de árbol
DSA - Estructura de datos de árbol
DSA - Recorrido del árbol
DSA - Árbol de búsqueda binaria
DSA - Árbol AVL
DSA - Árbol de expansión
DSA - Montón
Recursividad
DSA - Conceptos básicos de recursividad
DSA - Torre de Hanoi
DSA - Serie Fibonacci
Estructuras de datos y algoritmos:
descripción general
La Estructura de datos es una forma sistemática de organizar los datos para
usarlos de manera eficiente. Los siguientes términos son los términos básicos
de una estructura de datos.
Interfaz : cada estructura de datos tiene una interfaz. La interfaz representa el
conjunto de operaciones que admite una estructura de datos. Una interfaz solo
proporciona la lista de operaciones admitidas, el tipo de parámetros que pueden
aceptar y el tipo de retorno de estas operaciones.
Implementación : la implementación proporciona la representación interna de una
estructura de datos. La implementación también proporciona la definición de los
algoritmos utilizados en las operaciones de la estructura de datos.
Terminología básica
Datos : los datos son valores o un conjunto de valores.
Elemento de datos: el elemento de datos se refiere a una sola unidad de valores.
Elementos de grupo: los elementos de datos que se dividen en elementos
secundarios se denominan elementos de grupo.
Elementos elementales: los elementos de datos que no se pueden dividir se
denominan elementos elementales.
Atributo y entidad : una entidad es aquella que contiene ciertos atributos o
propiedades, a los que se les pueden asignar valores.
Conjunto de entidades: las entidades de atributos similares forman un conjunto
de entidades.
Campo : el campo es una unidad de información elemental única que representa
un atributo de una entidad.
Registro : el registro es una colección de valores de campo de una entidad
determinada.
Archivo : archivo es una colección de registros de las entidades en un conjunto de
entidades dado.
Instalación en Mac OS
Si utiliza Mac OS X, la forma más fácil de obtener GCC es descargar el
entorno de desarrollo Xcode del sitio web de Apple y seguir las sencillas
instrucciones de instalación. Una vez que haya configurado Xcode, podrá
utilizar el compilador GNU para C / C ++.
Xcode está actualmente disponible en developer.apple.com/technologies/tools/
Instalación en Windows
Para instalar GCC en Windows, debe instalar MinGW. Para instalar MinGW,
vaya a la página de inicio de MinGW, www.mingw.org , y siga el enlace a la
página de descarga de MinGW. Descargue la última versión del programa de
instalación de MinGW, que debería llamarse MinGW- <versión> .exe.
Al instalar MinWG, como mínimo, debe instalar gcc-core, gcc-g ++, binutils y el
tiempo de ejecución de MinGW, pero es posible que desee instalar más.
Agregue el subdirectorio bin de su instalación MinGW a su variable
de entorno PATH , para que pueda especificar estas herramientas en la línea
de comandos por sus nombres simples.
Cuando se complete la instalación, podrá ejecutar gcc, g ++, ar, ranlib, dlltool y
varias otras herramientas GNU desde la línea de comandos de Windows.
Análisis de algoritmo
La eficiencia de un algoritmo se puede analizar en dos etapas diferentes,
antes de la implementación y después de la implementación. Ellos son los
siguientes:
Un análisis a priori : este es un análisis teórico de un algoritmo. La eficiencia de
un algoritmo se mide asumiendo que todos los demás factores, por ejemplo, la
velocidad del procesador, son constantes y no tienen ningún efecto en la
implementación.
Un análisis posterior : este es un análisis empírico de un algoritmo. El algoritmo
seleccionado se implementa utilizando lenguaje de programación. Esto se ejecuta
en la máquina de la computadora de destino. En este análisis, se recopilan
estadísticas reales como el tiempo de ejecución y el espacio requerido.
Aprenderemos sobre el análisis de algoritmos a priori . El análisis de
algoritmos se ocupa de la ejecución o el tiempo de ejecución de varias
operaciones involucradas. El tiempo de ejecución de una operación se puede
definir como el número de instrucciones de computadora ejecutadas por
operación.
Complejidad Algoritmo
Supongamos que X es un algoritmo yn es el tamaño de los datos de entrada,
el tiempo y el espacio utilizados por el algoritmo X son los dos factores
principales, que deciden la eficiencia de X.
Factor de tiempo : el tiempo se mide contando el número de operaciones clave,
como las comparaciones en el algoritmo de clasificación.
Factor de espacio : el espacio se mide contando el espacio de memoria máximo
requerido por el algoritmo.
La complejidad de un algoritmo f (n) proporciona el tiempo de ejecución y / o
el espacio de almacenamiento requerido por el algoritmo en términos
de n como el tamaño de los datos de entrada.
Complejidad espacial
La complejidad espacial de un algoritmo representa la cantidad de espacio de
memoria requerida por el algoritmo en su ciclo de vida. El espacio requerido
por un algoritmo es igual a la suma de los siguientes dos componentes:
Una parte fija que es un espacio requerido para almacenar ciertos datos y
variables, que son independientes del tamaño del problema. Por ejemplo,
variables simples y constantes utilizadas, tamaño del programa, etc.
Una parte variable es un espacio requerido por variables, cuyo tamaño depende
del tamaño del problema. Por ejemplo, asignación de memoria dinámica, espacio
de pila de recursión, etc.
La complejidad espacial S (P) de cualquier algoritmo P es S (P) = C + SP (I),
donde C es la parte fija y S (I) es la parte variable del algoritmo, que depende
de la característica de instancia I. A continuación es un ejemplo simple que
trata de explicar el concepto
Algorithm: SUM(A, B)
Step 1 - START
Step 2 - C ← A + B + 10
Step 3 - Stop
Aquí tenemos tres variables A, B y C y una constante. Por lo tanto, S (P) = 1 +
3. Ahora, el espacio depende de los tipos de datos de variables dadas y tipos
constantes y se multiplicará en consecuencia.
Complejidad de tiempo
La complejidad temporal de un algoritmo representa la cantidad de tiempo que
el algoritmo requiere para ejecutarse hasta su finalización. Los requisitos de
tiempo se pueden definir como una función numérica T (n), donde T (n) se
puede medir como el número de pasos, siempre que cada paso consuma
tiempo constante.
Por ejemplo, la suma de dos enteros de n bits toma n pasos. En
consecuencia, el tiempo de cálculo total es T (n) = c ∗ n, donde c es el tiempo
necesario para la adición de dos bits. Aquí, observamos que T (n) crece
linealmente a medida que aumenta el tamaño de entrada.
Notaciones asintóticas
A continuación se presentan las notaciones asintóticas comúnmente utilizadas
para calcular la complejidad del tiempo de ejecución de un algoritmo.
Ο Notación
Notación Ω
θ Notación
Big Oh Notation, Ο
La notación Ο (n) es la forma formal de expresar el límite superior del tiempo
de ejecución de un algoritmo. Mide la complejidad del tiempo en el peor de los
casos o la mayor cantidad de tiempo que un algoritmo puede tardar en
completarse.
Por ejemplo, para una función f (n)
Ο(f(n)) = { g(n) : there exists c > 0 and n0 such that f(n) ≤
c.g(n) for all n > n0. }
Notación Omega, Ω
La notación Ω (n) es la forma formal de expresar el límite inferior del tiempo de
ejecución de un algoritmo. Mide la complejidad del tiempo del mejor caso o la
mejor cantidad de tiempo que un algoritmo puede tardar en completarse.
Notación Theta, θ
La notación θ (n) es la forma formal de expresar tanto el límite inferior como el
límite superior del tiempo de ejecución de un algoritmo. Se representa de la
siguiente manera:
θ(f(n)) = { g(n) if and only if g(n) = Ο(f(n)) and g(n) =
Ω(f(n)) for all n > n0. }
constante - Ο (1)
logarítmico - Ο (log n)
lineal - Ο (n)
n log n - Ο (n log n)
cuadrático - Ο (n 2 )
cúbico - Ο (n 3 )
polinomio - n Ο (1)
exponencial - 2 Ο (n)
Contando monedas
Este problema es contar hasta un valor deseado eligiendo las monedas menos
posibles y el enfoque codicioso obliga al algoritmo a elegir la moneda más
grande posible. Si se nos proporcionan monedas de ₹ 1, 2, 5 y 10 y se nos
pide que contemos ₹ 18, el procedimiento codicioso será:
1 - Seleccione una moneda ₹ 10, el recuento restante es 8
2 - Luego seleccione una moneda ₹ 5, el recuento restante es 3
3 - Luego seleccione una moneda ₹ 2, el recuento restante es 1
4 - Y finalmente, la selección de una moneda ₹ 1 resuelve el problema
Sin embargo, parece estar funcionando bien, para este recuento necesitamos
elegir solo 4 monedas. Pero si cambiamos ligeramente el problema, entonces
el mismo enfoque puede no ser capaz de producir el mismo resultado óptimo.
Para el sistema monetario, donde tenemos monedas de valor 1, 7, 10, contar
monedas por valor 18 será absolutamente óptimo, pero para contar como 15,
puede usar más monedas de las necesarias. Por ejemplo, el enfoque
codicioso usará 10 + 1 + 1 + 1 + 1 + 1, un total de 6 monedas. Mientras que el
mismo problema podría resolverse utilizando solo 3 monedas (7 + 7 + 1)
Por lo tanto, podemos concluir que el enfoque codicioso elige una solución
optimizada inmediata y puede fallar donde la optimización global es una
preocupación importante.
Ejemplos
La mayoría de los algoritmos de redes utilizan el enfoque codicioso. Aquí hay
una lista de algunos de ellos:
Divide / Break
Este paso implica dividir el problema en subproblemas más pequeños. Los
subproblemas deben representar una parte del problema original. Este paso
generalmente toma un enfoque recursivo para dividir el problema hasta que
ningún subproblema sea más divisible. En esta etapa, los subproblemas se
vuelven de naturaleza atómica pero aún representan una parte del problema
real.
Conquistar / Resolver
Este paso recibe muchos subproblemas más pequeños que deben
resolverse. En general, a este nivel, los problemas se consideran "resueltos"
por sí mismos.
Fusionar / Combinar
Cuando se resuelven los subproblemas más pequeños, esta etapa los
combina de manera recursiva hasta que formulan una solución del problema
original. Este enfoque algorítmico funciona de forma recursiva y los pasos de
conquistar y fusionar funcionan tan cerca que aparecen como uno solo.
Ejemplos
Los siguientes algoritmos informáticos se basan en el enfoque de
programación de divide y vencerás :
Ordenar fusión
Ordenación rápida
Búsqueda binaria
Multiplicación matricial de Strassen
Par más cercano (puntos)
Hay varias formas disponibles para resolver cualquier problema informático,
pero las mencionadas son un buen ejemplo de enfoque de divide y vencerás.
Comparación
A diferencia de los algoritmos codiciosos, donde se aborda la optimización
local, los algoritmos dinámicos están motivados para una optimización general
del problema.
A diferencia de los algoritmos de dividir y conquistar, donde las soluciones se
combinan para lograr una solución general, los algoritmos dinámicos utilizan la
salida de un subproblema más pequeño y luego intentan optimizar un
subproblema más grande. Los algoritmos dinámicos usan Memoization para
recordar la salida de subproblemas ya resueltos.
Ejemplo
Los siguientes problemas de la computadora se pueden resolver utilizando el
enfoque de programación dinámica:
Definición de datos
La definición de datos define datos particulares con las siguientes
características.
Atómico : la definición debe definir un concepto único.
Rastreable : la definición debe poder asignarse a algún elemento de datos.
Preciso : la definición no debe ser ambigua.
Claro y conciso : la definición debe ser comprensible.
Objeto de datos
Objeto de datos representa un objeto que tiene datos.
Tipo de datos
El tipo de datos es una forma de clasificar varios tipos de datos, como enteros,
cadenas, etc., que determina los valores que se pueden usar con el tipo de
datos correspondiente, el tipo de operaciones que se pueden realizar en el tipo
de datos correspondiente. Hay dos tipos de datos:
Enteros
Booleano (verdadero, falso)
Flotante (números decimales)
Carácter y cadenas
Lista
Formación
Apilar
Cola
Operaciones básicas
Los datos en las estructuras de datos son procesados por ciertas
operaciones. La estructura de datos particular elegida depende en gran
medida de la frecuencia de la operación que debe realizarse en la estructura
de datos.
Atravesar
buscando
Inserción
Supresión
Clasificación
Fusionando
Representación de matriz
Las matrices se pueden declarar de varias maneras en diferentes idiomas. Por
ejemplo, tomemos la declaración de la matriz C.
Operaciones básicas
Las siguientes son las operaciones básicas compatibles con una matriz.
Traverse : imprime todos los elementos de la matriz uno por uno.
Inserción : agrega un elemento en el índice dado.
Eliminación : elimina un elemento en el índice dado.
Buscar : busca un elemento usando el índice dado o por el valor.
Actualizar : actualiza un elemento en el índice dado.
En C, cuando una matriz se inicializa con tamaño, asigna valores
predeterminados a sus elementos en el siguiente orden.
carbonizarse 00
En t 00
flotador 0.0
doble 0.0f
vacío
wchar_t 00
Operación transversal
Esta operación es atravesar los elementos de una matriz.
Ejemplo
El siguiente programa atraviesa e imprime los elementos de una matriz:
#include <stdio.h>
main() {
int LA[] = {1,3,5,7,8};
int item = 10, k = 3, n = 5;
int i = 0, j = n;
printf("The original array elements are :\n");
for(i = 0; i<n; i++) {
printf("LA[%d] = %d \n", i, LA[i]);
}
}
Cuando compilamos y ejecutamos el programa anterior, produce el siguiente
resultado:
Salida
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
LA[3] = 7
LA[4] = 8
Operación de inserción
La operación de inserción consiste en insertar uno o más elementos de datos
en una matriz. Según el requisito, se puede agregar un nuevo elemento al
principio, al final o en cualquier índice dado de la matriz.
Aquí, vemos una implementación práctica de la operación de inserción, donde
agregamos datos al final de la matriz:
Ejemplo
A continuación se muestra la implementación del algoritmo anterior:
Demo en vivo
#include <stdio.h>
main() {
int LA[] = {1,3,5,7,8};
int item = 10, k = 3, n = 5;
int i = 0, j = n;
n = n + 1;
while( j >= k) {
LA[j+1] = LA[j];
j = j - 1;
}
LA[k] = item;
Operación de borrado
La eliminación se refiere a eliminar un elemento existente de la matriz y
reorganizar todos los elementos de una matriz.
Algoritmo
Considere LA es un array lineal con N elementos, y K es un número entero
positivo tal que K <= N . A continuación se presenta el algoritmo para eliminar
un elemento disponible en el K ª posición de LA.
1. Start
2. Set J = K
3. Repeat steps 4 and 5 while J < N
4. Set LA[J] = LA[J + 1]
5. Set J = J+1
6. Set N = N-1
7. Stop
Ejemplo
A continuación se muestra la implementación del algoritmo anterior:
Demo en vivo
#include <stdio.h>
void main() {
int LA[] = {1,3,5,7,8};
int k = 3, n = 5;
int i, j;
j = k;
while( j < n) {
LA[j-1] = LA[j];
j = j + 1;
}
n = n -1;
Operación de búsqueda
Puede realizar una búsqueda de un elemento de matriz en función de su valor
o su índice.
Algoritmo
Considere LA es un array lineal con N elementos, y K es un número entero
positivo tal que K <= N . El siguiente es el algoritmo para encontrar un
elemento con un valor de ITEM utilizando la búsqueda secuencial.
1. Start
2. Set J = 0
3. Repeat steps 4 and 5 while J < N
4. IF LA[J] is equal ITEM THEN GOTO STEP 6
5. Set J = J +1
6. PRINT J, ITEM
7. Stop
Ejemplo
A continuación se muestra la implementación del algoritmo anterior:
Demo en vivo
#include <stdio.h>
void main() {
int LA[] = {1,3,5,7,8};
int item = 5, n = 5;
int i = 0, j = 0;
j = j + 1;
}
Operación de actualización
La operación de actualización se refiere a actualizar un elemento existente de
la matriz en un índice dado.
Algoritmo
Considere LA es un array lineal con N elementos, y K es un número entero
positivo tal que K <= N . A continuación se presenta el algoritmo para
actualizar un elemento disponible en el K ª posición de LA.
1. Start
2. Set LA[K-1] = ITEM
3. Stop
Ejemplo
A continuación se muestra la implementación del algoritmo anterior:
Demo en vivo
#include <stdio.h>
void main() {
int LA[] = {1,3,5,7,8};
int k = 3, n = 5, item = 10;
int i, j;
LA[k-1] = item;
Operaciones básicas
Las siguientes son las operaciones básicas compatibles con una lista.
Inserción : agrega un elemento al principio de la lista.
Eliminación : elimina un elemento al comienzo de la lista.
Pantalla : muestra la lista completa.
Buscar : busca un elemento utilizando la clave dada.
Eliminar : elimina un elemento usando la clave dada.
Operación de inserción
Agregar un nuevo nodo en la lista vinculada es una actividad de más de un
paso. Aprenderemos esto con diagramas aquí. Primero, cree un nodo con la
misma estructura y encuentre la ubicación donde debe insertarse.
Esto colocará el nuevo nodo en el medio de los dos. La nueva lista debería
verse así:
Se deben seguir pasos similares si el nodo se inserta al principio de la lista. Al
insertarlo al final, el segundo último nodo de la lista debe apuntar al nuevo
nodo y el nuevo nodo apuntará a NULL.
Operación de borrado
La eliminación también es un proceso de más de un paso. Aprenderemos con
representación pictórica. Primero, ubique el nodo objetivo que se eliminará,
utilizando algoritmos de búsqueda.
El nodo izquierdo (anterior) del nodo objetivo ahora debe apuntar al siguiente
nodo del nodo objetivo:
LeftNode.next −> TargetNode.next;
Operación inversa
Esta operación es exhaustiva. Necesitamos hacer que el último nodo sea
señalado por el nodo principal e invertir toda la lista vinculada.
Excepto el nodo (primer nodo) señalado por el nodo principal, todos los nodos
deben apuntar a su predecesor, convirtiéndolos en su nuevo sucesor. El
primer nodo apuntará a NULL.
Haremos que el nodo principal apunte al nuevo primer nodo utilizando el nodo
temporal.
Operaciones básicas
Las siguientes son las operaciones básicas compatibles con una lista.
Inserción : agrega un elemento al principio de la lista.
Eliminación : elimina un elemento al comienzo de la lista.
Insertar último : agrega un elemento al final de la lista.
Eliminar último : elimina un elemento del final de la lista.
Insertar después : agrega un elemento después de un elemento de la lista.
Eliminar : elimina un elemento de la lista con la tecla.
Mostrar hacia adelante : muestra la lista completa de manera hacia adelante.
Mostrar al revés : muestra la lista completa al revés.
Operación de inserción
El siguiente código muestra la operación de inserción al comienzo de una lista
doblemente vinculada.
Ejemplo
//insert link at the first location
void insertFirst(int key, int data) {
//create a link
struct node *link = (struct node*) malloc(sizeof(struct
node));
link->key = key;
link->data = data;
if(isEmpty()) {
//make it the last link
last = link;
} else {
//update first prev link
head->prev = link;
}
Operación de borrado
El siguiente código muestra la operación de eliminación al comienzo de una
lista doblemente vinculada.
Ejemplo
//delete first item
struct node* deleteFirst() {
head = head->next;
//create a link
struct node *link = (struct node*) malloc(sizeof(struct
node));
link->key = key;
link->data = data;
if(isEmpty()) {
//make it the last link
last = link;
} else {
//make link a new last link
last->next = link;
Operaciones básicas
Las siguientes son las operaciones importantes respaldadas por una lista
circular.
insert : inserta un elemento al comienzo de la lista.
eliminar : elimina un elemento desde el comienzo de la lista.
display : muestra la lista.
Operación de inserción
El siguiente código demuestra la operación de inserción en una lista enlazada
circular basada en una lista enlazada única.
Ejemplo
//insert link at the first location
void insertFirst(int key, int data) {
//create a link
struct node *link = (struct node*) malloc(sizeof(struct
node));
link->key = key;
link->data= data;
if (isEmpty()) {
head = link;
head->next = head;
} else {
//point it to old first node
link->next = head;
Operación de borrado
El siguiente código muestra la operación de eliminación en una lista enlazada
circular basada en una lista enlazada única.
//delete first item
struct node * deleteFirst() {
//save reference to first link
struct node *tempLink = head;
if(head->next == head) {
head = NULL;
return tempLink;
}
printf(" ]");
}
Una pila del mundo real permite operaciones solo en un extremo. Por ejemplo,
podemos colocar o quitar una tarjeta o plato de la parte superior de la pila
solamente. Del mismo modo, Stack ADT permite todas las operaciones de
datos en un solo extremo. En cualquier momento, solo podemos acceder al
elemento superior de una pila.
Esta característica hace que sea una estructura de datos LIFO. LIFO significa
último en entrar, primero en salir. Aquí, el elemento que se coloca (insertado o
agregado) en último lugar, se accede primero. En la terminología de la pila, la
operación de inserción se llama operación PUSH y la operación de extracción
se llama operación POP .
Representación de pila
El siguiente diagrama muestra una pila y sus operaciones:
Se puede implementar una pila mediante Array, Estructura, Puntero y Lista
enlazada. La pila puede ser de tamaño fijo o puede tener una sensación de
cambio de tamaño dinámico. Aquí, vamos a implementar la pila usando
matrices, lo que la convierte en una implementación de pila de tamaño fijo.
Operaciones básicas
Las operaciones de apilamiento pueden implicar inicializar el apilamiento,
usarlo y luego desinicializarlo. Además de estas cosas básicas, se utiliza una
pila para las siguientes dos operaciones principales:
push () - Empujar (almacenar) un elemento en la pila.
pop () - Eliminar (acceder) un elemento de la pila.
Cuando los datos se EMPUJAN en la pila.
Para usar una pila de manera eficiente, también debemos verificar el estado
de la pila. Con el mismo propósito, se agrega la siguiente funcionalidad a las
pilas:
peek () : obtiene el elemento de datos superior de la pila, sin eliminarlo.
isFull () : comprueba si la pila está llena.
isEmpty () : comprueba si la pila está vacía.
En todo momento, mantenemos un puntero a los últimos datos PUSHed en la
pila. Como este puntero siempre representa la parte superior de la pila, se
llama top . El puntero superior proporciona el valor superior de la pila sin
eliminarlo realmente.
Primero, deberíamos conocer los procedimientos para admitir las funciones de
la pila:
ojeada()
Algoritmo de la función peek () -
begin procedure peek
return stack[top]
end procedure
Implementación de la función peek () en lenguaje de programación C -
Ejemplo
int peek() {
return stack[top];
}
está lleno()
Algoritmo de la función isfull () -
begin procedure isfull
end procedure
Implementación de la función isfull () en lenguaje de programación C -
Ejemplo
bool isfull() {
if(top == MAXSIZE)
return true;
else
return false;
}
esta vacio()
Algoritmo de la función isempty () -
begin procedure isempty
end procedure
La implementación de la función isempty () en el lenguaje de programación C
es ligeramente diferente. Inicializamos top en -1, ya que el índice en la matriz
comienza desde 0. Por lo tanto, verificamos si el top está por debajo de cero o
-1 para determinar si la pila está vacía. Aquí está el código.
Ejemplo
bool isempty() {
if(top == -1)
return true;
else
return false;
}
Operación de empuje
El proceso de poner un nuevo elemento de datos en la pila se conoce como
Operación Push. La operación de empuje implica una serie de pasos:
Paso 1 : comprueba si la pila está llena.
Paso 2 : si la pila está llena, produce un error y sale.
Paso 3 : si la pila no está llena, aumenta de arriba hacia abajo para apuntar al
siguiente espacio vacío.
Paso 4 : agrega un elemento de datos a la ubicación de la pila, donde apunta la
parte superior.
Paso 5 - Devuelve el éxito.
if stack is full
return null
endif
top ← top + 1
stack[top] ← data
end procedure
La implementación de este algoritmo en C es muy fácil. Ver el siguiente código
-
Ejemplo
void push(int data) {
if(!isFull()) {
top = top + 1;
stack[top] = data;
} else {
printf("Could not insert data, Stack is full.\n");
}
}
Operación Pop
Acceder al contenido mientras se elimina de la pila, se conoce como
Operación Pop. En una implementación de matriz de la operación pop (), el
elemento de datos no se elimina realmente, sino que la parte superior se
reduce a una posición inferior en la pila para apuntar al siguiente valor. Pero
en la implementación de la lista vinculada, pop () en realidad elimina el
elemento de datos y desasigna el espacio de memoria.
Una operación Pop puede incluir los siguientes pasos:
Paso 1 : comprueba si la pila está vacía.
Paso 2 : si la pila está vacía, produce un error y sale.
Paso 3 : si la pila no está vacía, accede al elemento de datos al que apunta
la parte superior .
Paso 4 - disminuye el valor de la parte superior por 1.
Paso 5 - Devuelve el éxito.
Algoritmo para la Operación Pop
Un algoritmo simple para la operación Pop puede derivarse de la siguiente
manera:
begin procedure pop: stack
if stack is empty
return null
endif
data ← stack[top]
top ← top - 1
return data
end procedure
if(!isempty()) {
data = stack[top];
top = top - 1;
return data;
} else {
printf("Could not retrieve data, Stack is empty.\n");
}
}
Notación de infijo
Notación de prefijo (polaco)
Notación Postfix (polaco inverso)
Estas anotaciones se denominan como usan el operador en la
expresión. Aprenderemos lo mismo aquí en este capítulo.
Notación de infijo
Escribimos expresión en infijo notación, por ejemplo, a - b + c, donde los
operadores se utilizan en -entre operandos. Es fácil para nosotros los
humanos leer, escribir y hablar en notación infija, pero lo mismo no funciona
bien con los dispositivos informáticos. Un algoritmo para procesar la notación
infija podría ser difícil y costoso en términos de consumo de tiempo y espacio.
Notación de prefijo
En esta notación, el operador está prefijado a los operandos, es decir, el
operador se escribe antes de los operandos. Por ejemplo, + ab . Esto es
equivalente a su notación infija a + b . La notación de prefijo también se
conoce como notación polaca .
Notación de Postfix
Este estilo de notación se conoce como notación polaca invertida . En este
estilo de notación, el operador se pospone en los operandos, es decir, el
operador se escribe después de los operandos. Por ejemplo, ab + . Esto es
equivalente a su notación infija a + b .
La siguiente tabla trata brevemente de mostrar la diferencia en las tres
notaciones:
1 a+b + ab ab +
2 (a + b) ∗ c ∗ + abc ab + c ∗
3 a ∗ (b + c) ∗ a + bc abc + ∗
44 a/b+c/d + / ab / cd ab / cd / +
55 (a + b) ∗ (c + d) ∗ + ab + cd ab + cd + ∗
66 ((a + b) ∗ c) - d - ∗ + abcd ab + c ∗ d -
Analizando Expresiones
Como hemos discutido, no es una forma muy eficiente de diseñar un algoritmo
o programa para analizar las anotaciones infijadas. En cambio, estas
anotaciones de infijo se convierten primero en anotaciones de postfijo o prefijo
y luego se calculan.
Para analizar cualquier expresión aritmética, también debemos ocuparnos de
la precedencia del operador y la asociatividad.
Precedencia
Cuando un operando se encuentra entre dos operadores diferentes, qué
operador tomará el operando primero, se decide por la precedencia de un
operador sobre otros. Por ejemplo
Un ejemplo real de cola puede ser una carretera de un solo sentido, donde el
vehículo entra primero, sale primero. Se pueden ver más ejemplos del mundo
real como colas en las ventanillas y las paradas de autobús.
Representación de cola
Como ahora entendemos que en la cola, accedemos a ambos extremos por
diferentes razones. El siguiente diagrama a continuación intenta explicar la
representación de la cola como estructura de datos:
Al igual que en las pilas, una cola también se puede implementar utilizando
matrices, listas vinculadas, punteros y estructuras. En aras de la simplicidad,
implementaremos colas utilizando una matriz unidimensional.
Operaciones básicas
Las operaciones de la cola pueden implicar inicializar o definir la cola, utilizarla
y luego borrarla completamente de la memoria. Aquí intentaremos comprender
las operaciones básicas asociadas con las colas:
enqueue () : agrega (almacena) un elemento a la cola.
dequeue () : elimina (accede) un elemento de la cola.
Se requieren pocas funciones más para que la operación de la cola
mencionada anteriormente sea eficiente. Estos son -
peek () - Obtiene el elemento al frente de la cola sin eliminarlo.
isfull () - Comprueba si la cola está llena.
isempty () - Comprueba si la cola está vacía.
En la cola, siempre ponemos en cola (o accedemos) los datos, apuntando con
el puntero delantero y mientras enquistamos (o almacenamos) datos en la
cola, tomamos la ayuda del puntero trasero .
Primero aprendamos sobre las funciones de apoyo de una cola:
ojeada()
Esta función ayuda a ver los datos al frente de la cola. El algoritmo de la
función peek () es el siguiente:
Algoritmo
begin procedure peek
return queue[front]
end procedure
Implementación de la función peek () en lenguaje de programación C -
Ejemplo
int peek() {
return queue[front];
}
está lleno()
Como estamos utilizando una matriz de una sola dimensión para implementar
la cola, solo verificamos que el puntero trasero llegue a MAXSIZE para
determinar que la cola está llena. En caso de que mantengamos la cola en
una lista circular vinculada, el algoritmo será diferente. Algoritmo de la función
isfull () -
Algoritmo
begin procedure isfull
end procedure
esta vacio()
Algoritmo de la función isempty () -
Algoritmo
begin procedure isempty
end procedure
Si el valor de front es menor que MIN o 0, indica que la cola aún no se ha
inicializado, por lo tanto, está vacía.
Aquí está el código de programación C -
Ejemplo
bool isempty() {
if(front < 0 || front > rear)
return true;
else
return false;
}
Operación en cola
Las colas mantienen dos punteros de datos, frontal y posterior . Por lo tanto,
sus operaciones son relativamente difíciles de implementar que las de las
pilas.
Se deben seguir los siguientes pasos para poner en cola (insertar) datos en
una cola:
Paso 1 : compruebe si la cola está llena.
Paso 2 : si la cola está llena, produzca un error de desbordamiento y salga.
Paso 3 : si la cola no está llena, incremente el puntero trasero para apuntar al
siguiente espacio vacío.
Paso 4 : agregue un elemento de datos a la ubicación de la cola, donde apunta la
parte posterior.
Paso 5 : devuelve el éxito.
if queue is full
return overflow
endif
rear ← rear + 1
queue[rear] ← data
return true
end procedure
Implementación de enqueue () en lenguaje de programación C -
Ejemplo
int enqueue(int data)
if(isfull())
return 0;
rear = rear + 1;
queue[rear] = data;
return 1;
end procedure
Operación en espera
El acceso a los datos desde la cola es un proceso de dos tareas: acceder a los
datos hacia donde apunta el frente y eliminar los datos después del
acceso. Se realizan los siguientes pasos para realizar la operación de retirada
de cola :
Paso 1 : compruebe si la cola está vacía.
Paso 2 : si la cola está vacía, produzca un error de flujo insuficiente y salga.
Paso 3 : si la cola no está vacía, acceda a los datos donde apunta el frente .
Paso 4 : incremente el puntero frontal para señalar el siguiente elemento de datos
disponible.
Paso 5 - Devuelve el éxito.
Algoritmo para operar en cola
procedure dequeue
if queue is empty
return underflow
end if
data = queue[front]
front ← front + 1
return true
end procedure
Implementación de dequeue () en lenguaje de programación C -
Ejemplo
int dequeue() {
if(isempty())
return 0;
return data;
}
Para un programa completo de Queue en lenguaje de programación C, haga
clic aquí .
Algoritmo
Linear Search ( Array A, Value x)
Step 1: Set i to 1
Step 2: if i > n then go to step 7
Step 3: if A[i] = x then go to step 6
Step 4: Set i to i + 1
Step 5: Go to Step 2
Step 6: Print Element x Found at index i and go to step 8
Step 7: Print element not found
Step 8: Exit
Pseudocódigo
procedure linear_search (list, value)
end procedure
Para conocer la implementación de búsqueda lineal en lenguaje de
programación C, haga clic aquí .
Pseudocódigo
El pseudocódigo de los algoritmos de búsqueda binaria debería verse así:
Procedure binary_search
A ← sorted array
n ← size of array
x ← value to be searched
Set lowerBound = 1
Set upperBound = n
while x not found
if upperBound < lowerBound
EXIT: x does not exists.
if A[midPoint] < x
set lowerBound = midPoint + 1
if A[midPoint] > x
set upperBound = midPoint - 1
if A[midPoint] = x
EXIT: x found at location midPoint
end while
end procedure
where −
A = list
Lo = Lowest index of the list
Hi = Highest index of the list
A[n] = Value stored at index n in the list
Si el ítem central es mayor que el ítem, entonces la posición de la sonda se
calcula nuevamente en la matriz secundaria a la derecha del ítem central. De
lo contrario, el elemento se busca en la submatriz a la izquierda del elemento
central. Este proceso también continúa en la submatriz hasta que el tamaño
de la submatriz se reduce a cero.
La complejidad del tiempo de ejecución del algoritmo de búsqueda de
interpolación es Ο (log (log n)) en comparación con Ο (log n) de BST en
situaciones favorables.
Algoritmo
Como es una improvisación del algoritmo BST existente, estamos
mencionando los pasos para buscar el índice de valor de datos 'objetivo',
utilizando el sondeo de posición:
Step 1 − Start searching data from middle of the list.
Step 2 − If it is a match, return the index of the item, and
exit.
Step 3 − If it is not a match, probe position.
Step 4 − Divide the list using probing formula and find the
new midle.
Step 5 − If data is greater than middle, search in higher
sub-list.
Step 6 − If data is smaller than middle, search in lower sub-
list.
Step 7 − Repeat until match.
Pseudocódigo
A → Array list
N → Size of A
X → Target Value
Procedure Interpolation_Search()
Set Lo → 0
Set Mid → -1
Set Hi → N-1
if A[Mid] = X
EXIT: Success, Target found at Mid
else
if A[Mid] < X
Set Lo to Mid+1
else if A[Mid] > X
Set Hi to Mid-1
end if
end if
End While
End Procedure
Para conocer la implementación de la búsqueda de interpolación en lenguaje
de programación C, haga clic aquí .
Hashing
El hash es una técnica para convertir un rango de valores clave en un rango
de índices de una matriz. Vamos a utilizar el operador de módulo para obtener
un rango de valores clave. Considere un ejemplo de tabla hash de tamaño 20,
y los siguientes elementos deben almacenarse. Los artículos están en el
formato (clave, valor).
(1,20)
(2,70)
(42,80)
(4,25)
(12,44)
(14,32)
(17,11)
(13,78)
(37,98)
1 1 1% 20 = 1 1
2 2 2% 20 = 2 2
3 42 42% 20 = 2 2
44 44 4% 20 = 4 44
55 12 12% 20 = 12 12
66 14 14% 20 = 14 14
77 17 17% 20 = 17 17
8 13 13% 20 = 13 13
99 37 37% 20 = 17 17
Sondeo lineal
Como podemos ver, puede suceder que la técnica de hashing se use para
crear un índice ya usado de la matriz. En tal caso, podemos buscar la
siguiente ubicación vacía en la matriz mirando la siguiente celda hasta que
encontremos una celda vacía. Esta técnica se llama sondeo lineal.
1 1 1% 20 = 1 1 1
2 2 2% 20 = 2 2 2
3 42 42% 20 = 2 2 3
44 44 4% 20 = 4 44 44
55 12 12% 20 = 12 12 12
66 14 14% 20 = 14 14 14
77 17 17% 20 = 17 17 17
8 13 13% 20 = 13 13 13
99 37 37% 20 = 17 17 18 años
Operaciones básicas
Las siguientes son las operaciones primarias básicas de una tabla hash.
Buscar : busca un elemento en una tabla hash.
Insertar : inserta un elemento en una tabla hash.
eliminar : elimina un elemento de una tabla hash.
DataItem
Defina un elemento de datos que tenga algunos datos y clave, en función de
los cuales se realizará la búsqueda en una tabla hash.
struct DataItem {
int data;
int key;
};
Método hash
Defina un método de hash para calcular el código de hash de la clave del
elemento de datos.
int hashCode(int key){
return key % SIZE;
}
Operación de búsqueda
Siempre que se busque un elemento, calcule el código hash de la clave
pasada y ubique el elemento usando ese código hash como índice en la
matriz. Utilice el sondeo lineal para adelantar el elemento si el elemento no se
encuentra en el código hash calculado.
Ejemplo
struct DataItem *search(int key) {
//get the hash
int hashIndex = hashCode(key);
//move in array until an empty
while(hashArray[hashIndex] != NULL) {
if(hashArray[hashIndex]->key == key)
return hashArray[hashIndex];
return NULL;
}
Insertar operación
Siempre que se inserte un elemento, calcule el código hash de la clave
pasada y ubique el índice utilizando ese código hash como índice en la
matriz. Utilice el sondeo lineal para la ubicación vacía, si se encuentra un
elemento en el código hash calculado.
Ejemplo
void insert(int key,int data) {
struct DataItem *item = (struct DataItem*)
malloc(sizeof(struct DataItem));
item->data = data;
item->key = key;
hashArray[hashIndex] = item;
}
Eliminar operación
Siempre que deba eliminarse un elemento, calcule el código hash de la clave
pasada y ubique el índice utilizando ese código hash como índice en la
matriz. Utilice el sondeo lineal para adelantar el elemento si no se encuentra
un elemento en el código hash calculado. Cuando lo encuentre, guarde un
elemento ficticio allí para mantener intacto el rendimiento de la tabla hash.
Ejemplo
struct DataItem* delete(struct DataItem* item) {
int key = item->key;
if(hashArray[hashIndex]->key == key) {
struct DataItem* temp = hashArray[hashIndex];
return NULL;
}
Términos importantes
Algunos términos generalmente se acuñan al discutir técnicas de clasificación,
aquí hay una breve introducción a ellos:
Orden creciente
Se dice que una secuencia de valores está en orden creciente , si el
elemento sucesivo es mayor que el anterior. Por ejemplo, 1, 3, 4, 6, 8, 9 están
en orden creciente, ya que cada elemento siguiente es mayor que el elemento
anterior.
Orden decreciente
Se dice que una secuencia de valores está en orden decreciente , si el
elemento sucesivo es menor que el actual. Por ejemplo, 9, 8, 6, 4, 3, 1 están
en orden decreciente, ya que cada elemento siguiente es menor que el
elemento anterior.
Orden no creciente
Se dice que una secuencia de valores está en orden no creciente , si el
elemento sucesivo es menor o igual que su elemento anterior en la
secuencia. Este orden ocurre cuando la secuencia contiene valores
duplicados. Por ejemplo, 9, 8, 6, 3, 3, 1 están en orden no creciente, ya que
cada elemento siguiente es menor o igual que (en el caso de 3) pero no mayor
que cualquier elemento anterior.
Orden no decreciente
Se dice que una secuencia de valores está en orden no decreciente , si el
elemento sucesivo es mayor o igual que su elemento anterior en la
secuencia. Este orden ocurre cuando la secuencia contiene valores
duplicados. Por ejemplo, 1, 3, 3, 6, 8, 9 están en orden no decreciente, ya que
cada elemento siguiente es mayor o igual que (en el caso de 3) pero no menor
que el anterior.
En este caso, el valor 33 es mayor que 14, por lo que ya está en ubicaciones
ordenadas. A continuación, comparamos 33 con 27.
Para ser precisos, ahora mostramos cómo debería verse una matriz después
de cada iteración. Después de la segunda iteración, debería verse así:
Algoritmo
Suponemos que list es una matriz de n elementos. Además, suponemos que
la función de intercambio intercambia los valores de los elementos de la
matriz dados.
begin BubbleSort(list)
end BubbleSort
Pseudocódigo
Observamos en el algoritmo que Bubble Sort compara cada par de elementos
de la matriz a menos que toda la matriz esté completamente ordenada en
orden ascendente. Esto puede causar algunos problemas de complejidad,
como si la matriz no necesita más intercambio ya que todos los elementos ya
están ascendiendo.
Para solucionar el problema, utilizamos una variable de
bandera intercambiada que nos ayudará a ver si ha ocurrido o no un
intercambio. Si no se ha producido un intercambio, es decir, la matriz no
requiere más procesamiento para clasificarse, saldrá del bucle.
El pseudocódigo del algoritmo BubbleSort se puede escribir de la siguiente
manera:
procedure bubbleSort( list : array of items )
loop = list.count;
end for
end for
Implementación
Un problema más que no abordamos en nuestro algoritmo original y su
pseudocódigo improvisado es que, después de cada iteración, los valores más
altos se establecen al final de la matriz. Por lo tanto, la siguiente iteración no
necesita incluir elementos ya ordenados. Para este propósito, en nuestra
implementación, restringimos el ciclo interno para evitar valores ya ordenados.
Para saber acerca de la implementación de clasificación de burbujas en
lenguaje de programación C, haga clic aquí .
Pseudocódigo
procedure insertionSort( A : array of items )
int holePosition
int valueToInsert
end for
end procedure
Para saber acerca de la implementación de clasificación de inserción en
lenguaje de programación C, haga clic aquí .
Entonces reemplazamos 14 por 10. Después de una iteración 10, que resulta
ser el valor mínimo en la lista, aparece en la primera posición de la lista
ordenada.
Pseudocódigo
procedure selection sort
list : array of items
n : size of list
for i = 1 to n - 1
/* set current element as minimum*/
min = i
for j = i+1 to n
if list[j] < list[min] then
min = j;
end if
end for
end procedure
Para saber acerca de la implementación de selección de selección en lenguaje
de programación C, haga clic aquí .
Pseudocódigo
Ahora veremos los pseudocódigos para las funciones de clasificación de
fusión. Como nuestros algoritmos señalan dos funciones principales: dividir y
fusionar.
La ordenación por fusión funciona con recursividad y veremos nuestra
implementación de la misma manera.
procedure mergesort( var a as array )
if ( n == 1 ) return a
l1 = mergesort( l1 )
l2 = mergesort( l2 )
var c as array
while ( a and b have elements )
if ( a[0] > b[0] )
add b[0] to the end of c
remove b[0] from b
else
add a[0] to the end of c
remove a[0] from a
end if
end while
return c
end procedure
Luego, tomamos el intervalo de 2 y esta brecha genera dos sublistas: {14, 27,
35, 42}, {19, 10, 33, 44}
/* calculate interval*/
while interval < A.length /3 do:
interval = interval * 3 + 1
end while
end for
/* calculate interval*/
interval = (interval -1) /3;
end while
end procedure
Para saber sobre la implementación de ordenación de shell en lenguaje de
programación C, haga clic aquí .
Luego, tomamos el intervalo de 1 y esta brecha genera dos sublistas: {14, 27,
35, 42}, {19, 10, 33, 44}
Comparamos e intercambiamos los valores, si es necesario, en la matriz
original. Después de este paso, la matriz debería verse así:
/* calculate interval*/
while interval < A.length /3 do:
interval = interval * 3 + 1
end while
end for
/* calculate interval*/
interval = (interval -1) /3;
end while
end procedure
Para saber sobre la implementación de ordenación de shell en lenguaje de
programación C, haga clic aquí .
end while
swap leftPointer,right
return leftPointer
end function
if right-left <= 0
return
else
pivot = A[right]
partition = partitionFunc(left, right, pivot)
quickSort(left,partition-1)
quickSort(partition+1,right)
end if
end procedure
Para saber acerca de la implementación de ordenación rápida en lenguaje de
programación C, haga clic aquí .
En el gráfico anterior,
V = {a, b, c, d, e}
E = {ab, ac, bd, cd, de}
Operaciones básicas
Las siguientes son operaciones primarias básicas de un gráfico:
Agregar vértice : agrega un vértice al gráfico.
Agregar borde : agrega un borde entre los dos vértices del gráfico.
Mostrar vértice : muestra un vértice del gráfico.
Inicializa la pila.
2
Marque S como visitado y póngalo
en la pila. Explora cualquier nodo
adyacente unvisited
de S . Tenemos tres nodos y
podemos elegir cualquiera de
ellos. Para este ejemplo,
tomaremos el nodo en orden
alfabético.
44
55
77
Inicializa la cola.
2
44
55
77
En esta etapa, nos quedamos sin nodos no marcados (no visitados). Pero
según el algoritmo, seguimos disminuyendo para obtener todos los nodos no
visitados. Cuando la cola se vacía, el programa termina.
La implementación de este algoritmo en lenguaje de programación C se
puede ver aquí .
Insertar operación
La primera inserción crea el árbol. Luego, cada vez que se inserte un
elemento, primero ubique su ubicación correcta. Comience a buscar desde el
nodo raíz, luego, si los datos son menores que el valor clave, busque la
ubicación vacía en el subárbol izquierdo e inserte los datos. De lo contrario,
busque la ubicación vacía en el subárbol derecho e inserte los datos.
Algoritmo
If root is NULL
then create root node
return
endwhile
insert data
end If
Implementación
La implementación de la función de inserción debería verse así:
void insert(int data) {
struct node *tempNode = (struct node*)
malloc(sizeof(struct node));
struct node *current;
struct node *parent;
tempNode->data = data;
tempNode->leftChild = NULL;
tempNode->rightChild = NULL;
while(1) {
parent = current;
Operación de búsqueda
Siempre que se deba buscar un elemento, comience a buscar desde el nodo
raíz, luego, si los datos son menores que el valor clave, busque el elemento en
el subárbol izquierdo. De lo contrario, busque el elemento en el subárbol
derecho. Siga el mismo algoritmo para cada nodo.
Algoritmo
If root.data is equal to search.data
return root
else
while data not found
If data found
return node
endwhile
end if
La implementación de este algoritmo debería verse así.
struct node* search(int data) {
struct node *current = root;
printf("Visiting elements: ");
while(current->data != data) {
if(current != NULL)
printf("%d ",current->data);
//not found
if(current == NULL) {
return NULL;
}
return current;
}
}
Recorrido en orden
Pre-pedido transversal
Recorrido posterior al pedido
En general, atravesamos un árbol para buscar o localizar un elemento o clave
en el árbol o para imprimir todos los valores que contiene.
Recorrido en orden
En este método transversal, primero se visita el subárbol izquierdo, luego la
raíz y luego el subárbol derecho. Siempre debemos recordar que cada nodo
puede representar un subárbol en sí.
Si se atraviesa un árbol binario en orden , la salida producirá valores de clave
ordenados en orden ascendente.
Pre-pedido transversal
En este método transversal, primero se visita el nodo raíz, luego el subárbol
izquierdo y finalmente el subárbol derecho.
Partimos de una , y siguiendo el recorrido de pre-orden, primero
visitamos Un sí y luego pasar a su subárbol izquierdo B . B también se
atraviesa el pedido anticipado. El proceso continúa hasta que se visiten todos
los nodos. La salida del recorrido de preorden de este árbol será:
A→B→D→E→C→F→G
Algoritmo
Until all nodes are traversed −
Step 1 − Visit root node.
Step 2 − Recursively traverse left subtree.
Step 3 − Recursively traverse right subtree.
Representación
BST es una colección de nodos dispuestos de manera que mantienen las
propiedades BST. Cada nodo tiene una clave y un valor asociado. Durante la
búsqueda, la clave deseada se compara con las claves en BST y, si se
encuentra, se recupera el valor asociado.
A continuación se muestra una representación gráfica de BST:
Observamos que la clave del nodo raíz (27) tiene todas las claves de menor
valor en el subárbol izquierdo y las claves de mayor valor en el subárbol
derecho.
Operaciones básicas
Las siguientes son las operaciones básicas de un árbol:
Buscar : busca un elemento en un árbol.
Insertar : inserta un elemento en un árbol.
Recorrido de pedido anticipado : atraviesa un árbol de manera ordenada.
Recorrido en orden : atraviesa un árbol en orden.
Recorrido posterior al pedido : atraviesa un árbol de manera posterior.
Nodo
Defina un nodo que tenga algunos datos, referencias a sus nodos secundarios
izquierdo y derecho.
struct node {
int data;
struct node *leftChild;
struct node *rightChild;
};
Operación de búsqueda
Siempre que se busque un elemento, comience a buscar desde el nodo
raíz. Luego, si los datos son menores que el valor clave, busque el elemento
en el subárbol izquierdo. De lo contrario, busque el elemento en el subárbol
derecho. Siga el mismo algoritmo para cada nodo.
Algoritmo
struct node* search(int data){
struct node *current = root;
printf("Visiting elements: ");
while(current->data != data){
if(current != NULL) {
printf("%d ",current->data);
//not found
if(current == NULL){
return NULL;
}
}
}
return current;
}
Insertar operación
Siempre que se inserte un elemento, primero ubique su ubicación
correcta. Comience a buscar desde el nodo raíz, luego, si los datos son
menores que el valor clave, busque la ubicación vacía en el subárbol izquierdo
e inserte los datos. De lo contrario, busque la ubicación vacía en el subárbol
derecho e inserte los datos.
Algoritmo
void insert(int data) {
struct node *tempNode = (struct node*)
malloc(sizeof(struct node));
struct node *current;
struct node *parent;
tempNode->data = data;
tempNode->leftChild = NULL;
tempNode->rightChild = NULL;
if(current == NULL) {
parent->leftChild = tempNode;
return;
}
} //go to right of the tree
else {
current = current->rightChild;
Se observa que el rendimiento en el peor de los casos de BST está más cerca
de los algoritmos de búsqueda lineal, es decir, Ο (n). En datos en tiempo real,
no podemos predecir el patrón de datos y sus frecuencias. Por lo tanto, surge
la necesidad de equilibrar el BST existente.
El nombre de su inventor Adelson , Velski & Landis , los árboles
AVL son árboles de búsqueda binaria de equilibrio de altura. El árbol AVL
verifica la altura de los subárboles izquierdo y derecho y asegura que la
diferencia no sea mayor que 1. Esta diferencia se llama Factor de equilibrio .
Aquí vemos que el primer árbol está equilibrado y los dos siguientes no lo
están:
Rotaciones AVL
Para equilibrarse, un árbol AVL puede realizar los siguientes cuatro tipos de
rotaciones:
Rotación izquierda
Rotación derecha
Rotación izquierda-derecha
Rotación derecha-izquierda
Las dos primeras rotaciones son rotaciones simples y las siguientes dos
rotaciones son rotaciones dobles. Para tener un árbol desequilibrado, al
menos necesitamos un árbol de altura 2. Con este árbol simple,
comprendamos uno por uno.
Rotación izquierda
Si un árbol se desequilibra, cuando se inserta un nodo en el subárbol derecho
del subárbol derecho, entonces realizamos una sola rotación izquierda:
En nuestro ejemplo, el nodo A se ha desequilibrado a medida que se inserta
un nodo en el subárbol derecho del subárbol derecho de A. Realizamos la
rotación a la izquierda haciendo que A sea el subárbol izquierdo de B.
Rotación derecha
El árbol AVL puede desequilibrarse si se inserta un nodo en el subárbol
izquierdo del subárbol izquierdo. El árbol entonces necesita una rotación
correcta.
Estado Acción
Se ha insertado un nodo en el subárbol derecho del subárbol
izquierdo. Esto hace que C sea un nodo desequilibrado. Estos
escenarios hacen que el árbol AVL realice una rotación izquierda-
derecha.
Estado Acción
Max-Heap : donde el valor del nodo raíz es mayor o igual que cualquiera de
sus elementos secundarios.
Ambos árboles se construyen utilizando la misma entrada y el mismo orden de
llegada.
printf("%d ",value);
}
Ejemplo : una función que llama a otra función que a su vez la vuelve a
llamar.
int function1(int value1) {
if(value1 < 1)
return;
function2(value1 - 1);
printf("%d ",value1);
}
int function2(int value2) {
function1(value2);
}
Propiedades
Una función recursiva puede ser infinita como un bucle. Para evitar el
funcionamiento infinito de la función recursiva, hay dos propiedades que debe
tener una función recursiva:
Criterios de Base - debe haber al menos uno de los criterios de base o condición,
de tal manera que, cuando esta condición se cumple la función se detiene hace
llamar de forma recursiva.
Enfoque progresivo : las llamadas recursivas deben progresar de tal manera que
cada vez que se realiza una llamada recursiva se acerque al criterio base.
Implementación
Muchos lenguajes de programación implementan la recursión por medio
de pilas . Generalmente, cada vez que una función ( llamante ) llama a otra
función ( llamante ) o se llama a sí misma como llamada, la función llamante
transfiere el control de ejecución al llamante. Este proceso de transferencia
también puede implicar que algunos datos pasen de la persona que llama a la
persona que llama.
Esto implica que la función de llamada tiene que suspender su ejecución
temporalmente y reanudarla más tarde cuando el control de ejecución regrese
de la función de llamada. Aquí, la función de llamada debe comenzar
exactamente desde el punto de ejecución donde se pone en espera. También
necesita exactamente los mismos valores de datos en los que estaba
trabajando. Para este propósito, se crea un registro de activación (o marco de
pila) para la función del llamante.
Análisis de recursión
Uno puede discutir por qué usar la recursión, ya que la misma tarea se puede
hacer con la iteración. La primera razón es que la recursividad hace que un
programa sea más legible y, debido a los últimos sistemas de CPU mejorados,
la recursividad es más eficiente que las iteraciones.
Complejidad de tiempo
En caso de iteraciones, tomamos varias iteraciones para contar la complejidad
del tiempo. Del mismo modo, en caso de recursión, suponiendo que todo sea
constante, tratamos de calcular la cantidad de veces que se realiza una
llamada recursiva. Una llamada realizada a una función es Ο (1), por lo tanto,
el (n) número de veces que se realiza una llamada recursiva convierte la
función recursiva Ο (n).
Complejidad espacial
La complejidad del espacio se cuenta como la cantidad de espacio adicional
que se requiere para que un módulo se ejecute. En caso de iteraciones, el
compilador apenas requiere espacio adicional. El compilador sigue
actualizando los valores de las variables utilizadas en las iteraciones. Pero en
caso de recursividad, el sistema necesita almacenar el registro de activación
cada vez que se realiza una llamada recursiva. Por lo tanto, se considera que
la complejidad espacial de la función recursiva puede ser mayor que la de una
función con iteración.
Reglas
La misión es mover todos los discos a otra torre sin violar la secuencia de
disposición. Algunas reglas a seguir para la Torre de Hanoi son:
Algoritmo
Para escribir un algoritmo para la Torre de Hanoi, primero debemos aprender
a resolver este problema con una menor cantidad de discos, digamos → 1 o 2.
Marcamos tres torres con nombre, origen , destino y auxiliar (solo para
ayudar a mover los discos ) Si solo tenemos un disco, se puede mover
fácilmente de la clavija de origen a la de destino.
Si tenemos 2 discos:
IF disk == 1, THEN
move disk from source to dest
ELSE
Hanoi(disk - 1, source, aux, dest) // Step 1
move disk from source to dest // Step 2
Hanoi(disk - 1, aux, dest, source) // Step 3
END IF
END Procedure
STOP
Para verificar la implementación en la programación en C, haga clic aquí .
set f0 to 0
set f1 to 1
display f0, f1
for loop ← 1 to n
fib ← f0 + f1
f0 ← f1
f1 ← fib
display fib
end for
end procedure
set f0 to 0
set f1 to 1
display f0, f1
for loop ← 1 to n
fib ← f0 + f1
f0 ← f1
f1 ← fib
display fib
end for
END