# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Julia 1.5.1
#     language: julia
#     name: julia-1.5
# ---

# # Estructuras de Datos

#
# ## Estructuras de Datos
# Las estructurtas de datos son formatos especiales de datos que permiten guardar, organizar y manipular información. En particular, cada estructura especifica la forma en la cual se pueden realizar operaciones sobre los datos que contiene y a la vez sobre qué tipo de datos trabajan. 
#
# Las estructuras de datos ocultan al programador la manera en que se maneja la memoria y la gestionan de forma eficiente, por ende genera eficiencia en los algoritmos que los implementan. 
#
# Existen numerosas estructuras de datos cada una diseñada para una aplicación en específico, Julia incorpora algunos tipos de datos básicos y sus procedimientos correspondientes, sin embargo no incorpora por defecto todos los tipos conocidos. Por ello, la comunidad de Julia ha trabajado en un paquete que incorpora diferentes estructuras de datos procurando rendimiento (revisar el paquete [DataStructures.jl](https://juliacollections.github.io/DataStructures.jl/latest/circ_buffer/)).
#
# A continuación, se disertará sobre algunos de los tipos de datos básicos incorporados por defecto en Julia.
#
#

#
# ###  Rangos
#
# `(:)(start,[step],stop)`
#
# Esta estructura permite crear secuencias de números, desde un número dado (`start` ) hasta otro (`stop`) con un tamaño de paso (`[step]`) que puede ser inidicado de forma opcional. 
#
#

begin
	range₁ = -5:10
	range₂ = 0:2:10
	range₃ = 0:10.0
end


#
# Por defecto el tamaño del paso es `1`. Si es requerido Julia realiza una conversión automática de tipos de datos. En general el rango puede ser tipo `UnitRange` o `StepRange`.
#
#

typeof(range₁)


typeof(range₂)


typeof(range₃)


#
# Puede notar que el tipo de dato de cada elemento del range está determinado infiriendo este (de forma automática) por cualquiera de los tipos de datos de: `start`, `step` o `stop`.
#
# Para crear un rango también puede utilizarse el constructor `OneTo` (perteneciente al módulo Base), que garantiza el valor inicial de la secuencia es `1` al igual que el paso.
#
#

range₄ = Base.OneTo(10)


range₄[5]


#
# ### Array
#
# Los arrays son un tipo de estructura que contiene datos relacionados, con ellos pueden ser representados vectores y matrices. 
#
# En Julia los Arrays son listas, que pueden contener más de un tipo de dato, según este sea creado o inicializado, además implementa métodos para incrementar y eliminar elementos. También, a diferencia de otras estructuras los array pueden tener datos duplicados.
#
# #### Inicialización de arrays
# El array puede ser inicializado de diversas maneras, como se contempla a continuación:
#
# **Usando el constructor explícito**
#
#

array₁ = Array{Float64}(undef,5,2,2)


array₁[5][1]


#
# En el constructor explícito debe especificarse: 
# * El tipo de dato (en el ejemplo `Float64`).
# * Las dimensiones del array (en el ejemplo al array se especifica 3 dimensiones).
# * El tamaño del array, el keyword `undef` debe ser declarado, posteriormente se indica la cantidad cantidad de elementos por dimensión.
#
# **NOTA:**  Aunque se desea tener un array vacío, al especificar el tamaño de array, Julia rellena los elementos con datos contenidos en el garbage.
#
# El array vacío (sin especificar el tamaño) se puede inicializar de las siguientes maneras:
#
#

array₂ = Array{Any,1}()


array₃ = []


#
# En las inicializaciones anteriores el array acepta cualquier tipo de valor, pues su definición contempla elementos con el supertipo `Any` (en el caso de `array₂` se define de forma explícita y en el de `array₃` de forma implícita).
#
# Pueden existir casos en que el array solo deba contener elementos de un número limitado de tipos, en tales casos la declaración del tipo del array puede ser decalarado mediante `Union`, por ejemplo: `Array{Union{Int64,Float64,String},2}`. En el ejemplo el array solo podrá contener elementos del tipo: `Int64`, `Float64` o `String`.
#
#

array₄ = Array{Float64,1}()


array₅ = Float64[]


