-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlists.tex
1278 lines (1025 loc) · 56.7 KB
/
lists.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\newpage
\part{Estructuras de Datos Dinámicas}
\setcounter{section}{0}
%\begin{center}
%\huge{\textbf{Estructuras de Datos Dinámicas}} \\
%\end{center}
Una estructura de datos dinámica se refiere a una organización en memoria como una colección de datos que es flexible al momento de expandirse o reducirse en tamaño en tiempo de ejecución de un algoritmo. En muchas ocasiones, este tipo de estructuras permite al programador tener control sobre cuánta memoria está empleando. Las estructuras dinámicas juegan un papel muy importante en diversos lenguajes de programación ofreciendo la posibilidad de ajustar la cantidad de memoria que un programa empleará.
Las estructuras de datos estáticas es el caso opuesto a las dinámicas, donde el tamaño de la estructura es fija. Son ideales para almacenar un número fijo de elementos pero carecen de la flexibilidad para consumir memoria adicional si la requieren o liberar memoria para mejorar el rendimiento de los programas.
Entonces, para cada tipo de dato se presenta la definición de ésta, una posible clasificación (si es posible), su especificación e implementación. La idea de la especificación empleando una clase está en responder ¿qué hace? la estructura y no en ¿cómo lo hace?. Para responder la segunda pregunta, se utilizará la subsección de Implementación. En este documento se presentan las estructuras básicas dinámicas: tipo List, Stack y Queue.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Tipo List}
El tipo de dato List representa a una lista de elementos la cual permite representar una serie de actividades, tareas, objetos, etc. Una lista del mercado, una lista de cosas por hacer, una lista de libros por leer, entre muchos otros ejemplos de la vida diaria.
Una lista es una colección finita de elementos del mismo tipo, ordenados de acuerdo a su posición en la lista. Podemos utilizar la siguiente notación para denotar una lista $L$ de $n$ elementos: si la lista es vacía se denota como $L=()$, y si contiene elementos entonces como $L=(e_1, e_2, ..., e_k)$. Generalmente las listas tienen las siguientes características:
\begin{itemize}
\item Todos los elementos son de un mismo tipo de dato
\item Las listas pueden crecer o decrecer en tiempo de ejecución
\item Se pueden subdividir en sublistas
\item Su acceso es de forma secuencial (un elemento seguido de otro)
\end{itemize}
Un tipo List es una estructura flexible que puede crecer y acortarse según se requiera. Mediante una posición dentro de la lista es posible moverse y efectuar operaciones básicas como insertar, eliminar, modificar un elemento, o concatenarse con otras listas. Una clasificación básica de listas puede ser de acuerdo al número de enlaces o conexiones con los otros elementos de la lista. Así, se pueden clasificar en listas simplemente enlazadas (acceso en un solo sentido) o doblemente enlazadas (acceso en ambos sentidos). A continuación se describe cada una de ellas.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Simplemente Enlazada}
Los elementos en una lista están colocados en forma secuencial. Ahora, esto no quiere decir que están contiguos en memoria, de hecho, en estructuras dinámicas los elementos se pueden encontrar dispersos en memoria pero enlazados desde una posición y tener acceso al siguiente elemento.
Para una lista simplemente enlazada, se define una estructura (denominada Node) que almacenará el valor como tal de la estructura homogénea (denominada List), y un enlace al próximo elemento desde dicha estructura Node. Primeramente, se realizará la especificación de una lista sin entrar en detalles de su implementación basado en un conjunto de atributos y métodos dentro de una clase. Luego, se muestra las implementaciones posibles dada dicha especificación.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{Especificación}
En una especificación de una estructura de datos se pueden diferenciar dos roles principales: usuario y programador. En el rol de usuario, se emplea la estructura de datos sin conocer cómo está implementado sino empleando los métodos dispuestos en la especificación de la estructura. Ahora, el rol de programador incluye el uso de la estructura según su especificación y la forma como será implementado (i.e. uso de variables, desarrollo de las unidades invocables).
Para la especificación de la lista, se define como atributo un tipo de dato que represente una posición dentro de una lista. Esta posición en su implementación puede ser un tipo Integer, Pointer, entre otros. Así, el tipo List se especifica en la siguiente clase.
\begin{lstlisting}[upquote=true, language=pseudo]
class List <T> // definición de una plantilla o template de listas, T indica el tipo de dato
public:
Type <...> tPosition // tipo de dato posición para desplazarse (sin mostrar la implementación)
Constructor List() // construye la lista vacía ().
Destructor List() //destructor de la clase. Libera la lista de memoria
function IsEmpty() : Boolean // retorna True si la lista es vacía, y false en caso contrario
function First() : tPosition // retorna la posición del 1er elemento de la lista, o Last() si la lista es vacía
function Last() : tPosition // retorna la posición final de lista (posición después del último)
void Next(ref tPosition pValue) // Pre: pValue != Last().Mueve la posición hacia la posición siguiente de la lista
function Get (tPosition pValue): ref T// Retorna la referencia a la información contenida en pValue
void Insert (ref T x, tPosition pValue)// Inserta x antes de la posición pValue
void Delete (tPosition pValue) // Pre: pValue!=Last(). Elimina el elemento de pValue. La posición pValue queda // referenciando a un elemento inexistente
function Size() : Integer // retorna el número de elementos en la lista
end
\end{lstlisting}
Nótese que la función Last retorna la posición final o posición disponible al final de la lista, lo cual difiere a ser el último elemento de la lista. Del mismo modo, es importante destacar que existe pase de parámetros por referencia y retorno de la referencia del tipo de dato $T$ que es empleado de forma genérica como plantilla en la clase.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{Ejemplos}
A continuación una serie de ejemplos donde se utiliza las unidades invocables presentes en el tipo List sin conocer su implementación.
- Dada una lista, imprimir sus elementos
\begin{lstlisting}[upquote=true, language=pseudo]
void PrintList(ref List <T> L)
tPosition tIndex = L.First()
while tIndex != L.Last() do
Print (*L.Get(tIndex))
L.Next(ref tIndex)
end
end
\end{lstlisting}
El algoritmo consiste simplemente en dada la primera posición de la lista, iterar de forma secuencial sobre cada elemento e imprimir su valor.
- Dada una lista de enteros, asignar el valor de 666 a todos sus elementos:
\begin{lstlisting}[upquote=true, language=pseudo]
void Change (ref List <Integer> L)
tPosition tIndex = L.First()
Const Integer iSixSixSix = 666
while tIndex != L.Last() do
L.Insert(ref iSixSixSix, tIndex) // también se puede emplear *L.Get(tIndex) = iSixSixSix
L.Next(ref tIndex)
end
end
\end{lstlisting}
Siendo muy similar al algoritmo anterior, dado el primer elemento de la lista se itera sobre cada elemento y se emplea la función Insert dada la posición y la constante a añadir.
- Dada una lista de valores enteros, eliminar todos los elementos que tengan valor mayor a 15:
\begin{lstlisting}[upquote=true, language=pseudo]
void Delete15 (ref List <Integer> L)
tPosition tIndex = L.First()
while tIndex != L.Last() do
if *L.Get(tIndex) > 15 then
tPosition tTemp = tIndex
L.Next (ref tTemp)
L.Delete (tIndex)
tIndex = tTemp
else
L.Next(ref tIndex)
end
end
end
\end{lstlisting}
Esta función busca todas las posiciones que cumplen con la condición $> 15$ y para cada una ``salta" una posición más allá de la actual con el fin de eliminar todos aquellos elementos que cumple dicha condición. Existen algunas consideraciones que se deben tomar en cuenta al momento de su ejecución, pero que dependen de la implementación tal como liberación de la memoria si es dinámica, enlace de los elementos de forma correcta, entre otros.
-Concatenar dos listas del tipo User llamadas A y B, y dejar el resultado en la lista A
\begin{lstlisting}[upquote=true, language=pseudo]
void Concat (ref List<User> A, B)
tPosition tIndex = B.First()
while tIndex != B.Last() do
A.Insert(B.Get(tIndex), A.Last())
B.Next(ref tIndex)
end
end
\end{lstlisting}
Al igual que operar sobre un par de arreglos, dada la lista A y a partir de su posición final se anexará cada uno de los elementos que pertenecen a B hasta que no queden elementos en B. Nuevamente, aspectos como no eliminar los elementos de B o el tamaño máximo de elementos de A, son concernientes al programador.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{Implementación}
La forma básica de implementar una lista simplemente enlazada es empleando un arreglo unidimensional dinámico, donde a medida que se agreguen o eliminen elementos, el tamaño de la lista se modifica. Una operación de acceso, inserción, eliminación y búsqueda tendrá complejidad $O(n)$, pero con una implementación simple. Otra forma de implementar el tipo List con arreglos, es empleando un arreglo de registros, donde cada posición contiene el valor a almacenar (información) y el índice al próximo elemento dentro de la lista. Al mismo tiempo, es posible tener sublistas para definir las próximas posiciones libres u ocupadas del arreglo. A diferencia de la implementación anterior, ésta es estática y se debe definir un tamaño máximo de elementos.
En diversos lenguajes de programación existen implementaciones dinámicas de listas como es el caso en C++ empleando la librería STL (i.e. vector), en Java de forma nativa (i.e. Vector), en Python se emplea como un tipo de dato secuencia, entre otros. A continuación, estudiaremos diversas implementaciones .
\paragraph{Implementación con arreglos}
Para la implementación con arreglos se asume que existe un tamaño máximo o que se modifica de forma dinámica. Ahora, el punto importante es que para cada posición dentro del arreglo se debe conocer cuál es el próximo elemento dada una posición.
De este modo, la Fig. \ref{fig:listarray} muestra un ejemplo de una posible distribución del tipo List empleando arreglos.
\begin{figure}[htpb!]
\begin{center}
\includegraphics[width=0.5\textwidth]{images/listarray.eps}
\end{center}
\caption{Ejemplo de representación de una lista simplemente enlazada empleando arreglos.}
\label{fig:listarray}
\end{figure}
La Fig. \ref{fig:listarray} es un instante de tiempo en durante la ejecución de la estructura. En esta implementación se consideran dos posiciones claves que son la primera posición libre y la primera posición ocupada. Se debe mantener un arreglo del tipo Position, y otro donde se almacena el tipo de dato de la lista.
En el ejemplo, la primera casilla ocupada está en la posición 2 correspondiente al valor (tInfo) 'A'. Desde allí, el próximo campo ocupado (Next) se encuentra en el valor que indica el arreglo de ocupados, es decir, la posición 6. La posición 6 corresponde al valor 'B'., luego a la posición 1 para el valor 'C', y finalmente el último valor está en la posición 5 correspondiente a 'D'. El próximo elemento ocupado desde 'D' corresponde al valor de 0, este valor indica la finalización de la lista.
De forma similar ocurre con la primera posición libre en la posición 4, la segunda libre será la posición 3, luego la posición 7 y ésta será la última para dicho arreglo.
\paragraph{Implementación con apuntadores}
En este caso, tenemos un conjunto de nodos enlazados mediante el campo denominado pNext. Por otro lado, la lista contendrá a un apuntador al primer elemento de dicha lista y el valor pNext del último nodo apunta a NIL. En esta versión las operaciones de Insertar y Eliminar son de complejidad $O(n)$.
En la Fig. \ref{fig:listas} se muestra de forma visual la representación de una lista simplemente enlazada. Se nota que el inicio de la lista está indicada por el apuntador $pFirst$, de allí el nodo con $tInfo = e_1$ indica el siguiente elemento de la lista a través del campo $pNext$ hasta llegar al final de la lista. Empleando apuntadores, el final de la lista se ubica cuando el próximo es NIL (representado por el símbolo $\backslash \backslash$ en la figura.
\begin{figure}[!htpb]
\centering
\includegraphics[scale=.7]{images/listas.eps}
\caption{Representación visual de una estructura de lista simplemente enlazada empleando apuntadores.}
\label{fig:listas}
\end{figure}
\begin{lstlisting}[upquote=true, language=pseudo]
class Node <T>
public:
T tInfo
Node<T>* pNext
end
class List <T>
private:
Node<T>* pFirst // apuntador al primer nodo
Integer iN // número de elementos en la lista
public:
Type Node<T>* tPosition // la posición es un apuntador a Node*<T>
Constructor List ()
iN = 0
pFirst = NIL
end
Destructor List ()
Node<T>* pTemp
while pFirst != NIL then
pTemp = pFirst
pFirst = (*pFirst).pNext
delete pTemp
end
end
function IsEmpty() : Boolean
return pFirst == NIL
end
function First() : tPosition
return pFirst
end
function Last() : tPosition
return NIL
end
void Next(ref tPosition pValue)
pValue = *pValue.pNext
end
function Get(tPosition pValue) : ref T
return ref (*pValue).tInfo
end
void Insert(ref T x, tPosition pValue) //O(n). Se require que el nodo anterior apunte al próximo nodo
Node<T>* pNew = new Node
(*pNew).tInfo = *x
(*pNew).pNext = pValue
if (pFirst == NIL or pFirst == pValue) then
pFirst = pNew
else
Node<T>* pTemp = pFirst
while *pTemp.pNext != pValue do
pTemp = *pTemp.pNext
end
*pTemp.pNext = pNew
end
iN = iN + 1
end
void Delete(tPosition pValue)
if pFirst == pValue then
pFirst = *pFirst.pNext
else
Node<T>* pTemp = pFirst
while *pTemp.pNext != pValue do
pTemp = *pTemp.pNext
end
*pTemp.pNext = *pValue.pNext
end
delete pValue
iN = iN - 1
end
function Size() : Integer
return iN
end
end
\end{lstlisting}
Es posible lograr la operación de inserción y eliminación de complejidad O(1) si se cuenta con un puntero al nodo anterior. Del mismo modo, si se contará con un puntero a los nodos anteriores es posible hacer el recorrido en ambos sentidos (ver sección \ref{lb:dobleenlazada}.
\paragraph{Implementación con apuntadores y nodo cabeza}
Un nodo cabeza es un nodo especial por donde se realizarán las operaciones de la lista. Con el nodo cabeza se garantiza que todo nodo tiene un predecesor en la lista, y la posición de un nodo se referenciará desde la posición del nodo anterior. En esta implementación, todas las operaciones son de complejidad $O(1)$. Adicionalmente, como atributo de la clase se requiere mantener un apuntador al último nodo para que la operación Last() sea de complejidad $O(1)$.
\begin{lstlisting}[upquote=true, language=pseudo]
class Node <T>
public:
T tInfo
Node<T>* pNext
end
class List <T>
private:
Node<T>* pFirst // apuntador al nodo cabeza. Este apuntador no se modifica
Node<T>* pLast // apuntador al último nodo (nodo cuyo próximo es NIL)
Integer iN // número de elementos en la lista
public:
Type Node<T>* tPosition // la posición es un apuntador a Node*<T>
Constructor List ()
iN = 0
pFirst = new Node ()
pLast = pFirst
*pFirst.pNext = NIL
end
Destructor List ()
Node<T>* pTemp
while pFirst != NIL then
pTemp = pFirst
pFirst = (*pFirst).pNext
delete pTemp
end
end
function IsEmpty() : Boolean
return *pFirst.pNext == NIL
end
function First() : tPosition
return pFirst
end
function Last() : tPosition
return pLast
end
void Next(ref tPosition pValue)
pValue = *pValue.pNext
end
function Get(tPosition pValue) : ref T
return ref *(*pValue.pNext).tInfo
end
void Insert(ref T x, tPosition pValue)
Node<T>* pNew = new Node
(*pNew).tInfo = *x
if (pFirst == pValue) then // insertando de primero
*pNew.pNext = *pFirst.pNext
*pFirst.pNext = pNew
else
*pNew.pNext = *pValue.pNext
*pValue.pNext = pNew
end
if pLast == pValue then
pLast = pNew
end
iN = iN + 1
end
void Delete (tPosition pValue)
if (*pValue.pNext == pLast) then
pLast = pValue
end
Node<T>* pTemp = *pValue.pNext
*pValue.pNext = (*(*pValue.pNext)).pNext
delete pTemp
N = N-1;
end
function Size() : Integer
return iN
end
end
\end{lstlisting}
\paragraph{Implementación eficiente con apuntadores}
En la primera implementación con apuntadores todas las operaciones son $O(1)$ a excepción de las funciones Insert y Delete. Para lograr que estas operaciones sean $O(1)$, se debe emplear como posición la dirección del campo pNext del nodo anterior. Dado que el primer nodo no posee un nodo anterior, se emplea la dirección de pFirst. De esta manera, no se buscará al nodo anterior en la inserción y eliminación ya que se referenciará al campo pNext del nodo anterior. Así, todas las operaciones son $O(1)$ a excepción de Last() porque ésta debe retornar el apuntador al campo pNext del último nodo (o la dirección de pFirst) de complejidad $O(N)$ en el peor de los casos. Para evitar ello, se introduce un atributo apuntador para el último campo pNext como atributo de la lista, el cual posiblemente deba actualizarse durante la operación de inserción y eliminación.
\begin{lstlisting}[upquote=true, language=pseudo]
class Node <T>
public:
T tInfo
Node<T>* pNext
end
class List <T>
public:
Type Node<T>** tPosition // la posición es un apuntador doble a Node**<T>
private:
Node<T>* pFirst // apuntador al primer nodo
tPosition pLast // apuntador al último nodo
Integer iN // número de elementos en la lista
public:
Constructor List ()
iN = 0
pFirst = NIL
pLast = ref pFirst
end
Destructor List ()
Node<T>* pTemp
while pFirst != NIL then
pTemp = pFirst
pFirst = (*pFirst).pNext
delete pTemp
end
end
function IsEmpty() : Boolean
return pFirst == NIL
end
function First() : tPosition
return ref pFirst
end
function Last() : tPosition
return pLast
end
void Next(ref tPosition pValue)
pValue = ref (*(*pValue.pNext))
end
function Get(tPosition pValue) : ref T
return ref (*(*pValue)).tInfo
end
void Insert(ref T x, tPosition pValue)
Node<T>* pNew = new Node
(*pNew).tInfo = *x
if pFirst == NIL then // la lista estaba vacía
*pNew.pNext = NIL
pFirst = pNew
pLast = ref *pNew.pNext
elseif (*pValue == pFirst) then // insertando en la lista de primero
*pNew.pNext = pFirst
pFirst = pNew
elseif *pValue == NIL then // insertando en la lista de último
*pLast = pNew
*pNew.pNext = NIL
pLast = ref (*pNew).pNext
else // insertando en un nodo intermedio
*pNew.pNext = *pValue
*pValue = pNew
end
iN = iN + 1
end
void Delete (tPosition pValue)
Node<T>* pTemp = *pValue
if *pValue == pFirst then // se elimina el primer elemento
pFirst = *pFirst.pNext
if pFirst == NIL then
pLast = ref pFirst
end
elseif ((*(*pValue)).pNext == NIL) then //se elimina el ultimo element
pLast = pValue
*pValue = NIL
else // se elimina un nodo intermedio
*pValue = (*(*pValue)).pNext
end
delete pTemp
iN = iN-1
end
function Size() : Integer
return iN
end
end
\end{lstlisting}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Doblemente Enlazada} \label{lb:dobleenlazada}
La idea central de una lista doblemente enlazada es mantener una relación de conectividad de cada elemento de la lista con el elemento próximo y anterior. Entonces, es posible recorrer la lista en ambos sentidos: desde el inicio al final, y del final al inicio. Para ello se requiere de una marca de finalización para cada recorrido. Para dicha marca se emplea una función denominada End que sirve como parada cuando se recorre desde el inicio al final, y otra función llamada Start que es cuando se recorre en sentido inverso. Del mismo modo, se permiten las operaciones de PreInsert y PostInsert debido a que será posible insertar antes y después de una posición dada.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{Especificación}
La especificación de las operaciones de una lista doblemente enlazada general se puede definir como una clase de la siguiente forma:
\begin{lstlisting}[upquote=true, language=pseudo]
class ListDouble <T>
public:
Type <...> tPosition // tipo de dato posición para desplazarse (no se muestra la implementación)
Constructor ListDouble () // construye la lista vacía ()
Constructor ListDouble (ref List <T> lSource) // constructor de copia
Destructor ListDouble () // destructor de la clase. Libera lavlc lista de memoria
function IsEmpty() : Boolean // retorna True si la lista es vacía, y false en caso contrario
function Start() : tPosition // retorna la posición del inicio de la lista (previo al primero)
function First() : tPosition // retorna la posición del 1er elemento de la lista
function End() : tPosition // retorna la posición final de la lista (después del último)
function Last() : tPosition // retorna la posición del último elemento de la lista
void Next(ref tPosition pValue) // Pre: pValue!= End()
void Prev(ref tPosition pValue) // Pre: pValue!= Start()
function Get (tPosition pValue): ref T// Retorna la referencia a la información contenida en pValue
void PreInsert (ref T x, tPosition pValue)// Pre: pValue!= Start().Inserta x antes de pValue.
void PostInsert (ref T x, tPosition pValue)// Pre: pValue!= End().Inserta x después de pValue.
void Delete (tPosition pValue) // Pre: pValue es válida. Elimina el elemento de posición pValue.
function Size() : Integer // retorna el número de elementos en la lista
end
\end{lstlisting}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{Ejemplos}
Una serie de ejemplos del uso de la especificación de la clase ListDouble.
- Imprimir los elementos de una lista en orden inverso
\begin{lstlisting}[upquote=true, language=pseudo]
void PrintList(ref ListDouble <Integer> L)
tPosition tIndex = L.Last()
while (tIndex != L.Start()) do
Print (*L.Get(tIndex))
L.Prev (ref tIndex)
end
end
\end{lstlisting}
En este algoritmo se comienza desde la posición del último elemento (Last) y se itera con la función Prev hasta llegar a Start. Es importante destacar que la condición de parada es Start y no First (First representa al 1er elemento de la lista).
- Eliminar los últimos 3 elementos de la lista de enteros
\begin{lstlisting}[upquote=true, language=pseudo]
void Delete3(ref ListDouble <Integer> L)
tPosition tIndex = L.Last(), tTemp
Integer iCont = 0
while (tIndex != L.Start() and iCont != 3) do
tTemp = tIndex
L.Prev(ref tTemp)
L.Delete(tIndex)
tIndex = tTemp
iCont = iCont + 1
end
end
\end{lstlisting}
Nuevamente, empezando desde la última posición y hasta llegar al inicio, se cuentan 3 elementos y son eliminados de la lista doblemente enlazada.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{Implementación}
De forma similar que las listas simplemente enlazadas, existen diversas implementaciones de las listas doblemente enlazadas: empleando arreglos y apuntadores. Un aspecto relevante es la posibilidad de particularizar la estructura de datos para resolver problemas específicos. Por ejemplo, buscar en un rango ordenado de valores de forma eficiente un valor; representar los lugares de una mesa redonda como una lista circular (caso particular de lista doblemente enlazada); o emplear múltiples apuntadores dentro de una lista para crear sub-listas (una lista de enteros donde se ordenen los números pares e impares como sublistas de ésta).
En la Fig. \ref{fig:listas2} se muestra una representación de una estructura ListDouble empleando apuntadores. En ella, existen dos apuntadores principales pFirst y pLast. Al mismo tiempo, en cada nodo existen 2 apuntadores que van a permitir el recorrido en ambos sentidos: pNext y pPrev.
\begin{figure}[!htb]
\centering
\includegraphics[scale=.7]{images/listas2.eps}
\caption{Representación visual de una estructura de lista simplemente enlazada empleando apuntadores.}
\label{fig:listas2}
\end{figure}
Es posible que una lista con enlaces no solo tenga una conexión con el nodo anterior y con el previo, sino que requiera conectarse con otro nodo de forma ``salteada" de acuerdo ala naturaleza del problema. Por ello, a continuación se muestra las listas multienlazadas o con múltiples enlaces.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Lista Multienlazada}
Una lista multienlazada o n-enlazada tiene como característica que sus nodos contienen enlaces que los asocian con más de una lista. Esto quiere decir que con una misma lista física, los diferentes apuntadores hacen que en realidad se enlacen $n$ listas lógicas o se manipulen de forma particular de acuerdo a dichos enlaces.
De este modo, las listas doblemente enlazadas son un caso particular de listas multienlazadas donde:
\begin{enumerate}
\item Cada nodo tiene 2 identificadores de tipo Pointer
\item Los apuntadores son exactamente inversos una de otro
\end{enumerate}
Así, en una lista multienlazada cada nodo puede tener cualquier cantidad de apuntadores a otros nodos que pueden formar diversas estructuras o no. Por ejemplo, dada una lista de personas enlazada entre sí de dos maneras:
\begin{enumerate}
\item Ordenada alfabéticamente por el primer apellido
\item Ordenada ascendentemente por su documento de identidad
\end{enumerate}
Es posible representar dicha información como dos listas, pero existe el problema de la doble representación de los nodos (nodos duplicados) tanto en su representación como en el desarrollo de sus operaciones. Entonces, las operaciones de Insert o Delete deben realizarse en ambas listas. Una solución es representar mediante el uso de apuntadores en una lista multienlazada estas dos listas lógicas en una sola lista física, definiendo sus nodos (ver Fig. \ref{fig:multifirst}).
\begin{figure}[htp!]
\begin{center}
\includegraphics[width=0.75\textwidth]{images/first.png}
\end{center}
\caption{Ejemplo de dos listas lógicas en una sola lista multienlazada.}
\label{fig:multifirst}
\end{figure}
El puntero $N$ y las flechas continuas representan la lista ordenada por nombre y el puntero $C$ y las flechas punteadas la lista ordenada por documento de identidad. También esta lista se puede representar con el uso de un nodo cabeza. Dicho nodo cabeza contedrá un puntero al nodo inicial que representa el comienzo de cada lista lógica, tal como se muestra en la Fig. \ref{fig:multisecond}.
\begin{figure}[htp!]
\begin{center}
\includegraphics[width=0.75\textwidth]{images/second.png}
\end{center}
\caption{Ejemplo de dos listas lógicas en una sola lista multienlazada con nodo cabeza L.}
\label{fig:multisecond}
\end{figure}
Las estructuras de datos de la lista multienlazada para el ejemplo anterior se pueden definir como:
\begin{lstlisting}[upquote=true, language=pseudo]
class CPerson
public:
String strName
Integer iDocIdentidad
end
class Node
public:
CPerson Info
Node* pNextN
Node* pNextC
end
\end{lstlisting}
En el caso de emplear dos apuntadores para cada lista, la definición queda como:
\begin{lstlisting}[upquote=true, language=pseudo]
class ListPerson
public:
Node* N //primer elemento de la lista de nombres
Node* C //primer elementos de la lista de documentos de identidad
end
\end{lstlisting}
Y en caso de emplear nodo cabeza, solo cambia la definición de ListPerson y se agrega la estructura NodeHead.
\begin{lstlisting}[upquote=true, language=pseudo]
class NodeHead
public:
Node* pFirstN
Node* pFirstC
end
class ListPerson
public:
NodeHead* pHead //nodo cabeza de la lista
end
\end{lstlisting}
Las operaciones cambian para adaptarse a los nuevos características de lista, básicamente mantener el orden entre sus elementos. A continuación se muestra como ejemplo el algoritmo de inserción, que va en las operaciones de la clase ListPerson.
\begin{lstlisting}[upquote=true, language=pseudo]
void Insert (CPerson pers) //inserta una persona en la lista manteniendo el orden
Node* pNew, pPrev, pThis
pNew = new Node ()
*pNew.Info = pers
*pNew.pNextN = *pNew.pNextC = NIL
if (*pHead.pFirstN == NIL) then
*pHead.pFirstN = pNew
*pHead.pFirstC = pNew
else
//buscar donde va en la lista por nombre
pThis = *pHead.pFirstN
pPrev = NIL
// isLessThan compara lexicográficamente dos strings
while (pThis != NIL and isLessThan(*pThis.Info.strName, pers.strName)) do
pPrev = pThis
pThis = *pThis.pNextN
end
*pNew.pNextN = pThis
if pPrev == NIL then
*pHead.pFirstN = pNew
else
*pPrev.pNextN = pNew
end
//buscar donde va en la lista por cedula
pThis = *pHead.pFirstC
pPrev = NIL
while (pThis != NIL and *pThis.Info.iCedula < pers.iDocIdentidad) do
pPrev = pThis
pThis = *pThis.pNextC
end
*pNew.pNextC = pThis
if pPrev == NIL then
*pHead.pFirstC = pNew
else
*pPrev.pNextC = pNew
end
end
end
\end{lstlisting}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Tipo Sparse Matrix} \label{sec:sparse}
Una estructura Sparse Matrix, o matriz esparcida o matriz dispersa es una combinación de estructuras que se utiliza para representar solamente los elementos que son significativos en una matriz estática “grande”, buscando lograr una menor complejidad en memoria. Esta estructura deriva del análisis numérico donde una Sparse Matrix es una matriz la cual la mayoría de sus elementos son cero (inverso a matriz densa). La estructuta de matriz esparcida es por naturaleza más fácil de comprimir sus datos y requiere del uso de algoritmos que difieren de los clásicos empleados en matrices densas.
Para analizar un poco la ventaja en complejidad en espacio que puede representar una matriz esparcida se muestra un ejemplo: Dada una matriz de $50 \times 50$ elementos y solo son significativos $100$ de ellos (no nulos, diferentes a cero, con valor true, etc., dependiendo de la clase de información que se guarde), existe un desperdicio de memoria significativo.
Calculando la complejidad en memoria de una matriz de enteros estática de tamaño $50 \times 50$.
\begin{lstlisting}[upquote=true, language=pseudo]
Array aMatrix of Integer [1..50][1..50]
CM(aMatrix) = (50 - 1 + 1) * (50 - 1 + 1) * CM(Integer) = 2500 palabras
\end{lstlisting}
En cambio empleando una representación de tipo matriz esparcida, se define como se muestra en la Fig. \ref{fig:sparse}.
\begin{figure}[htp!]
\begin{center}
\includegraphics[width=0.85\textwidth]{images/sparse.png}
\end{center}
\caption{Ejemplo de representación de una matriz de $50 \times 50$ en una matriz esparcida.}
\label{fig:sparse}
\end{figure}
Ahora, cada elemento de la lista (Node) está formado por dos identificadores del tipo Pointer y dos de tipo Integer, que indican la fila y columna asociada. Además, el arreglo solo contiene un apuntador al tipo Node, el cálculo de su complejidad queda como:
\begin{lstlisting}[upquote=true, language=pseudo]
CM(aMatrixSparse) = CM(Row) + CM(Col) + 100 * CM(Node)
CM(aMatrixSparse) = (2 * (50-1+1) * CM(Node*)) + 100 * (2*CM(Integer) + 2*CM(Node*))
CM(aMatrixSparse) = (2 * 50 * 1) + 100 * (2 + 2) = 500 palabras
\end{lstlisting}
De esta forma se tiene un ahorro en memoria respecto a la representación completa de una matriz $50 \times 50$ de:
\begin{lstlisting}[upquote=true, language=pseudo]
CM(aMatrix) - CM(aMatrixSparse) = (2500 - 500) palabras = 2000 palabras.
\end{lstlisting}
Esto indica que aMatrixSparse es solo una quinta parte de aMatrix en espacio en memoria.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{Implementación}
La forma clásica de implementar una estructura Sparse Matrix es empleando apuntadores para la definición de los elementos que la conformará. A continuación se muestra una posible representación implementada con apuntadores.
\begin{lstlisting}[upquote=true, language=pseudo]
class Node <T>
public:
T tInfo
Integer iIdF, iIdC
Node<T>* pNextR, pNextC
end
class NodeR <T> // encabezados de las filas de la matriz
public:
Integer iIdR
T tInfo
NodeR <T>* pNext
Node <T>* pFirst
end
class NodeC <T> // encabezados de las columnas de la matriz
public:
Integer iIdC
NodeC <T>* pNext
Node <T>* pFirst
end
class HeadNodeM <T>
public:
NodeR <T>* pFirstR
NodeC <T>* pFirstC
end
class SparseMatrix <T>
public:
HeadNodeM <T>* Matrix
function isElementOf (Integer iI, iJ) : Boolean
void Insert (T tInfo, Integer iI, iJ)
void Delete (Integer iI, iJ)
end
\end{lstlisting}
Nótese que las listas dentro de una matriz esparcida también se pueden usar doblemente enlazadas incorporando dos apuntadores a cada tipo Node: uno para el previo por fila y otro para el previo por columna (o en las listas de nodos cabeza (i.e. Node* pPrevR, pPrevC).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{Ventajas y Desventajas}
Con una matriz esparcida se ahorra espacio en memoria cuando el número de nodos es muy pequeño respecto al número de elementos totales de la matriz. Se suele considerar un ahorro de memoria razonable cuando la cantidad de elementos significativos es un porcentaje de la cantidad total de nodos posibles (un aproximado que varía y puede llegar hasta un $20\%$ del total). Adicionalmente, con una representación basada en apuntadores es posible agregar/eliminar filas/columnas y nodos de forma sencilla.
En contraparte, cuando existen pocos elementos "no nulos" una solución basada en matriz esparcida puede no ser la más conveniente ya que ocupará más memoria que su contraparte estática a medida que el número de nodos sea mayor. En ese caso, se debe utilizar otra implementación que siga siendo dinámica pero que ocupe menos espacio en memoria (i.e. lista de adyacencia).
Por otro lado en implementaciones de matrices esparcidas, las operaciones de insertar y eliminar tienen un mayor nivel dificultad en su implementación y en su mayoría de complejidad $O(N)$ debido a las búsquedas lineales en una o varias listas.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Tipo Stack}
Es una estructura de datos representado como un conjunto de elementos ordenados, donde el acceso a cada uno de sus elementos es en orden inverso a como se han agregado. El tipo Stack, sigue el esquema LIFO (\textit{last in first out}) donde el último en entrar (ser insertado en la estructura) será el primero en salir o ser tratado para almacenar y recuperar datos. Hay múltiples ámbitos donde se utilizan este tipo pila como estructura de datos debido su simplicidad y orden implícito. (e.g. la pila de ambientes de programas, simulación de una pila de libros o una mano de cartas, almacenar expresiones en notación polaca, etc.). En todo momento, solamente se tiene acceso a la parte superior de la pila (tope). Para el manejo de los datos, las operaciones actúan sobre el tope de la pila: colocar un objeto (push/apilar) y su operación inversa, retirar el último elemento apilado (pop/desapilar).
La Fig. \ref{fig:stack1} se observa de forma gráfica un tipo Stack. Se puede ver que existe un tope y una base, donde los elementos entran a la pila por el tope y el primer elemento insertado siempre formará la base.
\begin{figure}[htp!]
\begin{center}
\includegraphics[width=0.35\textwidth]{images/stack.eps}
\end{center}
\caption{Representación gráfica del tipo Stack.}
\label{fig:stack1}
\end{figure}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Especificación}
Dada la naturaleza del tipo Stack, la especificación de la clase resulta simple en cuanto a las funciones que emplea. A continuación se describe:
\begin{lstlisting}[upquote=true, language=pseudo]
class Stack <T>
public:
Constructor Stack() //construye la pila vacía.
Destructor Stack() //destruye la pila.
function IsEmpty() : Boolean //indica si la pila está vacía o no
void Push(ref T x) //agrega un nuevo elemento al tope de la pila. Ssegura que Top()==x
void Pop() //precondición: la pila no puede estar vacía. Elimina el elemento del tope
function Size() : Integer //devuelve el \# de elementos en la pila
function Top() : T //precondición: la pila no puede estar vacía y retorna la información del tope
end
\end{lstlisting}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Implementación}
\paragraph{Implementación reutilizando la clase List}
Empleando el mecanismo de herencia, se puede implementar una pila basado en superclase lista aplicando todas las operaciones definidas en su especificación. Se emplea la herencia simple, donde todos los miembros públicos de la clase List serán privados dentro de la subclase Stack.
\begin{lstlisting}[upquote=true, language=pseudo]
class Stack <T> inherited List <T>
public:
Constructor Stack() //construye la pila vacía. El const. de la superclase es invocada implícitamente
//se puede invocar a this.List() pero no es necesario
end
Destructor Stack () //se destruye la pila. El destructor de la superclase es invocada implícitamente
end
function IsEmpty() : Boolean //la pila está vacía si la lista (superclase) lo está
return List<T>::IsEmpty()
end
void Push (ref T x) // se asume que el tope es el primero de la lista
Insert(x, First())
end
void Pop() // dado que el tope es el primero de la lista, se elimina el primero
Delete (First())
end
function Size() : Integer //el tamaño de la pila se reduce a saber el tamaño de la lista
return List<T>::Size()
end
function Top() : T //retorna el elemento del tope, es decir, el 1ro de la lista
return Get (*First())
end
end
\end{lstlisting}
\paragraph{Implementación con apuntadores}
En esta caso, se realiza la implementación directamente con apuntadores al tipo Node siguiendo la especificación ya definida.
\begin{lstlisting}[upquote=true, language=pseudo]
class Node <T>
public:
T tInfo
Node<T>* pNext
end
class Stack <T>
private:
Node<T>* pTop
Integer iN
public:
Constructor Stack()
iN = 0
pTop = NIL
end
Destructor Stack ()
while pTop != NIL do
Node<T>* pTemp = *pTop
pTop = *pTop.pNext
delete pTemp
end
end
function IsEmpty() : Boolean
return iN == 0
end
void Push (ref T x)
Node<T>* pNew = new Node
*pNew.x = *x
*pNew.pNext = pTop
pTop = pNew
iN = iN + 1
end
void Pop()
Node<T>* pTemp = pTop
pTop = *pTop.pNext
delete pTemp
iN = iN - 1
end
function Size() : Integer
return iN
end
function Top() : T
return *pTop.tInfo
end
end
\end{lstlisting}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Algoritmos}
A continuación una serie de algoritmos sencillos para demostrar el uso del tipo Stack.
\subsubsection{Paréntesis}
El problema de la paréntesis consiste en verificar que una secuencia de paréntesis sea correcta en al abrir-cerrar, es decir, se consideran correctos (()), [{}], {()[]} dado que para símbolo de paréntesis de apertura siempre tendrá su equivalente de cierre (basado en agrupamiento). Se consideran incorrectos los siguientes por ejemplo (()(, [[)]] ([)] entre otros.
\begin{lstlisting}[upquote=true, language=pseudo]
function Parenthesis(String strText, Integer iN) : Boolean
Stack<Char> S
for Integer iK = 1 to iN do
if strText[ik] == '(' or strText[ik] == '[' or strText[ik] == '{' then
S.Push(strText[ik])
elseif S.IsEmpty() then
return false
else //verificar o no que sea ')', ']' o '}'
S.Pop()
end
end
return S.IsEmpty()
end
String strText = "{()()[]}"
if Parenthesis(strText, 8) then
Print("Correcto")
end
\end{lstlisting}
La estructura de la pila garantiza que si no está vacía al final del algoritmo entonces no están colocados los paréntesis correctamente. Del mismo modo, si la pila llega a estar vacía antes de finalizar, entonces tampoco es incorrecto también.
\subsubsection{Notación PostFija}
La notación postfija o notación polaca inversa (\textit{Reverse Polish Notation} - RPN) es un método algebraico alternativo de representación de datos de entrada. En la RPN cada operador está antes de sus operandos, por ejemplo 3 4 + representa 3 + 4.
\begin{lstlisting}[upquote=true, language=pseudo]
function IsDigit(Char cValue) : Boolean
return (cValue >= '0' and cValue <= '9')
end
function Char2Int(Char cValue) : Integer
return cValue - '0'
end
function Compute(Integer iOp1, iOp2, Char cOp) : Integer
if cOp == '+' then
return iOp1 + iOp2
elseif cOp == '-' then
return iOp1 - iOp2
elseif cOp == '*' then
return iOp1 * iOp2
elseif cOp == '/' then
return iOp1 / iOp2 //iOp2 must be != 0
end
function RPN(String strExp, Integer iN) : Integer //Reverse Polish Notation
Integer iOp1, iOp2
Stack<Integer> S
for Integer iK = 1 to iN do
if IsDigit(strExp[iK]) then
S.Push(Char2Int(strExp[iK]))
else
iOp1 = S.Top()
S.Pop()
iOp2 = S.Top()
S.Pop()
S.Push(Compute(iOp1, iOp2, strExp[iK]))
end
end
return S.Top()
end
Print (RPN("59+2*", 5)) //output: 28
\end{lstlisting}
\subsubsection{Invertir Pila}
Utilizando únicamente las primitivas de la clase Stack, la idea es invertirla \textbf{sin utilizar} estructuras auxiliares. Este problema se basa en el hecho de aprovechar que las invocaciones recursivas se comportan como una pila de estados y permiten guardar en cada estado, una serie de variables.
\begin{lstlisting}[upquote=true, language=pseudo]
void InsertEnd(ref Stack<Integer> S, Integer iValue)
if S.IsEmpty() then
S.Push(iValue)
else
Integer iTop = S.Top()
S.Pop()
InsertEnd(ref S, iValue)
S.Push(iTop)
end
end
void Reverse(ref Stack<Integer> S)
if not S.IsEmpty() then
Integer iTop = S.Top()
S.Pop()
Reverse(ref S)
InsertEnd(ref S, iTop)
end
end
void PrintStack(Stack S)
while not S.IsEmpty() do
Print (S.Top())
S.Pop()
end
end
Stack<Integer> sValues
fillWithSomeValues (ref sValues) //asignarle valores
PrintStack(sValues) //i.e. 8 6 3 5 2 7 9
Reverse(ref sValues)
PrintStack(sValues) //i.e. 9 7 2 5 3 6 8
\end{lstlisting}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Recursión} \label{sec:recstack}
Es posible emplear una estructura de pila para simular los ambientes de programas recursivos (recursión simple/ múltiple/ anidada/ indirecta). La idea consiste en apilar los resultados obtenidos en un ambiente, para luego combinarlos (extracción del tope) con una operación actual en cierto estado recursivo. De esta forma, es posible simular un algoritmo recursivo de forma iterativa empleando una pila para almacenar los resultados de cada ambiente “recursivo”.
Veamos un ejemplo simple con la función Factorial:
\textbf{Versión Iterativa}
Su versión clásica iterativa sirve como guía.