#
# Los arrays anteriores están vacios, pero solo aceptan elementos que son del tipo `Float64` o que pueden ser promovidos a ese tipo (por ejemplo Int64 puede ser convertido a Float64).
#
#

push!(array₄,2)


##push!(array₅,"Hola")


#
# **NOTAS:** Con el método `push!` se pueden agregar elementos al array. 
#
#

#
# Si ya se conoce el contenido del array, este puede ser inicializado como 'array literal', es decir puede ser creado de forma manual. 
#
#

array₆ = [1,2,3]


#
# El array anterior, que es de 1 dimensión, es conocido como array columna, en lugar de utilizar `,` puede utilizar `;`. Un array fila puede ser creado si cada elemento del array es separado mediante un espacio.
#
#

array₇ = [1 2 3]


#
# Como puede notar el tipo de dato entre `array₆` y `array₇` difieren, el primero es de una dimensión mientras que el segundo es de dos dimensiones. Lo anterior significa que en `array₇`  cada elemento se encuentra en columnas diferentes de la matriz.
#
# Lo anterior puede suponer varias ventajas, ya que se pueden realizar operaciones conocidas entre matrices.
#
#

array₇*array₆


array₆*array₇


#
# En el caso de un array de dos dimensiones, existen diferentes formas en que este puede ser inicializado de formas manual. 
#
#

array₈ = [[1,2,3] [4,5,6] [7,8,9]]


#
# En el caso anterior el array fue creado por columnas, mediante el ingreso de arrays columnas separados por un espacio. Similares resultados se obtienen al utilizar el método [hcat](https://docs.julialang.org/en/v1/base/arrays/##Base.hcat).
#
#

hcat([1,2,3],[4,5,6])


array₉ = [[1 2 3]; [4 5 6]]


#
# En el caso anterior el array fue creado mediante filas, mediante el ingreso de arrays filas separados por una `;`. Similares resultados se obtienen al utilizar el método [vcat](https://docs.julialang.org/en/v1/base/arrays/##Base.vcat). 
#
#

vcat([1 2 3], [4 5 6] )


#
# #### Inicialización de arrays especiales
#
# Julia implementa métodos que pueden ser útiles para crear array de forma rápida
#
# ##### Array de ceros
# Un array de ceros puede ser creado con el método `zeros`, que admite como argumentos el tipo de datos y luego las dimensiones del array.
#
#

zeros(Int64,4)


zeros(Float64,4,4)


#
# ##### Array de unos
# Un array de unos puede ser creado con el método `ones`, que admite como argumentos el tipo de datos y luego las dimensiones del array.
#
#

ones(Float64,3)


ones(Float64,3,3)


#
# **NOTA:** Puede leer más sobre inicialización de arrays y métodos para el manejo de arrays en:
# + La sección [Construction and Initialization](https://docs.julialang.org/en/v1/manual/arrays/##Construction-and-Initialization) del capítulo Multi-dimensional Arrays del manual de Julia.
# + La sección [Constructors and Types](https://docs.julialang.org/en/v1/base/arrays/##Constructors-and-Types) del capítulo Arrays en el manual de Julia.
#
#

#
# #### Accediendo a los elementos de un Array
# Se accede a los elementos de un array con `[]` e indicando la posición de elemento.
#
#

array₆


array₆[1]


#
# Como puede ser observado, a diferencia de otros lenguajes de programación, Julia tiene indexación base-1, lo que significa que para el primer elemento del array tiene índice `1` no `0`.
#
#

array3dim = cat([1 2; 3 4],["Hola 1" "Hola 2"; "Hola 3" "Hola 4"],[5 6; 7 8],dims=3)


array3dim[1,1,2]


#
# En el ejemplo anterior se accede a un elemento de un array de 3 dimensiones, indicando la posición con una terna de números. Sin embargo, se puede acceder a más de un elemento extrayendo un subarray del primero.
#
#

array₆[2:3]


#
# Los índices de los elementos que se desean extraer pueden ser indicados mediante un objeto tipo range como se muestra en el ejemplo.
#
#

array3dim[:,:,2]


#
# Con el símbolo `:` se indica que todos los elementos de una dimensión del array deben ser extraidos; en el caso del array de una dimensión, todos los elementos del mismo.
#
#

array3dim[1,1:2,3]


array3dim[1:2,2,1]


#
# En los ejemplos anteriores, se recorre el arreglo por columna y por fila respectivamente. 
#
# Para un mejor rendimiento, se recomienda recorrer un array por columna, debido a la forma en que Julia almacena los elementos del array en memoria, como se muestra a continuación.
#
#

array₁₀ = [1:3 4:6 7:9 ] 


#
# Note que `array₁₀` es un array de dos dimensiones y aunque se accede a los elementos indicando dos índices, también puede accederse mediante un solo índice.
#
#

[array₁₀[1],array₁₀[3],array₁₀[4],array₁₀[6],array₁₀[7],array₁₀[end]]


#
# Ello indica que Julia almacena los elementos del array de forma concecutiva por columnas. Por ende, si el array se recorre por filas se tendrán que hacer cálculos adicionales llevando un costo de tiempo; por esa razón el costo por acceder a un array por columnas es menor que si se hiciera por filas.
#
#

#
# #### Métodos útiles
# Julia implementa numerosos métodos para trabajar con los arrays. A continuación, se mencionan algunas de las más básicas que permiten realizar varias tareas.
#
# En Julia los argumentos de las funciones se pasan por referencia, es decir, se pasa a la función no solo el valor contenido en la variable sino que, también se pasa la referencia a la variable original. Lo anterior significa que, cualquier cambio realizado al argumento dentro de la función, dicho cambio se verá reflejado en la variable original.
#
#
# Por ello, en Julia se sigue una convención cuando se diseña funciones y esta es: si la función modifica el argumento se denota añadiendo al nombre el símbolo `!`. La convención mencionada es algo que se mira en varias funciones, existiendo dos versiones de las mismas (las que modifican la variable original y las que no).
#
#

array₁₁ = [1,2,3]


#
# El método `push!` modifica el array, agregando un elementos al final del mismo y devuelve el array modificado. 
#
#

push!(array₁₁,4)


push!(array₁₁,5,6,7)


array₁₁


#
# El método `pushfirst!` añade un elementos al inicio del array.
#
#

pushfirst!(array₁₁,-1,0)


#
# El método `pop!` elimina y retorna el último elemento del array.
#
#

pop!(array₁₁)


array₁₁


#
# El método `popfirst!` elimina y retorna el primer elemento del array.
#
#

popfirst!(array₁₁)


array₁₁


#
# El método `deleteat!` elimina un elemento indicado la posición de este dentro del array y devuelve el array modificado.
#
#

deleteat!(array₁₁,6)


deleteat!(array₁₁,3:5)


#
# El método `splice!` tiene diferentes formas de uso; entre ellas reemplazar elementos del array, indicando la posición del elemento por reemplazar y devuelve el valor del elemento reemplazado.
#
#

splice!(array₁₁,3,3)


array₁₁


# Otra forma de uso es la de eliminar elementos, para ello solo se pasa como argumento el array a modificar y la posición del elemento, el método devuelve el valor del elemento eliminado.
#

splice!(array₁₁,2)


array₁₁


#
# Otro caso de uso para `splice!` es la eliminar un elemento y agregar elementos indicándolos por medio de una lista.
#
#

splice!(array₁₁,2,[3,4,5,6])


array₁₁


splice!(array₁₁,2,[1,2,3])


array₁₁


#
# También pueden insertarse elementos elementos entre otros, como se ilustra.
#
#

splice!(array₁₁,4:3,0)


array₁₁


#
# Para que `splice!` funcione de la manera mostrada anteriormente, es necesario que el índice de los elementos donde se desea insertaer se indiquen de la forma `n:n-1`.
#
#

#
# Existe un método para vaciar un array, este método presenta los dos casos: el que modifca el array y el que no, el método `empty` y `empty!`.
#
#

empty(array₁₁)


array₁₁


empty!(array₁₁)


array₁₁


#
# El método `maximum` devuelve el máximo valor entre los valores del elemento de un array.
#
#

maximum([1 2; 3 4])


#
# El método `minimum` devuelve el mínimo valor entre los valores del elemento de un array.
#
#

minimum([1 2; 3 4])


#
# Merece la atención los métodos anteriores, pues devuelven el menor o mayor valor (según sea el caso) al llamado de una función, para más información puede leer del manual de Julia las [secciones](https://docs.julialang.org/en/v1/base/collections/##Base.maximum) relacionadas con dichas funciones.
#
#

#
# El método `isempty` comprueba si el array está vacío o no.
#
#

isempty(array₁₁)


isempty(array₁₀)


#
# La función `length` devuelve la cantidad de elementos que contiene el array.
#
#

length(array₁₁)


length(array₁₀)


#
# La funión `size` devuelve las una tupla con las dimensiones del array.
#
#

size(array₄)


size(array₁)


#
# **NOTA:** Más funciones e información acerca de los arrays en Julia puede encontrarlo en el manual en la seción [Multi-dimensional Arrays](https://docs.julialang.org/en/v1/base/collections/##Base.maximum)
#
#

#
# #### Precauciones
#
# Al trabajar con arrays en alguna ocasión se querrá hacer una copia de esta. 
#
#

array₁₂ = [1,2,3]


arraycopy₁ = array₁₂


#
# El problema de las líneas anteriores es: tanto `array₁₂` como `arraycopy₁` apuntan a la misma dirección de memoria, es decir, la línea de código `arraycopy₁ = array₁₂` no asigna el valor de `array₁₂` en `arraycopy₁`. 
#
# Para comprobar lo anterior puede usarse el operador de igualdad estricto.
#
#

arraycopy₁ === array₁₂


#
# Además cualquier modificación realizado con `arraycopy₁` se verá reflejado en `array₁₂`.
#
#

push!(arraycopy₁,5)


array₁₂


#
# Para hacer una copia de una array puede utilizarse el método `copy`, el cual realiza una copia del array indicado en su argumento.
#
#

arraycopy₂ =  copy(array₁₂)


arraycopy₂ === array₁₂


push!(arraycopy₂,5)


array₁₂


#
# ### Tuplas
#
# Las tuplas (`Tuples`) son listas, pero a diferencia de los arrays son inmutables. Lo anterior quiere decir que una vez creada la tupla esta no puede ser: modificada, eliminada o sobreescrita, como sí sucede con los arrays. Al igual que los arrays, las tuplas pueden contener diferentes tipos de datos.
#
# Para crear una tupla, los elementos de esta se encierran entre `()`. Se accede a los elementos de las tuplas de la misma forma en que se acceden a los elementos de un array.
#
#

tuple₁ = (1,2,3,"Hello, World!")


typeof(tuple₁)


#
# Para comprobar la inmutabilidad de las tuplas, se puede intentar modificar el tercer elemento de `tuple₁` y de `array₁₂` (en el caso de `tuple₁` se obtendrá un error).
#
#

##tuple₁[3] = 33


array₁₂[3] = 33; array₁₂


#
# Cuando se quiere crear una tupla de un solo elemento, al final del elemento debe colocarse `,`; ya que el uso de `( )` serían interpretados como elementos de agrupación. Para mantener coherencia, se permite lo mismo si se inicializa una tupla con más de un elemento.
#
#

tuple₂ = (11,) ##Compare con la asignación (11), en el cual tuple₂ sería una variable con valor 11 del tipo Int64


tuple₃ = (11,12,13,) 


#
# A menudo las tuplas son implementadas en el retorno de funciones, por ejemplo, la función `divrem` que retorna el cociente y el resto de la división euclidiana. Cuando sucede lo anterior los valores de la tupla se puede descomprimir en variables.
#
#

cociente,resto = divrem(39,5)


[cociente, resto]


#
# Una discusión sobre el tipo de tuplas puede encontrarse en [Tipos de Tuplas](https://riptutorial.com/es/julia-lang/example/22725/tipos-de-tuplas).
#
#

#
# De forma opcional, los componentes de las tuplas pueden ser nombrados, en dicho caso se construye una tupla con nombre.
#
#

tuple₄ = (a = 2, b = 4)


#
# Para acceder a los elementos de una tupla nombrada se puede usar la sintaxis `tuple.nombre`, además de la indexación normal `tuple[i]`.
#
#

[tuple₄.a, tuple₄[:b], tuple₄[1],tuple₄[2]]


#
# ### Diccionarios
# Los diccionarios son estructuras de datos que guardan datos en forma de clave-valor, es decir es una colección asociativa de datos que no tienen un orden establecido. 
#
# #### Creando diccionarios
# Los diccionarios son creados con el constructor `Dict`, este puede contener como argumentos las claves asociadas a los valores o puede usarse sin argumentos (en dicho caso es vacío).
#
#

dict₁ = Dict("Nombre"=>"Juan", "Edad"=>25, 3.0=>50)


#
# Como puede observarse, los diccionarios pueden aceptar como clave cualquier valor y tipo de dato. Para asociar los valores a las claves se utiliza el operador `=>`, que corresponde a la función `Pair()`.
#
#

dict₂ = Dict()


#
# `dict₂` es un diccionario vacío, pero al igual que los array es una estructura de datos mutable, por lo que pueden añadirse elementos como se haría con array asociativos en otros lenguajes de programación.
#
#

begin
	dict₂[1] = "Hola" ##La clave es un valor del tipo Int64
	dict₂["dos"] = "Mundo" ##La clave es un valor del tipo String
	dict₂[:x] = "Tercer elemento" ##La clave es un valor del tipo Symbol
	dict₂
end


#
# En un diccionario las claves deben ser únicas, no pueden existir dos claves con el mismo nombre.
#
#

[dict₂[1], dict₂["dos"], dict₂[:x]]


#
# #### Accediendo a los elementos de un array
# El acceso a los elementos del diccionario se realiza de forma similar a los arrays, mediante `[ ]`.
#
#

Dict("nombre"=>"Pedro", "nombre"=>"Rodríguez") ##El diccionario solo tiene una única clave 'nombre'


#
# #### Métodos usados con diccionarios
#
# Para conocer si una determinada clave existe se puede utilizar el método `haskey`, este retorna `true` o `false` dependiendo si existe o no.
#
#

[haskey(dict₂,5), haskey(dict₂,"dos"), haskey(dict₂,1)]


#
# De igual manera se puede determinar si un par `clave => valor` existe con el método `in`.
#
#

[in(1=>"Hola",dict₂), in(1=>"Mundo",dict₂)]


#
# Las claves de un diccionario pueden obtenerse uando el método `keys`, el cual retorna un iterador. Se puede utilizar el método collect para transformarlo en un array.
#
#

keys(dict₂)


collect(keys(dict₂))


#
# Los valores de un diccionario pueden obtenerse uando el método `values`, el cual retorna una lista con ellas.
#
#

values(dict₂)


collect(values(dict₂))


#
# #### Tipos de datos y los diccionarios
#
# En los diccionarios mostrados, el tipo de dato para la clave y los valores podian ser de diferentes tipos. La definición del diccionario es la siguiente: 
#
#

typeof(dict₂)


#
# Como se observa `Dict{Any,Any}` indica que el tipo de dato para la clave es `Any` y para el valor es `Any`.
#
# Puede crearse un dicconario que restrinja el tipo de datos para la clave y valor, de esta manera puede crearse una estructura más compleja.
#
#

typeof(Dict{String,Int64}("1"=>1, "2"=>2))


typeof(Dict{Int64,String}(1=>"1", 2=>"2"))


typeof(Dict{Symbol,Int64}(:x=>1, :y=>2))


#
# Los diccionarios y las tuplas nombradas pueden ser vistas como similares, sin embargo, existe una diferencia fundamental: que las tuplas son inmutables mientras que los diccionarios son mutables.
#
#

#
# ### Conjuntos 
#
# Los conjuntos son una colección desordenada de datos, pero tiene la peculiaridad de no contener elementos duplicados. Así como los arrays y los diccionarios son tipos de datos mutables. 
#
# Al igual que los arrays y los diccionarios, los conjuntos pueden contener cualquier tipo de dato. Al momento de crear el conjunto el tipo de dato puede ser especificado, como en otras estructuras de datos; si el tipo de dato no es especificado, Julia analiza los elementos y define  el tipo de datos del conjunto.
#
# #### Creación de conjuntos
# Los conjuntos son creados con el constructor `set`, cuyos elementos son especificados mediante un array.
#
#

set₁ = Set()


#
# `set₁ ` es un conjunto vacío que acepta cualquier valor.
#
#

set₂ = Set{Float64}()


#
# `set₂` es un conjunto vacío que acepta valores del tipo `Float64`.
#
#

set₃ = Set([1,2,3,3])


#
# Como puede notar, en el caso de `set₃` Julia analiza que los elementos del conjunto `set₃` deben ser del tipo `Int64`. Dado que los conjuntos no pueden tener elementos duplicados, finalmente `set₃` solo tiene un elemento con valor `3`. 
#
# Con el método `push!` se puede agregar datos a los conjuntos, pero dado que es una colección de elementos desordenados no puede usarse el método `pushfirts!`. De la misma manera con el método `pop!` se puede eliminar un elemento del conjunto indicándolo.
#
#

push!(set₃,5)


pop!(set₃,5)


set₃


#
# #### Métodos relacionados con los conjuntos
# Julia implementa diferentes métodos para trabajar con los conjuntos (muchos de ellos también puede usarse con los diccionarios).
#
#

set₄ = Set(["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado"])


set₅ = Set([1,2,3,"Sábado","Domingo"])


#
# Con el método `union` los conjuntos pueden fusionarse, retornando el conjunto resultante. Es decir, un conjunto cuyo elementos se ecuentra en cualquiera de los conjuntos.
#
#

union(set₅,set₄)


union!(set₅,set₄)


set₅


#
# Con el método `intersect` se obtiene la interseción de los conjuntos, es decir, devuelve un conjunto cuyos elementos se encuentra en ambos conjuntos.
#
#

intersect(set₄,set₅)


intersect!(set₄,set₅)


set₄


#
# Con el método `setdiff` se obtiene la diferencia entre dos conjuntos, es decir, un conjunto cuyos elementos se encuentran en uno de ellos no en el otro.
#
#

setdiff(set₅,set₄)


setdiff!(set₅,set₄)


#
# ### List Comprehensions
# Es un forma poderosa de crear listas o array, que también ha sido implementado en otros lenguages de programación.  Tiene la peculiaridad de definir los elementos del array de manera similar a la notación constructiva en matemática. Tiene la siguiente sintaxis:
#
# `[ expresión for elemento ∈ colección]`
#
#
# Por ejemplo, un matemático podría decir: *'Sea $S$ el conjunto de todos los elementos $n$, donde $n$ es mayor o igual a $1$ y menor o igual a $10$'*. En tal caso se puede crear en Julia un conjunto que responda a la definición anterior.
#
#

set₆ = Set([n for n ∈ 1:10])


#
# Ahora bien, con list comprehensions se pueden utilizar condicionales que actúan como filtro, de esta manera se pueden definir conjuntos más complicados como: *'Sea $C$ el conjunto de todos los elementos $n$; donde $n$ es mayor o igual a $1$, menor o igual a $10$ y divisible entre dos'*
#
#

C = [n for n ∈ 1:10 if n%2 == 0]


#
# Dobles iteraciones se pueden utilizar para generar arrays con list comprehensions.
#
#

tablamultiplicar₁ = [ x*y for x ∈ 1:10, y ∈ 1:10]


#
# En el caso anterior, se crea un array de más de dos dimensiones. Si se utiliza dos bucles de forma explícita se generan los mismos elementos, sin embargo, de esta última manera se crea un array de una dimensión.  
#
#

tablamultiplicar₂ = [ x*y for x ∈ 1:10 for y ∈ 1:5]


#
# Al igual que los arrays el tipo de datos de los elementos es analizado por Julia, pero este puede ser definido de forma explícita.
#
#

typeof(Float64[n for n ∈ 1:10])


#
# Tipos de estructuras como: diccionarios, tuplas y arrays, pueden ser usados para realizar iteraciones.
#
#

for i ∈ [1,2,3]
	print(i)
end


tuple₄


for i ∈ tuple₄
	print(i)
end


dict₂


for i ∈ keys(dict₂)
	print(dict₂[i])
end


for i ∈ values(dict₂)
	print(i)
end


StarcoderdataJupyterScriptsDedupFiltered