100% encontró este documento útil (5 votos)
3K vistas615 páginas

Estructura de Datos

Estructura de Datos y algoritmos en java - Adam Drozdek

Cargado por

Zafenat Paneah
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF o lee en línea desde Scribd
100% encontró este documento útil (5 votos)
3K vistas615 páginas

Estructura de Datos

Estructura de Datos y algoritmos en java - Adam Drozdek

Cargado por

Zafenat Paneah
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF o lee en línea desde Scribd
Está en la página 1/ 615
5 § #2 3 3 S 3 2 5 rs THOMSON ———- Estructuras de datos y algoritmos con Java, 28. edicién Presidente de Thomson Learning oeroamérica: avie Arollano Gutiérrez Director editorial Iberoamérica: Jose Tomas Pérez Bonia Gerente editorial y de produccién: Ula Moreno Olvera COPYRIGHT © 2007 por international ‘Thomson Eatores, SA. de CV, una > (desplazamiento a la derecha), >>> (desplazamiento a la derecha rellenando con ceros), y ~ (complemento a). Las abreviaturas &=, |=, <<=, >=, y >>>= también son posibles. Excepto por el operador >>>, el resto de los operadores también se incluyen en C/C++. El operador >> desplaza hacia la derecha un ntimero especifico de bits del extremo derecho (menos significativos) y desplaza hacia adentro el mismo nimero de ceros (0) para los ntimeros positivos y de unos (1) para los niimeros negativos. Por ejemplo, el valor de m después de las asignaciones int n= int m= n >> 1; Seccién 1.1 Java rudimentario M5 5 -2 debido a que ~4 en n es una representacién del complemento a dos como la secuencia de 32 bits 11 . . . 1100, la cual después de desplazar un bit a la derecha da el patrn 11... 1110, en m, el cual es una representacién del complemento a dos de ~2. Para hacer que los 0 también se desplacen hacia adentro para los niimeros negativos, debe utilizarse el operador >>> , int n= —4; int m= n >>> 1; en cuyo caso, el patrén 11, .. 1100 en n se transforma en el patron 01... 1110 en m, que ¢s el nimero 2147483646 (uno menos que el valor maximo para un entero). 1.1.3 Instrucciones de decision ‘Una instruccién de decision es una instruccion if if (condiciony hacer algo; hacer algo mds;) cn la cual la palabra 4¢ va seguida por una condicion encerrada entre paréntesis, luego por el cuerpo de la clausula if, que es un bloque de instrucciones, y después por una cldusula else opcional, la cual es es la palabra e1se seguida por un bloque de instruc- ciones. Una condicién debe regresar un valor booleano (en C/C++, puede regresar cual- 4quier valor). Una condicién se forma con los operadores relacionales <, <=, ==,!=, >=, > que toman dos argumentos y regresan un valor booleano, y con operadores logicas que toman uno (!) 0 dos (&&, ) argumentos booleanos y regresan un valor boolean. Una alternativa a una instruccién if-else es el operador condicional de la forma condicién ? hacer-algo-si-verdadero + hacer-algo-si-falso; El operador condicional regresa un valor, mientras que una instruccién 4£ no lo hace, asi el anterior puede utilizarse, por ejemplo, en asignaciones tales como neice 0? 101 20; ‘Otra instruccién de decisién es una instruccién switch, la cual es una abreviatara para instrucciones i anidadas. Su forma es la siguiente: switch (expresinentera) { ease valorl: bloquel; break; case valorN: bloqueN; break; default: bloque predeterminado; + La expresion de prueba que va después de switch debe ser una expresién entera de modo que cualquier expresion del tipo byte, char, short ¢ int pueda utilizarse. El valor de la expresin se compara con los valores que siguen a la palabra case. Si se en~ cuentra una coincidencia, el bloque de instrucciones que va después de este valor se ¢jecuta, y al encontrar break, se sale de la instruccin switch. Observe que si falta la Capitulo 1 Programaci6n orientada a objetos utilizando Java palabra break, entonces la ejecucién contimia para el bloque de la siguiente cléusula case. Después de ejecutar la instruccién switch (i) { case 5: x + 10; case 6 : x = 20; case 7: x *= 2; break; default : x = 30; y el valor de x ¢5 10 si i ¢s igual a 5, 40 si 4 es igual a 6, se duplica si { es igual a7 y es 30 para cualquier otro valor de i. 1.1.4 Ciclos (loops) El primer ciclo disponible en Java es el ciclo while: while (condicién) hhacer algo; La condicién debe ser una expresién booleana. El segundo ciclo es un ciclo do-whil do hacer algo; while (condicién); El ciclo continia hasta que la con El tercer ciclo es el ciclo £ tor (inicializacién; condicién; incremento) hacer algo; La parte de inicializacién también puede declarar variables y estas variables existen slo durante la ejecucién del ciclo. Es posible salir de un ciclo antes de que todas las instrucciones en su cuerpo se eje- cuten con una instruccién break sin etiqueta. Ya hemos visto una instruccién break utilizada en la instrucci6n switeh. En el caso de los ciclos anidados, cuando se encuen- tra una instruccién break, se sale del ciclo actual de modo que el ciclo exterior pueda continuarse, Una instruccién continue sin etiqueta provoca que el ciclo omita el resto del cuerpo y comience con la iteracién siguiente. 1.1.5 Manejo de excepcién Si se detecta un error durante la ejecucién de un programa Java, Java formula una excepcién, después de la cual el programa finaliza y se despliega un mensaje de error informando al usuario cual excepcién se formulé (es decir, que tipo de error ocurrié y cen qué parte del programa). No obstante, si llega a ocurrir un error el usuario puede mangjarlo en el programa, cuando menos puede hacer que el programa ignore el error de modo que contintie la ejecucién del programa. Pero si se despliega una excepcién, se Seccién 1.1 Javarudimentario @ 7 puede tomar una medida especial y entonces el programa puede continuar. Es posible descubrir un error al utilizar un mecanismo try-catch. ‘Un formato general de la instruccién try-catch es try { hacer algo; } catch (tipo-de-excepcién-nombre-de-excepcion) { hacer algo; ? El nimero de cldusulas catch no esté limitado a uno. Puede haber tantas como se re- La clausula catch imprime un mensaje, pero no realiza ninguna operacién de repara- ci6n en el arreglo a, aunque podria hacerlo. En este ejemplo, la clausula catch también incluye la instruccién throw, aun cuando esto no es muy comiin. De esta manera, la excepcién no sélo se descubre y maneja en £2( ), sino que ademés una llamada de £2 () se ve forzada a manejarla, como en el método £3( ): public void £3() { try { £205 ) catch (ArrayIndexOutofBoundsException e) { systen.out.print1n("Exception caught in £3()"); , 8 Mi Capitulo 1 Programacién orientada a objetos utilizando Java Sila Hamada de £2( no maneja la excepcién, el programa se colapsa, aun cuando la ex- cepcién se descubrié en £2). La misma suerte sucede a un programa si una llamada de £1() no descubre la excepcién: public void £4() { int{] a= (1,2,3,4,5}; for (int i= 0; i < a.length; itt) system.out.print(fl(a,i) + * "); , Observe que el comportamiento del programa en todos estos casos es el mismo (es decir, el manejo de una excepcién, el paso de la misma a otro método o el colapso del programa) sila cléusula throws no se incluye en el encabezado de £1() de modo que £1() podria ser simplemente: public int f1(int(] a, int ny ¢ return n * a(nt2]; } La cléusula throws es por lo tanto una seftal muy importante para el usuario de que un problema posible puede ocurrir cuando se lama a un método en particular. No todos los tipos de excepciones pueden ignorarse ya que la excepcién formulada por £1() ¢s ignorada por £4(). La mayor parte del tiempo, las excepciones deben manejarse en el programa, de lo contrario, el programa no se compila. Este, por ejem- plo, es el caso del ZOEXception arrojado por los métodos 1/0; por consiguiente, estos métodos generalmente se llaman dentro de las cliusulas try-catch. PROGRAMACION ORIENTADA A OBJETOS EN JAVA 1.2.1 Encapsulamiento La programacién orientada a objetos (OOP: Object-oriented programming) gira en torno al concepto de un objeto. Los objetos, sin embargo, se crean utilizando una defini- ci6n de clase. Una clase es una plantilla a partir de la cual se crean los objetos. Una clase es una pieza de software que incluye una especificacién de datos y funciones que operan en estos datos y posiblemente en los datos que pertenecen a otras instancias de clases. Las funciones definidas en una clase se llaman métodos y la variables utilizadas en una clase se llaman variables de alcance de clases (para distinguirlas de las variables locales para el método 0 los bloques), los campos de datos, o simplemente campos. Esta combinacién de los datos y las operaciones relacionadas se llama encapsulamiento de datos. Un objeto es tuna instancia de una clase, una entidad creada utilizando una definicién de clase, En contraposicién a las funciones en lenguajes que no estén orientados a objetos (QOL: non-object-oriented languajes), los objetos hacen la conexién entre los datos y Jos métodos mucho mas estrecha y mas significativa. En los lenguajes no orientados a objetos, las declaraciones de datos y las definiciones de funciones pueden intercalarse a Jo largo de todo el programa, y solo la documentacién del programa indica que hay una conexién entre ellas. En los OOL, una conexidn se establece justo al principio; de hecho, el programa se basa en esta conexi6n. Un objeto engloba datos y operaciones relaciona- dos y debido a que puede haber muchos objetos utilizados en el mismo programa, los objetos se comunican al intercambiar mensajes, revelindose entre si de esta manera tantos, o tan pocos, detalles sobre su estructura interna como se requiera para una Seccion 1.2 Programacién orientada a objetos en Java M9 comunicacién adecuada. La estructuracién de los programas en términos de los objetos 1nos permite lograr varios objetivos. Primero, este acoplamiento de datos y operaciones puede usarse mucho mejor en la modelacién de un fragmento del mundo, lo cual enfatiza especialmente la ingenieria de software. Como es l6gico, la OOP tiene sus origenes en la simulacién; esto es, en la modelacién de sucesos de la realidad. El primer OOL fue Simula; se desarrollé a finales de la década de 1960 en Noruega. Segundo, los objetos permiten encontrar errores de una manera mds facil debido a gue las operaciones se localizan dentro de los confines de sus objetos. Aun cuando pueden ocurrir efectos colaterales, son mis ficiles de trazar. ‘Tercero, los objetos nos permiten ocultar ciertos detalles de sus operaciones a otros objetos, de modo que estas operaciones tal vez no se vean afectadas de manera adversa por otros objetos. Esto se conoce como el principio de ocultacién de la informacién, En Jenguajes que no son orientados a objetos, este principio puede encontrarse hasta cierto punto en el caso de las variables locales, 0 como en Pascal, en las funciones y proce- dimientos locales, que slo pueden utilizarse y accederse por medio de la funcién que los define. Esto es, no obstante, una ocultacion muy leve o nula ocultaci6n. Algunas veces tal vez lleguemos a necesitar del uso (de nuevo, al igual que en Pascal) de una funcién f2 definida en fl fuera de fl, pero no podemos hacerlo. En ocasiones se puede necesitar tener acceso a algunos datos locales para fl sin conocer la estructura exacta de esos datos, pero en los lenguajes de programacidn no orientados a objetos, es imposible hacerlo. Por con- siguiente se requiere cierta modificacién y esto se logra en los OL. Un objeto en OL ¢s como un reloj. Como usuarios estamos interesados en lo que muestran las manecillas, pero no en el funcionamiento interno del reloj. Estamos cons- cientes de que existen engranajes y resortes dentro del reloj en su interior, pero por lo general sabemos muy poco sobre por qué todas estas partes tienen una configuracién en particular. Para evitar datios voluntarios o involuntarios, no se tiene acceso a este meca- nismo. Debido a ello, no debemos tener acceso a este mecanismo de modo que no lo dafemos, sin querer 0 a propésito. Por lo tanto, este mecanismo nos esté oculto, no te- nemos acceso inmediato a él, y por consiguiente el reloj esta protegido y funciona mejor que cuando el mecanismo esta abierto para que todos lo vean. Por consiguiente, un objeto es como una caja negra cuyo comportamiento esté muy bien definido; y utilizamos el objeto debido a que sabemos lo que hace, no porque tenga- ‘mos una idea de cémo lo hace. Esta opacidad de los objetos es muy itil para mantenerlos independientes unos de otros. Silos canales de comunicacién entre los objetos estén bien definidos entonces los cambios hechos dentro de un objeto pueden afectar a otros objetos s6lo en la medida que estos cambios afecten a los canales de comunicacién. Al conocer el tipo de informacién enviado y recibido por un objeto, éste puede reemplazarse de una manera més facil por otro objeto mas adecuado en una situacién en particular: un ob- jeto nuevo puede realizar la misma tarea de manera distinta pero mis répidamente o en cierto entorno de hardware. Por consiguiente, un objeto revela sélo lo que el usuario necesita utilizar. Tiene una parte publica a la que puede acceder cualquier usuario cuan- do uno de ellos envia un mensaje que coincide con cualquier nombre de método revelado por el objeto. En esta parte pablica, el objeto muestra al usuario botones que pueden pre- sionarse para invocar las operaciones del objeto. El usuario sélo conoce los nombres de estas operaciones y el comportamiento esperado. El ocultamiento de informacién tiende a desdibujar Ja linea divisoria entre los datos y las operaciones. En los lenguajes tipo Pascal, la distincién entre los datos y las fun- ciones/procedimientos es clara y rigida. Estin definidos de una manera distinta en sus funciones y son muy diferentes. Los OL ponen los datos y los métodos juntos, y para el usuario del objeto esta distincién es mucho menos perceptible. Hasta cierto punto, ésta es tuna incorporacién de las funciones de los lenguajes funcionales. LISP, uno de los primeros lenguajes de programacién, permite al usuario tratar a los datos y a las fun- iones en el mismo nivel, debido a que la estructura de ambos es la misma, 10 M Capitulo 1 Programacidn orientada a objetos utilizando Java Hemos hecho una distincién entre objetos particulares y tipos de objetos o clases. Escribimos métodos para utilizarlos con distintas variables y, por analogia, no quere- ‘mos vernos obligados a escribir tantas declaraciones de objetos como objetos requeri- dos por el programa, Ciertos objetos son del mismo tipo y nos gustaria utilizar s6lo una referencia a una especificacin de objetos en general. Para las variables individuales, queremos una distincién entre la declaracién de tipo y la declaracién de variable. En el «caso de los objetos, se hace una distincién entre una declaracién de clase y su instan- public void method2(int i, String s) { dataPield2 = System.out.printin(i + * received from * + s); > private String dataFieldi; private int dataField2; private double dataField3; public static void main (String args{]) { € object = new C(*object1*,100,2000), object? = new C("object2"), object3 = new C(); object1.method2 (123); object 1.methodl(); object2.method2 (123, "object2"); Seccién 1.2 Programacién osientada a objetos en Java MIT El programa contiene una declaracién de la clase . Dentro del método main( ), los objetos de la clase tipo c se generan al declararlos e instanciarlos: © object1 = new C("object1",100,2000), object2 = new C("object2"), object3 = new C(); Es muy importante ver que las declaraciones de objetos no crean objetos, asi que las dos lineas © objecti; object methodl (); dan como resultado un error de compilacién. La variable de objeto object debe asignarse a un objeto, lo cual puede realizarse directamente en la declaracién al ini- public void h(String ) { Systen.out.println(*Método h() en DerivediLeveli llamado desde “ + 8); ? void kiString 8) ¢ System.out-printin(*Método k() en DerivediLevell llamado desde * + 8); , > class Derived2ievell extends BaseClass { public void £(string 5) ¢ System.out.printin ("Método £() en Derived2Levell llamade desde * + 5); “ h("Derived2Levell"); // h() tiene acceso privado en basePackag: “ uw uw " uw u 7 SecciOn 1.2 Programacién orientada a objetos en Java M19 k(*Derived2uevell*); // k() no es piblico en basePackage.BaseClass; // no se puede tener acceso a 61 desde el paquete exterior. » protected void g(String s) { Systen.out.printLn( "Método g() en Derived2Levell llamado desde " + 8); Derivedtevel2 extends Deriveditevell ( public void #(String 6) { Systen.out.printin( “Método £() en Derivedtevel2 Llanado desde * + 8); 9(*Derivedtevei2") h("Derivedtevel2*); k("Derivedtevel2"); super. £(*Derivedtevel2"); ‘Testinheritance { void run() { BaseClass be = new BaseClass(); Derivediteveli dill = new Deriveditevell(); Derivedatevell d211 = new Derived2Levell(); DerivedLevel2 di2 = new DerivedLevel2(); be. £(*main(1)")3 be.g(*main(2)"); // g{) ha protegido el acceso en basePackage.BaseClass. be-h("main(3)")}; // h{) tiene acceso privado en basePackage.BaseClai be-k(*main(4)*); // k() n0 @& piblico en basePackage.BaseClas // 20 puede accederse desde el paquete exterior. A2Ld£¢omain(s)*)s d111.g("main(6)"); // 9() tiene acceso protegide en basePacka: A2L1-h(*main(7)*) 5 111 k¢mmain(s)"); d211.£("main(9)* d221.g(*main(10)*); d211.b("main(11)*); // h() tiene acceso privado en basePackage-BaseClass. d12.£("main(12)*); d12.g("main(13)"); // g{) tiene acceso protegide en basePacka: d12.h(*main(14)*) 5 Basecl Baseci ? public static void main(String args{}) ¢ (new Testinheritance()).2un(); 20 WM Capitulo 1 Programacion orientada a objetos utilizando Java La ejecucién de este cédigo genera la salida siguiente: Método £() en Liamado desde main(1) Método h() en. llamado desde BaseClass Método £() en DerivediLevell llamado desde main(5) Método g() en BaseClass llamado desde Deriveditevell Método h() en DerivediLevell llamado desde main(7) Método k() en DerivediLevell llamado desde main(8) Método £() en Derived2Level! llamado desde main(9) Método g() en Derived2teveli llamado desde main(10) Método £() en Derivedtevel2 llamado desde main(12) Método g() en BaseClass llamado desde Derivedtevel2 Método h() en DerivediLevell llamado desde DerivedLevel2 Método k() en DerivediLeveli llamado desde DerivedLevel2 Método £() en DerivedtLeveli llamado desde Derivedtevel2 Método g() en BaseClass llamado desde DerivediLevell Método h() en DerivediLevell llamado desde main(14) La clase Baseclass se llama clase base o superclase, y otras clases se llaman subclases co clases derivadas debido a que se derivan de la superclase en la cual pueden usar los cam- pos de datos y los métodos especificados en BaseClass como protected, public, 0 cuando las subclases estén en el mismo paquete que la clase base, no tienen modificador de acceso, Heredan todos estos campos y métodos de su clase base de modo que no tienen que repetir las mismas definiciones. Sin embargo, una clase derivada puede anular la definicién de un método que no es final al introducir su propia definicién, De esta manera, tanto la clase base como la clase derivada tienen alguna medida de control sobre ‘sus métodos. La clase base puede decidir cudles métodos y campos de datos pueden revelarse a las clases derivadas, de modo que el principio de ocultacién de informacidn se sostiene no slo con respecto al usuario de la clase, sino tambien a las clases derivadas, Ademés, la clase de- rivada puede decidir cuales métodas y campos de datos publicos y protegidos conservar y utiliza, y cudles modifica Por ejemplo, tanto DerivediLevel y Derived2ievel re- definen al método £() al dar sus propias versiones de £ (). Sin embargo, el acceso al mé- todo con el mismo nombre en la clase padre atin es posible al preceder el nombre del método con la palabra clave super, como se muestra en la llamada de super .£() desde £() en DerivedLevel2. Una clase derivada puede afiadir nuevos métodos y campos propios. Este tipo de clase puede volverse una clase base para otras clases que pueden derivarse de la misma manera que la jerarquia de herencia puede ampliarse deliberadamente. Por ejemplo, la clase DerivediLevelt se deriva de BaseClass, pero al mismo tiempo, s la clase base para DerivedLevel2. Los métodos o campos protegidos de la clase base estin accesibles para las clases derivadas. También estan accesibles para las clases no derivadas si estas clases se encuen- tran dentro del mismo paquete que la clase que define los métodos y campos protegi- dos. Por esta razén, DerivediLeveli puede llamar al método protegido a() de BaseCiass, pero una llamada a este método desde run() en TestInheritance se interpreta como ilegal. No obstante, run() puede lamar al método 9(), declarado como protegido en Derived2Levell, debido a que tanto Derived2Levell como TestInheritance en las que run( ) se define, estin en el mismo paquete. Seccién 1.2 Programacién orientada a objetos en Java M21 Los métodos y los campos de datos sin modificador de acceso pueden accederse por cualquier clase del mismo paquete. Por ejemplo, el método k( ) en BaseClass no puede accederse incluso en una clase derivada, Derived2beveli, debido a que la clase derivada esté en un paquete distinto, Pero el método k() en DerivediLevel1 puede accederse aun cuando esté en una clase no derivada, como Test Inheritance en run{ ), ya que tanto DerivedlLevel1 como Test Inheritance se encuentran en el mismo paquete. A diferencia de C++, el cual soporta herencia multiple, la herencia en Java debe limi- tarse a una clase solamente, de modo que no ¢s posible declarar una clase nueva con la declaracién class Derived2tevel2 extends Derivediteveli, Derived2Levell { ... } Adems, una clase declarada £inal no puede ampliarse (las clases de envoltura son ejemplos de las clases final). 1.2.4 Polimorfismo El polimorfismo se refiere a la capacidad de adquirir muchas formas. En el contexto de 1a OOP, esto significa que el mismo método denota muchos métodas que son miembros de diferentes abjetos, Esto se logra por medio del conocido enlace dindmico, cuando el tipo de un método que se va a ejecutar puede retardarse hasta el tiempo de ejecucion. Esto se distingue del enlace estatico, cuando el tipo de respuesta se determina en el tiempo de compilacién, como en el caso de IntOb ject y Doubleob ject presentados en la sec- ci6n 1.2.1, Estos dos objetos se declaran como objetos cuyos campos de almacenamiento guardan datos del tipo object y no enteros 0 dobles. La conversion se realiza dinémica- ‘mente, pero fuera del objeto mismo. Para el enlace dindmico, considere las declaraciones siguientes: cla: AG public void process() { System.out.printin( "Dentro de A"); > ) class Exta extends A { public void process() { System.out.printin(*Dentro de ExtA"); > ) entonces el ebdigo A object = new A(); object..process(); object = new ExtA(); object .process(); da como resultado la salida Dentro de A Dentro de Exta 22 = W Capitulo 1 Programacién orientada a objetos utilizando Java Esto se debe al enlace dindmico: el sistema verifica dindmicamente el tipo de objeto al cual se esté refiriendo actualmente la variable y elige el método apropiado para este tipo. Por lo tanto, aun cuando la variable object se declara para ser del tipo A, se asigna en la segunda asignacién de un objeto del tipo BxtA y ejecuta el método process), el ccual se define en la clase ExtA, en vez del método con el mismo nombre definido en la clase a, Esto también es vilido para las interfaces. Por ejemplo, si las declaraciones interface B { void process(); i class Imp1B1 implements B { public void process() { system.out.println(*Dentro de Imp1B1*); } » class ImplB2 implements B { public void process() { System.out.printin(*Dentro de Imp1B2"); y y vvan seguidas por las instrucciones B object = new ImplB1(); object .process(); object = new ImplB2(); object .process(); entonces la salida es Dentro de Imp1B1 Dentro de Imp1B2 a pesar del hecho de que el objeto es del tipo 8. El sistema reconoce que, para la primera llamada de process (), object se refiere a un objeto del tipo tmp131, yen la segunda llamada, se refiere a un objeto del tipo Imp1B2. El polimorfismo es por tanto una herramienta poderosa en la OOP. Es suficiente para enviar un mensaje estindar a muchos objetos distintos sin especificar como se seguiré el mensaje. No hay necesidad de saber qué tipo de abjetos son. El receptor es responsable de interpretar el mensaje y seguirlo. El emisor no tiene que modificar el mensaje dependiendo de! tipo de receptor. No hay necesidad de instrucciones switch 0 if-else. Ademés, se pueden afiadir unidades nuevas a un programa complejo sin la necesidad de volver a compilar al programa entero, El enlace dindmico permite delegar la definicién de Genclass. Suponga que la definicin de esta clase también incluye un método para encontrar una posicion de una pieza de informacién en particular. Sila informacidn no se encuentra, se regresa a1. La definicién ahora es Seccidn 1.2 Programacién orientada a objetos en Java M23 cla GenClass { Object(] storage = new Object {50}; int find(Object el) ¢ for (int i= 0; i < 50; it+) if (storage[i) != null 6 storage{i}.equals(el)) return i; return -1; , void store(object el) { El método £ind() devuelve el resultado correcto si se usan las envolturas de los tipos bisicos —character(), Integer ( ), et.— pero, zqué ocurre si queremos almace- nar objetos no estindar en una instancia de GenClass? Considere fa declaracion class SomeInfo { SomeInfo (int n) ( this.n =n; > private int n; , Ahora el problema es qué pasa si para una declaracién GenClass object = new GenClass(); ejecutamos: object..store(new SomeInfo(17)); System.out.print1n(object.find(new SomeInfo(17))); Al final, 1 se imprime para indicar una biisqueda sin éxito. EI resultado es generado por el método equas( ). El sistema esté utilizando un método integrado para el tipo Ob- Sect que regresa true_si las referencias de las variables comparadas son las mismas, no el contenido de los objetos a los cuales se hace referencia, Para superar esta limitacién, el método equals() debe redefinirse al invalidar la definicién estindar con una nueva definicién. Por lo tanto, la definicién de Some nfo esté incompleta y debe ampliarse a class Someinfo { Someinfo (int n) ( this.n = n; » public boolean equais(Object si) { return n == ((SomeInfo) si).n; , private int n; 24 WM Capitulo 1 Programacidn orientada a objetos utilizando Java Con esta nueva definicion, £ind( ) regresa la posicién del objeto que contiene el niimero 17. La raz6n de que esto funcione adecuadamente es que todas las clases son extensiones de la clase Object. El sistema realiza esta extensién de manera expresa de ‘manera que la declaraciOn de SomeIn£o es en realidad cu SomeInfo extends Object { Elcalificador extends object esté implicito en la definici6n original y no tiene que ser explicito, De esta manera, el método estindar equals) se sobreescribe por re- definicién de este método en la clase Sometnfo y por el poder del enlace dinmico. Cuando se ejecuta la llamada object: .find(new Sometn£o( 17) ), el sistema utiliza el método equals ( )definido en la clase Sometn£o debido a que una instancia de esta clase un argumento en el método call. Por lo tanto, dentro de £ind( ), la variable local se ‘vuelve una referencia a un objeto del tipo Sometn£o, aun cuando esta definido como un parémetro del tipo Object. El problema es, sin embargo, que un ajuste répido como éste s6lo puede realizarse para los métodos integrados para Ob ject, en particular, equals () y toString( ). Sélo ligeramente mas complicado ¢s el caso cuando queremos definir una clase genérica que permita las comparaciones, Un ejemplo més realista de polimorfismo se proporciona en un caso de estudio. ENTRADA Y SALIDA El paquete java. io proporciona varias clases para leer y escribir datos. Para utilizar las clases, el paquete debe incluirse de manera explicita con la instruccién io. import 3) En esta seccidn, introducimos sucintamente las clases para la lectura desde un dis- positivo estindar (teclado), la escritura a un dispositivo estindar (monitor) y el proce- samiento de E/S hacia y desde los archivos. También existen varias otras clases que son particularmente importantes para interactuar con la red, como flujos con bifer,filtra- dos y con cauce (piped). Para imprimir cualquier cosa en la pantalla, son suficientes dos instrucciones: System.out.print (mensaje) ; system.out .print1n( mensaje) ; Las dos instrucciones difieren en que la segunda versién produce el caricter de fin de linea después de imprimir el mensaje. EI mensaje impreso por la instruccién es una ca- dena. La cadena puede componerse de cadenas literales, cadenas de variables y cadenas generadas por el método tostring( ) para un objeto en particular; todos los compo- nentes de la instruccién print estin concatenados con el operador +. Por ejemplo, al haber declarado la clase C: class ¢ ¢ int $= 10; char a= ‘At; public String toString() ¢ return "("# it" Hae yh; , Seccién 1.3 Entrada y salida M25 yun objeto € obj = new c(); una instruccién de impresién System.out.printin("El objeto: * + obj); produce El objeto: (10 A) Observe que si tostring() no estuviera definido en ¢, la salida seria Bl objeto: cé1ce789 debido a que la clase C es una extensién predeterminada de la clase Object cuyo método toString() imprime la direccién de un objeto en particular. Por consiguiente, casi siempre es critico que toString( ) se redefina en una clase de usuario para tener una salida mis significativa que una direccién. La entrada de lectura es marcadamente mas engorrosa, Los datos se leen desde la en- trada estindar con el flujo de entrada System. in. En ese punto, puede utilizarse el metodo read( jel cual regresa un entero, Para leer una linea, el usuario debe definir un método nuevo, por ejemplo, public String readLine() ¢ int ch; String s = **; while (true) ( ery ¢ ch = System.in.read(); if (ch == -1 || (char)ch == ‘\n') // fin de archivo o fin de linea; break; else if ((char)ch != '\r') // ignora el retorno de carro; 8 = 8 + (char)ch; } cateh( IOException @) ¢ y , return s; Dado que read( ) esti definido como un método que lanza un TOException, la ‘excepcidn tiene que manejarse con Ja cliusula try-catch. Observe que ch debe ser un entero de modo que detecte el final del archivo (que es Ctrl-z introducido desde el teclado de la PC). El marcador del final de archivo es niimero =I, y los caracteres en realidad son enteros sin signo. Si ch se declaré como un cardcter, entonces la instruccién de asignacién tendria que ser ch = (char) System.in.read(); con lo que el marcador de fin de linea -1 se almacenaria como el ntimero 65535 en ch, por medio de lo cual la condicién ch == —1 se evaluaria como falsa y el sistema espe- raria otra entrada. 26 MW Capitulo 1 Programacién orientada a odjetos utilizando Java Por fortuna, la tarea puede lograrse por un procedimiento diferente al declarar primero un flujo de entrada con las declaraciones: InputStreamReader cin = new InputStreamReader (System. in); BufferedReader br = new BufferedReader (cin); ‘© con una declaracién: BufferedReader br = new BufferedReader(new InputStreamReader (System. in)); y luego un método integrado readt.ine( ) puede llamarse para asignar un valor a una cadena 8: try { 8 = br-readbine(}; } catch(IOException e) ¢ } Para leer un niimero, una entrada debe leerse como una cadena y luego convertirse a tun ntimero, Por ejemplo, sila entrada estd en la cadena s, entonces la conversién puede realizarse con try ( i = Integer.parsernt(s-trim()); } catch (NumberFormatException e) { System.out.println("No un nimero"); > La entrada puede simplificarse después de que se importa javax. swing .Jop- tionPane: String s = JoptionPane.showInputDialog(*Introduzca un némero*); i = Integer.parsetnt(s.trim()); Para realizar la entrada y la salida en un archivo, debe tomarse una decisién respecto a qué tipo de datos se van a procesar de modo que el tipo apropiado del archivo pueda elegirse. Un archivo binario se procesa como una secuencia de bytes, un archivo de texto como tuna secuencia de caracteres. Por lo general, los archivos de texto son portables; los archivos binarios no lo son debido a que los datos en el archivo tienen la misma representacion que Jos datos almacenados en la memoria principal, y esta representacién varia de un sistema a ‘tro. No obstante, los archivos binarios de Java son independientes de la plataforma. El paquete java. io incluye muchos flujos para realizar la E/S que estin organiza dos en una jerarquia, Las clases InputStream y OutputStream se utilizan para proce- sar los archivos binarios, y las clases Reader y Weiter son para archivos de texto. 1.3.1. Lectura y escritura de bytes Considere un método que lea un byte ala ver desde un archivo de entrada y a la vez lo escriba en un archivo de salida y en la pantalla: void readBytesl(String fInName, String fOutName) throws IOException ( Fileznputstream £In = new FileInputStream(fInNane Fileoutputstrean f0ut = new Pileoutputstream(fouttame) int i; while ((i = fIn.read()) I= -1) ( Seccién 1.3 Entraday salida M27 System.out.print((char)i + " "); // muestra caracteres; // System.out.print(i + * "); // muestra valores ASCII; fOut.write(i); , fIn.close(); fOut .close( Una variable entera se utiliza para leer la entrada, y el final del archivo se indica por medio de -1. Para hacer la Jectura y escritura mas eficientes, la entrada y la salida se al- macenan temporalmente en un biifer: void readBytes2(String €InName, String fOutName) throws IOException { BufferedinputStream fIn = new Bufferedinputstream( new FileInputStream(fInName)); Bufferedoutputstream fOut = new BufferedOutputstream( new FiledutputStream(foutName) ); int i; while ((i = fIn.read()) != -1) { System.out.print(i +" "); fOut.weite(i); > fIn-close(); fOut.close(); 1.3.2 Lectura de lineas Para leer una linea a la vez, se utiliza el método readLine( ) desde BufferedReader, como en el ejemplo: void readLines(String fInName, String fOutName) throws IOException ( BufferedReader fIn = new BufferedReador( new FileReader(fInName)); PrintWriter fOut = new PrintWriter(new FileWriter(foutName)); String s; while ((s = fIn.readbine()) != null) { System.out .print1n(s); fout-printin(s); > £In.close(); fout.close(); El final del archivo se detecta una vez que se realiza Ja lectura de una cadena nula (después de la lectura de una linea vacia, la cadena s no es nula, pero su longitud es 0). 28 W Capitulo 1 Programacidn orientada a objetos utilizando Java 1.3.3 Lectura de simbolos: palabras y nimeros Un Streamrokenizer extrac desde un archivo de texto varios tips de simbolos (to- zens) incluyendo identificadores (secuencias de letras y digitos que comienzan con una letra o un byte del intervalo de \uOOA0' a '\uOOFF') y ntimeros. También puede extraer cadenas entrecomilladas y varios estilos de comentarios cuando el sefializador se ins- tala correctamente. El método nextToken() omite los caracteres de espacio que separan a los simbolos y actualiza las variables de instancia del sefalizador: sval del tipo String, que contiene el simbolo actual cuando ésta es una palabra; nval del tipo double, que contiene el simbolo actual cuando es un ntimero, y ttype del tipo int, que contiene el tipo de simbolo actual. Existen cuatro tipos de simbolos: P7_EOF (inal de archivo), 77_£01, (final de linea), '?_WORD y TT_NUMBER. A continuacién se muestra un ejemplo: void readTokens (String fInName) throws IOException { Streamfokenizer fIn = new Streamfokenizer( new BufferedReader ( new FileReader(£InName))); fIn.nextToken( ); String 8; while (fIn.ttype != Streamfokenizer.TT EOF) { if (£In.ttype == Streamfokenizer.TT_WORD) 8 = “word”; else if (£In.ttype == Streamfokenizer.TT_NUMBER) s = “number”; else 5 = "other"; System.out.println(s + ":\t" + £In); £In.nextToken( ); ‘Cuando un archivo de texto se compone de palabras o miimeros separados por es- pacios en blanco, entonces tal vez sea més facil extraer cada palabra o nimero al leer una linea ala ver y luego aplicar un seftalizador de cadena, como en void readTokens2(String fInName) throws IOException { BufferedReader fIn = new BufferedReader( new FileReader(fInName)); String while ((s = £In.readLine()) != null) ( java.util.stringTokenizer line = new java.util.StringTokenizer(s); while (1ine.hasMoreTokens(}) System. out. printin(Line.nextToken()); » £In.close(); in de un sefializador de flujo puede encontrarse en la Seccidn 1.3 Entradaysalida M28 1.3.4 Lectura y escritura de tipos de datos primitivos La clase DataInputStream proporciona métodos para la lectura de tipos de datos Primitivos en formato binario. Los métodos incluyen readBoolean(), readByte(), readShort(), readChar(), readInt(), readLong() yreadUTF() (para leer cadenas en formato de texto Unicode), void writePrimitives(String foutName) throws IOException { DataOutputStream fout = new Datadutputstream( new FileOutputStream(fOutName) ); fOut .writeBoolean(5<6); fout.writechar(*A'); fOut.writeDouble(1.2); fOut.writeFloat (3.4f); fout.writeshort (56); fOut.writernt (78); fOut .writeLong(90); fout.writesyte('*"); fOut.writeUTF ("abe"); fOut.close(); > void readPrimitives(string £InName) throws IOException { DatainputStream fIn = new DataInputstream( new FileInputStream(fInName)); System.out.printin(fIn.readBoolean() +" * + fIn.readchar() + * "+ fIn.readDouble() +" " + fIn.readFloat() +" " + fIn.readshort() +" * + fIn.readint() +" * + fIn.readtong() + * * + £In.readByte() + * * + £In.readUTF()): fIn.close(); El caso de estudio al final del capitulo se basa en la E/S para tipos de datos primitivos. 1.3.5 Lectura y escritura de objetos Los objetos también pueden guardarse en un archivo si se yuelven persistentes. Un ob- jeto se vuelve persistente si su tipo de clase se establece para implementar la interface Serializable,comoen class C implements Serializable { int i; char c) ( WM Capitulo | Programacién orientada a odjetos utilizando Java is jich= cy y public String toString() ( return "(7+ it" + ch ty; y > ‘Una declaracién como ésta ¢s posible si todas las variables de instancia también son Serializable, que es el caso de la clase C, ya que en Java todos los tipos de datos bisi- os, arreglos y muchas clases son Serializable. He aqui un ejemplo de escritura y lectura de un objeto de clase tipo ¢: void writeobjects(String foutName) throws IOException { C el = new C(20,'A'), C2 = new C(20,'B'); Objectoutputstream fout = new Objectoutputstream( new FileOutputStream(foutName)); fout.writedbject (cl); fout .writeObject (c2); fout .close(); > void readObjects(String fInName) throws IOException ( € el = new C(30,'C"), 2 = cl; ObjectInputStream fIn = new ObjectInputstream( new FileInputStream(fInName) ); try ¢ et = (C)£tn.readobject(); 2 = (C)£In-readobject (); } catch(ClassNotFoundexception e) ¢ y System.out.printin(el + * " + €2); 1.3.6 Archivos de acceso aleatorio Los archivos estudiados hasta ahora se procesan en forma secuencial: leemos (escribi- mos) un elemento a la vez y continuamos hacia el final del archivo. Para poder leer y es- cribir en el mismo archivo en cualquier posicién dentro del archivo, debe utilizarse un archivo de acceso aleatorio. Un archiva se crea con el constructor RandomAccessFile(nombre, modo) ; El constructor abre un archivo con el nombre especificado ya sea para solo lectura © para lectura y escritura. El modo se especifica por medio de la letra w 0 por las letras, rw; por ejemplo, RandomAccessFile = raf new RandomAccessFile("myFile", *rw' Podemos movernos a cualquier parte del archivo, pero para saber que estamos den- tro del archivo podemos utilizar el método length() que regresa el tamano del archivo medido en bytes. El método getPilePointer() regresa la posicién actual en. elarchivo. El método seek (pos) mueve el apuntador del archivo a ta posicién especi- ficada por un entero pos Seccién 1.4 Javay apuntadores M31 La lectura se realiza por el método read( ), el cual regresa un byte como un entero; por read (b), que llena por completo un arreglo de byte b; por read(»,off, Len), que Hlena las celdas Len del arreglo del byte b que comienza desde la celda of £5 Y por readLine( ), que lee una linea de entrada. Otros métodos de lectura regresan un. valor especificado segin su nombre: readBoolean(), readByte(), reaShort(), readChar(), readint(), readLong() y readUTF( ). Todos estos métodos de Jectura tienen métodos de escritura correspondientes, por ejemplo, write(c),donde ¢ esunint, write(b) ywrite(b,off, len), ademis del método writeBytes(s) para escribir una cadena s como una secuencia de bytes. Después de que termina el procesamiento, el archivo debe cerrarse con el método close(): raf.close(): Ejemplos de la aplicacién de archivos de acceso aleatorio pueden encontrarse en el caso de estudio de la secci6n 1.7. EE Java v aruntapores Eneesta seccién, se analiza un problema de implementacién de objetos de Java. ‘Aun cuando Java no utiliza apuntadores explicitos y no permite al programador usarlos, el acceso a los objetos se implementa en términos de apuntadores. Un objeto ‘ocupa un poco de espacio de memoria a partir de una cierta localidad de memoria. Un apuntador a este objeto es una variable que contiene la direccién del objeto y esta direc entonces la instrucci6n de impresi6n systen.out.print(p.equals(q)); Seccién 1.4 Javay apuntadores M33 produce true. (Esto puede lograrse de una manera mucho més elegante en C++ al so- brecargar el operador de igualdad ==; es decir, al definir un método que permite la apli- cacién de este operador a las instancias de la clase Node.) La comprensién de que las variables de objeto son en realidad referencias a los ob- jetos, ayuda a explicar la necesidad de cautela con el uso del operador de asignacién. La intencién de las declaraciones Node nodel = new Node(*Roger",20), node2 = node1; ¢s crearel objeto node1, asignar valores a los dos campos en node}, y luego crear el ob- jeto node? e inicializar sus campos a los mismos valores que en node1. Estos objetos van a ser entidades independientes de modo que la asignacién de valores a uno de ellos no debe afectar a los valores del otro. Sin embargo, después de las asignaciones node2.name = "Wendy"; node2.age = 30; la instruccién de impresién System.out.print1n(nodel.name+" "+nodel.age+" "+node2.name+" “+ node2.age! genera la salida Wendy 30 Wendy 30 Tanto las edades como los nombres en los dos objetos son los mismos. ;Qué ocu- rri6? Debido a que node1 y node? son apuntadores, las declaraciones de node y node? provocan la situacién ilustrada en la figura 1.2a. Después de las asignaciones a Jos dos campos de node, la situacién es como aquella de la figura 1.2b. Para evitar que esto ocurra, tenemos que crear una copia nueva del objeto referenciado y hacer que FIGURA 1.2 —_Ejemplificacién de la necesidad de utilizar el método clone( ) node! node! Relelele Welala veo *f o ] matt x] @ o 1 node! alalgia[el gaia 20 node? Rlolslel+ I 34M Capitulo 1 Programacién orientada a objetos utilizande Java node? se vuelva una referencia a esta copia, Esto puede hacerse al definir el método clone( ) marcado en la interface Cloneable. Una nueva definicién de Node es ahora: class Node implements Cloneable ( String name; int age; Node(String n, int a) { name = n; age = a; > Wode() { this(*",0); » public Object clone() { return new Node(name,age) , public boolean equals(Node a) { return name.equals(n.name) && age d » Con esta definicién, las deciaraciones deben ser Node nodel = new Node("Roger",20), node2 = (Node) nodel.clone(); Jo cual da como resultado la situacién mostrada en la figura 1.2c, de modo que las dos asig- naciones node2.name = "Wendy"; node2.age = 30; afectan sélo al segundo objeto (figura 1.24). Los apuntadores de Java estin ocultos al programador. No hay tipo apuntador en Java. La falta de un tipo de apuntador explicito esta motivada por el deseo de eliminar un comportamiento dastino de los programas, Primero, no es posible en Java tener una refe- rencia no nula o a un objeto no existente. Si una variable de referencia no es nula, siem- pre apunta a un objeto debido a que el programador no puede destruir un objeto referenciado por una variable, Un objeto puede destruirse en Pascal utilizando la funcién. dispose() y en C++ por medio de delete. La raz6n para utilizar dispose() 0 delete es la necesidad de regresar al espacio del administrador de memoria ocupado por un objeto innecesario, Justo después de la ejecucion de dispose() 0 delete, las variables de apuntador alojan las direcciones de los objetos que ya se han regresado al ad~ ministrador de memoria. Si estas variables no se establecen como nulas o como la direc cin de un objeto accesible desde el programa, surge el conocido problema de referencia a tun objeto borrado que puede conducir a un colapso del programa. En Java, el problema de referencia a un objeto borrado no se presenta. Si una variable de referencia p cambia su referencia desde un objeto a otro, y el objeto no esti referenciado por otra variable a, entonces el espacio ocupado por el objeto se reclama automiticamente por el sistema operative a través de la recoleccién de basura (véase el capitulo 12). No existe un equi lente en Java de dispose() o delete. Los objetos innecesarios simplemente se aban- donan y el recolector de basura los incluye en el grupo de celdas de memoria libres en forma automitica durante la eecucién del programa de usuario, Seccion 1.5 Vectores en java.util M@ 38 tra raz6n para no tener apuntadores explicitos en Java es un peligro constante de tener una referencia a una localidad de memoria que no tiene nada que ver con la logica del programa. Esto seria posible a través del apuntador aritmético que no esté permi- tido en Java, donde las asignaciones tales como (p + q)-ch = "bY; (Hp) en = 67 son ilegales. Resulta interesante que aun cuando los apuntadores explicitos estén ausentes en Java, éste se base mis en apuntadores que C/C++. Una declaracién de objeto siempre es tuna declaracién de referencia a un objeto; por lo tanto, una declaracién de objeto Node p; debe ir seguida por la inicializacién de la variable p ya sea al utilizar de manera explicita tun constructor, como en p= new Node); ‘© porla asignaciGn de un valor desde una variable que ya se ha inicializado, como en pea Debido a que un arreglo también es un objeto, la declaracion int a(10]; ¢s ilegal; esta declaraci6n se considera un intento por definir una variable cuyo nombre es {10}. Una declaracién tiene que ir seguida por la inicializacién, que con frecuencia se combina con la declaracién como en int[] a = new int(10}; De esta manera, Java no permite variables que nombren a los objetos directa- mente. Por lo tanto, la notacién de puntos utilizada para tener acceso alos campos del objeto, como en p.name, en realidad es una referencia indirecta al campo nane debido a que p noes el nombre del objeto con el campo name, sino una referencia a (direccién de) este objeto. Por fortuna, el programador no tiene que preocuparse por esta distin- ci6n debido a que todo es cuestion de una implementacion del lenguaje. Pero como se mencioné, una comprensién de estos detalles de implementacion ayuda a explicar los resultados de algunas operaciones, como se ilustr6 antes con el operador Hs vecrores en java.util Una clase util en el paquete java.util ¢s la clase Vector aun cuando en la actualidad se considera una clase heredada. Un vector es una estructura de datos con un bloque de memoria contiguo, al igual que un arreglo. Debido a que las localidades de memoria son contiguas, pueden accederse aleatoriamente de modo que el tiempo de acceso de cualquier elemento del vector sea constante. El almacenamiento se maneja en forma au- tomdtica de modo que en un intento por insertar un elemento en un vector completo, un bloque de memoria més grande se asigna para el vector, los elementos del vector se copian al bloque nuevo y el bloque viejo se libera. El vector es por lo tanto un arreglo flexible; es decir, un arreglo cuyo tamafo puede cambiar dinmicamente, 36 «M Capitulol ramacién orientada a objetos utilizando Java La jerarquia de clases del paquete java.util esa siguiente: Object = AbstractCollection = AbstractList = Vector La figura 1.3 lista en orden alfabético los métodos de la clase Vector. Algunos de estos métodos se heredan de AbstractList; otros son de AbstractCollection. La figura 1.3 lista la mayoria de los métodos de la clase. S6lo los métodos iterator() y List Iterator), heredados de la clase AbstractList, y los métodos finalize(), getClass(), notify(), notifyAll() y wait), heredados de la clase Object, no estan incluidos, FIGURA 1.; Lista alfabética de las funciones miembro de la clase java.util.Vector. Método Operacion void add(object ob) {insert el objeto ob al final del vector void add(int pos, Object ob) —_insertaelabjeto ob en a posicién pos después de desplazar una ‘posicidn los elementos en las posiciones que estin despus de pos; arroja ArrayIndexOutOfBoundsException si pos esti fuera del intervalo boolean addAll (Collection c) _afade todos los elementos de la coleccibn e al final del vector; tegresa true sic mo estévacio;arroja Array IndexOutOfBoundsException si [POs esté fuera del intervalo yNul1PointerException sices nulo boolean addall(int pos, atade todos los elementos dela coleccin ¢ en la posicién pos del vector Collection ¢) después de desplaza ls objetos que van después de a posicidn pos; arroja ArrayIndexOutOfBoundsException si pos ests fuera del inter- valoy Wall PointerException sic.esnulo void addBlement (Object ob) inserta el objeto ob al final del vector; lo mismo que add. (ob) int capacity() regresa el niimero de objetos que pueden almacenarse en el vector void clear() public boolean equals(Object pr) { return SSN.equals(((Personal)pr).SSN); , public void writeToFile(RandomAccessFile out) throws IOException { writeString(SSN,out); writeString(name, out); writestring(city, out); out.writernt (year out .writeLong( salary); } public void writeLegibly() { Seccién 1.7 Caso de estudio: archivo de acceso FIGURA 1.5 (continuacién) System.out-print("SSN = " + SSN +", name = * + name.trim() city =" + city.trim() +", year = * + year salary = " + salary: , public void readPromFile(RandomAccessFile in) throws IOException { SSN = readString(9, in); name = readString(nameLen, in); city = readstring(cityLen, in); year = in.readInt(); ry = in.readLong(); > Public void readKey() throws IOException { System.out.print("Enter SSN: *); SSN = readLine(); } public void readFromConsole() throws I0Exception { System.out.print("Enter SSN: "); SSN = readtine(); System.out.print("Name: *); name = readLine(); for (int i= name.length(); i < nameLen; i++) name += ' '; System.out.print("City: * city = readLine(); for (int i = city-length(); i < cityLen; i++) city + * System.out.print("Birthyear: "); year = Integer.valueOf(readLine().trim()).intvalue(); System.out.print("salary: "); salary = Long.valueof(readLine().trim()).longvalue(); > public void copy(Dbobject{] d) ¢ 4[0] = new Personal(SSN,name,city, year, salary); , Ve steteseeaeseenes Student. J sto. import jav public class Student extends Personal { public int size() ¢ ‘continda 46 WM Capitulo 1 Programacion orientada a objetos utilizando Java FIGURA 1.5 (continuacién) return super.size() + majorLen*2; ? protected String majo protected final int majorLen = 10; Student() ¢ super()+ ) Student (String ssn, String n, String c, int y, long s, String m) { super(ssn,n,c,y,s major = m; > public void writeToFile(RandomAccessFile out) throws IOException { super.writeToFile(out); writestring(major,out); ) public void readFromPile(RandomAccessFile in) throws IOException { super.readFromFile(in); major = readString(majorLen, in); , public void readFromConsole() throws IOException { super. readFromConsole(); System.out.print(*Introduzca una asignatura principal: “); major = readLine(); for (int i = major.length(); 4 < nameLen; i++) major += * '; ? public void writebegibly() ( super.writeLegibly(); System.out.print(", major = “ + major.trim()); » public void copy(DbObject{] d) { [0] = new Student (SSN,name,city, year ary,major); , » [[ttetesaeseseetesesaeesaes Database. a tensaesaeanssaneaseenens import java.io.*; public class Database ( private RandomAccessFile database; private String fName = new String();7 Seccién 1.7 Caso de estudio: archivo de acceso aleatorio M47 FIGURA 1.5 (continuacién) private Tomethods io = new Tomethods(); Database() throws IOException { System.out.print("File name: *); fame = io.readbine(); ss private void add(Dbobject d) throws IOException { database = new RandomAccessFile(fName, "rw"); database. seek(database.length()); d.writeToFile(database) ; database.close( , Private void modify(Dbobject d) throws ToException { DbObject{] tmp = new DbObject[1}; .copy (tmp) + database = new RandomAccessFile(fName,"rw"); while (database.getFilePointer() < database.length()) { tmp[0].readFromFile(database); if (tmp[0}.equals(d)) { tmp[0].readFromConsole(); database. seek (database. getFilePointer()-d.size()); tmp[0}.writeToFile(database); database.close(); return; , ) database.close(); System.out.print1n("El registro que se va a modificar no esta en la base de datos"); , private boolean find(DbObject d) throws IOException { DbObject{} tmp = new Dbobject (1); copy (tmp) ; database = new RandomAccessFile(fName, "r* while (database.getFilePointer() < databa ‘tmp[0].readFronFile(database); if (tmp[0}.equals(d)) { database.close(); return true: slength()) { , } database.close(); continda 48 WM Capitulo 1 Programacién orientada a objetos utilizando Java FIGURA 1.5 (continuacién) return false; > private void printDb(Dbobject d) throws IOException ( database = new RandomAccessFile(fName,"r"); while (database.getFilePointer() < database.length()) { 4. readFromFile(databas d.writeLegibly(); System.out.printin(); ) database.close(); f public void run(pbobject rec) throws IOException ( String option; System.out.printin("1. Afadir 2. Buscar 3. Modificar un registro;4. salir"); System.out.print("Introduzca una opcién: "); option = io.readLine(); while (true) { if (option.charat(0 Say rec.readFromconsole(); add(rec); > else if (option.charAt(0) == '2') { rec.readKey(); System.out.print("El registro es "); if (find(rec) == false) System.out.print("no "); System.out.printin("en la base de datos"); t else if (option.charAt(0) == '3') ( rec.readKey(); modify(rec); ) else if (option.charAt(0) != '4') System. out.print1n("Opcién errénea"); else return; printDb(rec); Systen.out.print(*Introduzca una opcién: *); option = io.readLine(); Seccidn 1.7 Caso de estudio: archivo de acceso aleatorio Mm 49 FIGURA 1.5 []asenesnennsenens: (continuacién) * UseDatabase. java io.*; UseDatabase { static public void main(string| ]) throws I0Exception { ” (new Database())-run(new Personal()); (new Database(}).run(new Student ()); El método €ind( ) utiliza hasta cierto punto el hecho de que el archivo es aleatorio al inspeccionarlo registro por registro, no byte por byte. Para estar seguros, los registros se construyen a partir de bytes y todos los bytes que pertenecen a un registro en particu- lar que se vaa leer, pero solo los bytes requeridos por el operador de igualdad estén par- ticipando en la comparacién. El método modi fy () actualiza la informacién almacenada en un registro en par- ticular. El registro primero se recupera desde el archivo, también utilizando la biisqueda secuencial, y la informacién nueva es lefda por el usuario que utiliza el método readFromFile() definido para una clase en particular. Para almacenar en el archivo el registro tmp 0} actualizado, modi fy () obliga a la base de datos de apuntadores de archivo a regresar al principio del registro tmp 0} que se acaba de leer; de lo contrario, el registro que va después de tmp 0} en el archivo se sobreescribiria. La posicién inicial de tmp puede determinarse de inmediato debido a que cada registro ocupa el mismo niimero de bytes; por consiguiente, es suficiente regresar al mimero de bytes ocupados por un registro. Esto se logra al llamar a database. seek (database. getFile- Pointer ()-d.size()),donde size() debe definirse para la clase en particular. La clase Database genérica incluye dos métodos més. El método add( ) coloca un registro al final del archivo. El método printDb( ) imprime el contenido del archivo. Para ver la clase Database en accién, tenemos que definir una clase que especifique el formato de un registro en un archivo de acceso aleatorio. Como ejemplo, definimos la clase Personal con cinco campos, SSN, name, city, year y salary. Los primeros tres campos son cadenas, pero s6lo SSX tiene siempre el mismo tamafio. Para tener un poco més de flexibilidad con las otras dos cadenas, hay otras dos constantes definidas, name~ Leny cityLen. El almacenamiento de datos desde un objeto requiere particular atencidn, ésta es la tarea del método writeToFile(). El campo SSN ¢s el més simple de manejar. Un niimero de seguridad social siempre incluye nueve digitos; por lo tanto, puede utilizarse el operador de salida <<. No obstante, las longitudes de los nombres y ciudades varian de registro a registro, y aun asi las secciones de un registro en el archivo de datos dise- fiado para estos dos campos siempre deben tener la misma longitud. Para garantizar esto, el método readFromConsole () afiade a las cadenas espacios en blanco de relleno. ‘Otro problema es planteado por los campos numéricos, year y salary, en particu- lar el ultimo campo. Si salary se escribe en el archivo con el método printLong( ), entonces el salario 50 000 se escribe como una cadena de 5 bytes de longitud *50000*, y 5D WM Capitulo 1 Programacién orientada a abjetos utilizando Java 1 salario 100 000 como una cadena de 6 bytes de longitud *100000’, lo cual viola la condicién de que cada registro en el archivo de acceso aleatorio debe tener la misma longitud. Para evitar el problema, los nsimeros se almacenan en forma binaria. Por ejemplo, 50 000 esté representado en el campo salary como una cadena de 32 bits, ‘600000000000000011100001 101010000. Ahora podemos tratar a esta secuencia de bits no ‘como si representara un ntimero grande, sino una cadena de cuatro caracteres, 00000000, 00000000, 11000011, 01010000; es decir, los caracteres cuyos cédigos ASCII son, en sistema decimal, los ntimeros 0,0, 195 y 80. De esta forma, sin importar el valor de salary, el valor siempre se almacena en 4 bytes. Esto se logra en Java con el método writeLong( ). Este método de almacenar registros en un archivo de datos plantea un problema de legibilidad, en particular en el caso de mimeros. Por ejemplo, 50 000 se almacena como 4 bytes: dos caracteres nulos, un carécter especial y una letra maydiscula P. Para una persona que lee, es mas que evidente que estos caracteres representan 50 000, Por lo tanto, se nece- sita una rutina especial para imprimir los registros en forma legible. Esto se logra utilizando el método writeLegibly (), el cual explica por qué este programa utiliza dos métodos para leer registros y dos para escribir registros: uno es para mantener los datos en un archivo de acceso aleatorio, el oteo es para leery escribir datos en forma legible. Para probar la lexibilidad de la clase Database, se define otra clase de usuario, la clase Student. Esta clase también se utiliza para mostrar un ejemplo mas de la herencia. La clase Student utiliza los mismos campos de datos que la clase Personal al estar definida como una clase derivada de Personal més un campo adicional, un campo de cadena ma jor. B! procesamiento de la entrada y la salida en los objetos de la clase tipo Student es muy parecida a aquella de Ja clase Personal, pero el campo adicional debe ser representado, Esto se hace redefiniendo los métodos a partir de la clase base y al mismo tiempo reutilizindolos. Considere el método writeToPile() para escribir registros de estudiantes en un archivo de datos con un formato de longitud fija: public void writeToFile(RandomAccessFile out) throws IOException{ super .writeToPile(out); writestring(major, out) ; El método utiliza el writeToPile( ) de la clase base para inicializar los cinco campos, S8N,name, city, year y salary, ¢ inicializa el campo ma jor. Observe que una varia- ble especial super se debe utilizar para indicar claramente que el weiteToFile() que se esti definiendo para la clase Student llama al writeToFile( ) que ya esté definido en la clase base Personal. Sin embargo, la clase Student hereda sin la modificacién al método readkey() y el método equals(), debido a que la misma clave se utiliza tanto en Jos objetos Personal como en Student para identificar en forma tinica cualquier registro, en concreto,a SSW. Seccién 1.8 Ejercicios M51 HB) csercicios 1. ;Cuil debe ser el tipo de constructores definidos en clases? 2, Suponga que la clase e1assa incluye una variable k privada, una variable m sin modi- ficador, una variable privada n protegida, una variable p protegida y una variable pablica q-Ademiés, la clase cLassB se deriva de la clase classA, la clase class no se deriva de elassa, y las tres clases estin en el mismo paquete. Adems, la clase classD se deriva de classA, la clase class? no sederiva de class, y class esti en un paquete dis- tinto a aquel en que estén las clases cLassD y classE. ;Cual de las cinco variables definidas en la clase cLassa puede utilizarse por cualquiera de las otras cuatro clases? 3 Qué pasa sila declaracién dec: class ¢ ¢ void processi(char ch) { System.out.printin(" slenC* + ch); entro de proc » void process2(char ch) { System.out.printin(*Dentro de process? en C * + ch); > void process3(char ch) { System.out.print1n( "Dentro de process} en C * + ch); process2(ch); > vva seguida de la siguiente declaracién de su extensién: class ExtC extends C { void processi(int n) ¢ system.out.printin( "Dentro de process} en Extc * + n); d void process2(char ch) { system.out-printin(*Dentro de process? en ExtC * + ch); 5 void process4(int n) ¢ System.out.print1n( "Dentro de process4 en Extc * + n); » » Cuales métodos se invocan sila declaracién de los tres objetos ExtC object1 = new ExtC(); € object2 new Extc(), object3 = new Extc(); 52M Capitulo 1 Programacién orientada a objetos utilizanda Java vva seguida por estas instrucciones; indique cualquier problema que estas instruc- ciones puedan causar: object] .proct object 1.proces: object2.proct object2.procr object3.proces: object3.proces: object3.proces: 1(1000); 4(2000); 1 (3000); 4( 4000); 1(1PY); 20" DF BCR DT 4. Para la declaracién 11: interface T1 { int T1£1Q; void T1f2(int i , identifique los errores. a. enladeclaracién de la interfaz 12+ interface I2 extends I1 { double 12£1(); void 12f2(int 1); int T1£10); double 12#1() { return 10; } private int Acl£4(); private int n = 10; y b, ena decalaracién della clase C11: el CT implements 11 { int 11£1() { weed void T1f2(int i) {- +--+} int Cr1€3() (++ + + +} > c. yenladeclaracién del objeto e6: 11 c6 = new 110); 5. Identifique los errores de: a. abstract class ACI { int acIf1() (. + + +} void AcIf2(int i) {-- +} int ac1£3()7 Seccién 1.9 Tareas de programacién M53 b. interface C6 extends CaCl {. . - } donde caci es una clase, © class CACIAC2 extends ACI, AC2{... } donde Aci y Acz2 son dos clases abstractas. d. acl ¢7 = new acl(); donde Aci ¢s una clase abstracta. 6. Qué pasa sila clase Somezn£o en ver de la definicién de equals ( ) de la seccién 1.2.4 utiliza la definicién siguiente de este método: Public boolean equals(SomeInfo si) { return n == si.nj > TAREAS DE PROGRAMACION 1. Escriba una clase Fract ion que defina la suma, resta, multiplicacién y division de fracciones. Luego escriba un método para reducir los factores y métodos para intro- ducir ¢ imprimir fracciones. 2. Escriba una clase Quaternion que defina las cuatro operaciones bisicas de cuater- niones y las dos operaciones de E/S. Los cuaterniones, segtin los definié en 1843, William Hamilton y publicé en sus Lectures on Quaternions en 1853, son una exten- si6n de los niimeros complejos. Los cuaterniones son cuddruplos de los némeros reales, (aod) = a + bi + 6 + dk, donde 1 = (1,0,0,0),1= (0,1,0,0),j= (0,0,1,0) y k= (0,0,0,1), y las ecuaciones siguientes sostienen que: Wk jk=iki=j,jf=-k kj= ii (a+ bit oj +dk) + (pt git rj+sk) =(atp)+ (b+ git (ct nj+(dt9k (a+ bi+ 6 +dk)-(p + qi r+ 5k) = (ap —bq~ cr ds) + (ag + bp + es~ dri + (ar+ ep + dq—bs)j + (as + dp + br~ eq)k. Utilice estas ecuaciones en la implementacién de una clase de cuaterniones. 3. Escriba un programa para reconstruir un texto a partir de una concordancia de palabras. Este fue un problema real de reconstruccién de algunos textos inéditos de los Rollos del Mar Muerto utilizando concordancias, Por ejemplo, en seguida se presenta el poema de William Wordsworth, Nature and the Poet (La naturaleza y el poeta), y una concordancia de palabras que corresponden al poema. So pure the sky, so quiet was the air! (Tan claro el cielo, tan quieto el viento) So like, so very like, was day to day! (Tan parecido, todos los dias) ‘Whene'er [look'd, thy image still was there; (Siempre que veia, tu imagen seguia alli) It trembled, but it never pass'd away. (Se estremecia, sin desaparecer) 54 M_ Capitulo 1 Programaci6n orientada a objetos utilizando Java 5 La concordancia de 33 palabras ¢s la siguiente: 1:1 so quiet was the *air! tan quieto el *viento! 1:4 but it never pass'd "away. sin "desaparecer 1:4 [t trembled, “but it never ‘se estremecia *sin 1:2 was *day to day! todos “los dias! 1:2.was day to “day! todos “los dias! 3 thy “image still was there; ‘tu “imagen seguia ahis 1:2 so very like, “was day tan parecido, *todos los dias 1:3 thy image stil "was there; ‘tuimagen seguia *ahi 1:3 *Whene'er I look’d, “siempre que veia, Enesta concordancia, cada palabra se muestra en un contexto de hasta cinco palabras, ya la palabra referida en cada linea le antecede un asterisco. Para concordancias mas ‘grandes, dos niimeros deben incluirse, uno que corresponda al poema y uno al iimero de linea donde se encuentran las palabras. Por ejempio, suponiendo que 1 es el niimero de Nature and the Poet, inea1:4 but it never pass'd “away” significa que la palabra “away” se encuentra en este poema en la linea 4. Observe que las marcas de puntuacién se incluyen en el contexto. Escriba un programa que cargue una concordancia desde un archivo y cree un vector en el que cada celda se asocie con una linea de la concordancia. Luego, utilizando una buisqueda binaria, reconstruya cl texto. Modifique el programa del caso de estudio manteniendo un orden durante la inser- cién de los registros nuevos en el archivo de datos. Esto requiere que se defina ef método compareTo() en Personal y en Student para utilizarlos en un método add() modificado en Database. E método encuentra una posicién apropiada para un registro d, mueve todos los registros del archivo para hacer espacio para d y es- cribe d en el archivo. Con la nueva organizacién del archivo de datos, find() y modi fy () también pueden modificarse. Por ejemplo, £ind( ) detiene la busqueda secuencial cuando encuentra un registro mayor que el registro buscado (o llega al final del archivo). Una estrategia mds eficaz puede utilizar la busqueda binaria, estu- dada en la seccién 2.7. Escriba un programa que mantenga un orden en el archivo de datos en forma indi- recta. Utilice un vector de apuntadores de posicién de archivo (obtenidos a través de getFilepointer()) y mantenga el vector en el orden de clasificacién sin cambiar el orden de los registros dentro del archivo. Modifique el programa del caso de estudio para eliminar los registros del archivo de datos. Defina el método iswu11 ( ) en las clases Personal y Student para determ nar que un registro es nulo, Defina también el método writeNul1ToF ile( ) en las dos clases para sobreescribir un registro que se va a eliminar por un registro nulo. Un registro nulo puede definirse como si tuviera un cardcter no numérico (una lipida) en la primera posicién del campo SSN. Luego defina el método remove( ) en Data~ base (muy parecido a modi fy () ), que localiza la posiciOn de un registro que se va a eliminar y lo sobreescribe con el registro nulo, Después de que se termina una sesi6n, un método Database del destructor purge ( ) debe invocarse con copias de registros no nulos a un archivo de datos nuevo, elimina el archivo de datos viejo y renombra ef archivo de datos nuevo con el nombre del archivo de datos viejo, Bidliogratia M55 BB sisuiograria Programacién orientada a objetos Cardelli, Luca y Wegner, Peter,”On Understanding Types, Data Abstraction, and Polymorphism’, Computing Surveys 17 (1983),471-522. Ege, Raimund K., Programmting in an Object-Oriented Environment, San Diego: Academic Press, 1992, Khoshafian, Setrag y Razmik, Abnous, Object Orientation: Concepts, Languages, Databases, User Interfaces, Nueva York: Wiley, 1995. Meyer, Bertrand, Object-Oriented Software Construction, Upper Saddie River, Nueva Jersey: Prentice Hall, 1997. Java Dunn, Douglas, Java Rules, Reading, Massachusetts: Addison-Wesley, 2002. Gittleman, Art, Computing with Java, El Granada, California: Scott/Jones, 2002. Gosling, James, Joy Bll y Bracha, Silad, The Java Language Specification, Boston: Addison-Wesley, 2000. Naughton, Patrick y Schildt, Herbert, Java 2: The Complete Reference, Berkeley, California: Os- borne McGraw-Hill, 1999, ‘Weber, Joe (ed.), Using java 2 Platform, Indianspolis: Quebec, 1999, ‘Weiss, Mark A, Data Structures and Problem Solving Using Java, Reading, Massachusetts: Addison-Wesley, 2001. Analisis de la complejidad COMPLEJIDAD COMPUTACIONAL Y ASINTOTICA Con frecuencia el mismo problema puede resolverse con algoritmos que difieren en efi- iencia. Las diferencias entre los algoritmos pueden ser irrelevantes para el proce- samiento de un pequefio nimero de elementos de datos, pero estas diferencias crecen con la cantidad de informacién. Para comparar la eficiencia de los algoritmos, Juris Hartmanis y Richard E. Stearns desarrollaron una medida del grado de dificultad de un algoritmo llamada complejidad computacional. La complejidad computacional indica el esfuerzo que se requiere aplicar a un algo- ritmo 0 cudn costoso resulta. Este costo puede medirse en una variedad de formas, y el contexto particular determina su significado. Este libro se ocupa de los dos criterios de efi- ciencia: el tiempo y el espacio. El factor del tiempo por lo general es mas importante que el factor del espacio, asi que es comiin que las consideraciones de eficiencia se centren en la cantidad de tiempo transcurrido cuando se procesan datos. Sin embargo, el algoritmo mis ineficiente que corre en una computadora Cray puede ejecutarse mucho mis répido que el algoritmo mas eficaz.que corre en una PC, asi que el tiempo de ejecucién depende siempre del sistema. Por ejemplo, para comparar 100 algoritmos, todos ellos tendrian {que ejecutarse en la misma maquina. Ademés, los resultados de las pruebas en tiempo de ejecucién dependen del lenguaje en que se escribe un algoritmo dado, incluso si las prue- bas se realizan en la misma méquina. Si los programas se compilan, se ejecutan de una manera mucho més répida que cuando se interpretan. Un programa escrito en C 0 Ada puede ser 20 veces mds rapido que el mismo programa codificado en BASIC 0 LISP. Para evaluar la eficiencia de un algoritmo, no deben utilizarse unidades en tiempo real como los microsegundos y los nanosegundos. En su lugar deben usarse las unidades logicas que expresan una relacién entre el tamafio n de un archivo un arreglo y la canti- dad de tiempo t requerida para procesar los datos. Si existe una relacién lineal entre el tamafo n y el tiempo tes decir, f, = en,— entonces un incremento de datos por un factor de 5 da como resultado un aumento en el tiempo de ejecucién por el mismo factor; sin, = 5n, entonces t, = 5t,. De modo parecido, si t, = log,n, entonces al duplicar n aumenta t s6lo una unidad de tiempo. Por lo tanto, sit, = log,(2n), entonces t, =f, + 1. Una funcién que expresa la relacién entre ry t por lo general es mucho més com- pleja, y el célculo de una funcién como ésta es importante solamente en lo que atafie a los grandes cuerpos de datos; cualquier término que no cambie de manera sustancial la ‘magnitud de la funcién debe eliminarse de la funcidn. La funcién resultante s6lo da una Seccién 2.2 Notacién de ordend M57 medida aproximada de la eficiencia de la funcién original. Sin embargo, esta aproxi- macién esté lo suficientemente cerca de la original, en especial para una funcién que procesa cantidades grandes de datos. Esta medida de la eficiencia se lama complejidad asintética, y se usa cuando no se toman en cuenta ciertos términos de una funcién para expresar la eficiencia de un algoritmo o cuando se calcula una funcién que ¢s dificil 0 imposible y tinicamente se pueden encontrar aproximaciones. Para ilustrar el primer caso, considere el ejemplo siguiente: fn) =n? + 100n + log, + 1.000 (24) Para los pequefios valores de n, el tltimo término, 1 000, es el mayor. Cuando n es igual a 10, el segundo término (100n) y el ultimo (1 000) estén en igualdad de condiciones con respecto a los otros términos, haciendo la misma contribucién al valor de la fun- cin. Cuando m alcanza el valor de 100, el primero y el segundo términos hacen la misma contribucién al resultado. Pero cuando n se vuelve mayor que 100, la contribu- cién del segundo término se vuelve menos significativa. Por consiguiente, para los valores grandes de n, debido al crecimiento cuadritico del primer término (n°), el valor de la funcién f depende del valor del primer término, como se demuestra en la figura 2.1. Otros términos pueden ignorarse para n mayores. FIGURA 2.1 Indice de crecimiento de todos los términos de la funcién fin) = 1 + 100 + log,n+ 1.000. * ) ° 1000 log, 1000 Valor Valor. % Valor == -% Valor % 1 1101 1 O1 100 91 0 00 10 2101 100° 4:76 1000 476 ©1005 100 21002 10000476 10000 476 2 «0.001 1000 4.76 1000 1101003 1000000 90.8 100000 91 3 0.0003 1000 009 10.000 101001004 100.000.0009. 1000000 099 4 00 1000 0.001 100.000 10010001005 10000.000000. 99.9 10000000 0.099 5 00 1000 0,00 NOTACION DE ORDEN O La notacién de uso més comiin para especificar la complejidad asintética —es decir, para estimar el indice de crecimiento de funciones— es la notacién de orden O intro- ducida en 1894 por Paul Bachmann. Dadas dos funciones valuadas como positivas fy g, considere la definicion siguiente: Definicién 1: f(n) es O(g(7)) si existen los nimeros positivos cy N tales que flrt) $ ¢g(n) para todas las nN. Esta definicién se lee: fes de orden O de g si existe un niimero positivo ¢ tal que fno es mayor que ¢g para n suficientemente grandes; es decir, para todas las n mayores que 58 MW Capitulo 2 Analisis de la complejidad algdn niimero N. La relaciOn entre fy g puede expresarse al establecer ya sea que g(n) es un limite superior del valor de f(n) 0 que, ala larga, ferece cuando mucho a la misma velocidad que g. El problema con esta definicin es que, primero, establece que deben existirciertas ¢ y N, pero no da ninguna pista de cémo calcular dichas constantes. Segundo, no pone ninguna restriccién a estos valores y proporciona poca orientacién en situaciones donde hay muchos candidatos. De hecho, por lo general existe una infinidad de pares de cy N ‘que pueden darse para el mismo par de funciones fy g. Por ejemplo, para fin) = 20 + 3n+ 1 = O(n") (22) donde g(n) = n?, los valores candidatos para cy N se muestran en la figura 2.2. FIGURA 2.2 _Diferentes valores de cy N para la funcién f(n) = 2n? + 3n+1 acuerdo con la definicién de orden O. baci tesa sia eas N i 2 H 4 5 : > = Obtenemos estos valores al resolver la desigualdad: 20 +3n+1Sen* ode manera equivalente 3,4 2+ + 75Se para diferentes n. La primera desigualdad da como resultado la sustitucién de la fun- cién cuadratica de la ecuacién 2.2 para f(n) en la definicion de la notacién de orden O y de n? para g(n). Dado que se trata de una desigualdad con dos pares de constantes ¢ y N desconocidas y distintas para la misma funcién g(= n*) puede determinarse. Para elegir las constantes ¢ y N mayores, debe determinarse para cul N un cierto término en f se vuelve mayor y permanece como el mayor. En la ecuacién 2.2, los tinicos candidatos para el término mayor son 2n* y 31; estos términos pueden compararse usando la de- sigualdad 2n? > 3n que se cumple para n > 1. Por lo tanto, N = 2 y c2 32, como se indica ena figura 2.2. Cua es el significado prictico de los pares de constantes que se acaban de listar? ‘Todos ellos estan relacionados con la misma funcién g(n) = 1 y con la misma f(n). Para uuna ¢ fija, puede identificarse un mimero infinito de pares de cy N. El punto es que fy g crecen al mismo ritmo. La definicién establece, no obstante, que g casi siempre es mayor © igual que fsi esté multiplicado por una constante c. "Casi siempre" significa para todas las m que no son menores que una constante N. La esencia del problema es que el valor de cdepende de cual N se elige, y viceversa. Por ejemplo, si se elige 1 como el valor de N—es decir, si g se multiplica por ¢ de modo que cg(n) no sea menor que f inmediatamente— entonces c debe ser igual o mayor que 6. Si cg(7) es mayor 0 igual que f(1) a partir den = 2, entonces es suficiente que c sea igual a 3.75. La constante c debe ser cuando menos 3 Seccién 2.3 Propiedades de la notacién de ordend Mm 59 si qg(m) no es menor que f(n) a partir de n = 3. La figura 2.3 muestra las grificas de las funciones fy g. La funcién g se traz6 con diferentes coeficientes c. Ademis, N siempre es tun punto donde las funciones cg(n) y fs intersecan. FIGURA 2.3 Comparacidn de las funciones para distintos valores de c y N tomados de la figura 2.2. La imprecisidn inherente a la notacién de orden O va incluso mis lejos, debido a que pueden existir muchisimas funciones gms para una funcién fdada. Por ejemplo, la fde la ecuacién 2.2 esel orden O no sélo de m, sino también dem n',...,nt,.... para cualquier 22, Para evitar este conflicto se elige la funcién g més pequefia, n* en este caso. La aproximacién de la funcién f puede refinarse utilizando la notacién de orden © para la parte de la ecuacién que contiene informacién no relevante. Por ejemplo, en la ecuacién 2.1, la contribucién del tercer y ultimo términos al valor de la funcién puede omitirse (véase la ecuacién 2.3). fin) =n? + 100n + O(log.) (23) De modo parecido, la funcién fen la ecuacién 2.2 puede aproximarse como J(n) = 20? + O(n) (24) PROPIEDADES DE LA NOTACION DE ORDEN O La notacién de orden O tiene algunas propiedades ttiles que pueden utilizarse cuando se estima la eficacia de los algoritmos. Hecho 1. (transitividad) Si f(n) es O(g(r)) y g(m) es O(h(n)), entonces f(n) O(h(n)). (Esto puede expresarse de otra manera como O(O(g(1t))) es O(g(n)).) 60 WM Capitulo 2 Analisis de la complejidad Prueba: De acuerdo con la definici6n, f(n) es O(g(n)) si existen ntimeros positivos ¢, y N, tales que f(n) < ¢,¢(n) para todas las n 2-N,,y g(1) es O(h(n)) siexisten ntimeros positivos c, y N, tales que g(1) S c,)t(n) para todas las n 2 N,,. Por consiguiente, ¢,¢(1) $ ¢,¢:h(n) para n 2 N donde N es el mayor entre N, y N;-Sitomamos ¢= ¢,¢, entonces f(n) S$ ch(n) para n2N, lo cual significa que fes O(h(n). Hecho 2. Si f(n) es O(bi(n)) y g(n) es O(h(7)), entonces f(n) + g(n) es OCh(n)). Prueba: Después de establecer que ces igual ac, + cy, f(n) + s(n) Sch(n). Hecho 3. La funcién ant es Ont). Prueba: Para que la desigualdad an < cnt se cumpla, es necesario que c2 a. Hecho 4. La funcién n* es O(n'*’) para cualquier j positiva. 1 Prueba: La afirmacién sostiene que si c= N A partir de estos hechos se deduce que todo polinomio es del orden O de n elevado ala mayor potencia o fin) =a +a nh +++ ayn-ta, es On) ‘También es evidente que en el caso de los polinomios, fin) es O(n") para cualquier j positiva. Una de las funciones mas importantes en la evaluaci6n de la eficiencia de los algo- ritmos es la funcidn logaritmica. De hecho, si puede decirse que la complejidad de un algoritmo es de orden de funcién logaritmica, puede considerarse que el algoritmo es muy bueno. Existe un nimero infinito de funciones que pueden considerarse mejores que la funcién logaritmica, entre las que solo unas cuantas, como O(lg Ig n) 0 O(1), son relevantes en la prictica. Anteriormente mostramos un hecho importante sobre las funciones logaritmicas, expongimoslo sin prueba: Hecho $. Si f(n) = cg(), entonces f(n) es O(g(n)). Hecho 6. La funcién log, 1 es Otlog, n) para cualesquier nuimeros positivos a y b# 1. Esta correspondencia se mantiene entre las funciones logaritmicas. El hecho 6 establece que a pesar de sus bases, las funciones logaritmicas son de orden O entre si; es decir, todas estas funciones tienen el mismo indice de crecimiento. Prueba: Sea log, n= xy log, n = y, tenemos, por la definicién de logaritmo, que a = 1 yW=n Al tomar In de los dos lados obtenemos xlna=Inn y yinb=Inn Porlo tanto xlna=ylnb, In alog, n=In blog, n, Jog, n= Hb og, n= clog, Seccién 2.4 NotacionesQy@ mM 61 loccual demuestra que log, ny log, n son miltiplos uno de otro. Por el hecho 5, log, m es O(log, n). ‘Como la base del logaritmo es irrelevante en el contexto de la notacién de orden O, siempre se utiliza s6lo una base y el hecho 6 puede escribirse como Hecho7. Ellog, n es O(Ig n) para cualquier ntimero positivo a% 1, donde Ig n = log, n. Notaciones Q y @ La notacién de orden O se refiere alos limites superiores de las funciones. Hay una defi- nici6n simétrica para un limite inferior en la definicién de orden Q: Definicién 2: La funcién f(n) es Q(g(1)) siexisten mimeros positivos cy N tales que f(t) 2 eg(n) para todas las nN. Esta definicién se lee: fes de orden Q (omega) de ¢ si existe un muimero positive ctal que {Fes cuando menos igual a ¢g para casi todas las n, En otras palabras, eg(n) es un limite inferior en el tamafio de f(n), 0, a la larga, fcrece cuando menos al ritmo de g. La tinica diferencia entre esta notacién y la definicién de orden O es la direccién de desigualdad; una definicién puede convertirse en la otra al reemplazar el signo “2” con “<7 Existe una interconexién entre estas dos notaciones expresada por la equivalencia f(n) es Q¢g(n)) si y sdlo si gm) es O(f(n)) La notacién Q padece el mismo problema de profusién que la notacién de orden, ©: Existe un niimero ilimitado de opciones para las constantes cy N. Para la ecuacion 2.2, estamos buscando una c para la cual 2n? + 3n + 1 2 cn’, lo cual es verdadero para cualquier n2 0, sic 2, donde 2 es el limite para cen la figura 2.2. Ademés, si fes Q de g y hg, entonces fes Q de h; es decir, i para f podemos encontrar una g tal que f sea Q de g, entonces podemos encontrar un ntimero infinito. Por ejemplo, la funcién 2.2 es de orden Q de 1 pero también de 1, n,n", n'4,...,y también de Ig n, Ig Ig m,.-.5¥ de muchas otras funciones. Para propésitos pricticos, dnicamente las 2 mas cercanas resultan interesantes (por ejemplo, los limites inferiores mayores). Esta restriccién se hace de manera implicita cada vez que elegimos una Q de una funcion f. Existe un ntimero infinito de limites inferiores posibles para la funcién f; es decir, hay un conjunto infinito de ¢ tales que f(n) es £2(g(n)) asi como un numero infinito de imites superiores posibles de f. Esto puede ser un tanto inquietante, ast que restringi- ‘mos nuestra atencidn a los limites superiores mas pequefios y a los limites inferiores més grandes. Observe que hay un lugar comin para las notaciones de orden O y de orden &2 indicadas por las igualdades en las definiciones de estas notaciones: el orden O se define en términos de “S” y el orden @ en términos de “2”; “=” se incluye en las dos desigualdades. Esto sugiere una forma de restringir los conjuntos de limites inferiores y superiores posibles. Esta restriccién puede lograrse por medio de la siguiente definicion de la notacién @ (teta): Definicion 3: f(n) es (¢(n)) si existen nimeros positives ¢,¢, y N tales que ¢,g(n) S J (n) Scxg(n) para todas las n2 N. 62M Capitulo 2 Andlisis de la complejidad Esta definicin se lee: ftiene un orden de magnitud g, festé en el orden de g, 0 las dos funciones a la larga crecen a la misma velocidad. Vemos que f(n) es @(g(1)) si f(n) es Otg4n)) yfln) es gn). La dnica funcion que acabamos de listar que es tanto de orden © como de orden de la funcién 2.2 es n°, Sin embargo, no es la tinica opci6n, y todavia existe un nimero infinito de opciones ya que las funciones 2n?, 3n?, 4nz, ... también son @ de la funcién 2.2. Pero es muy obvio que se elegird la ms simple, 1, Cuando aplique cualquiera de estas notaciones (orden O, 2. y @), no olvide que son aproximaciones que ocultan algunos detalles que en muchos casos pueden considerarse importantes. PROBLEMAS POSIBLES ‘Todas las notaciones sirven para el propésito de comparar la eficiencia de varios algorit- mos disefados para resolver el mismo problema. No obstante, si sélo se utiliza la de orden O para representar la eficiencia de los algoritmos, entonces algunos de ellos pueden rechazarse de manera prematura, El problema es que en la definicién de la notacion de orden O, f se considera O(g(n)) si la desigualdad f(n) < cg(n) se cumple a Ja larga para todos los ntimeros naturales con unas cuantas excepciones. El nimero de n ‘que Viola esta desigualdad siempre es finito. Basta con cumplir la condicion de la defini- ci6n, Como indica la figura 2.2, este niimero de excepciones puede reducirse al elegir una ¢ lo suficientemente grande. Sin embargo, esto puede tener muy poca trascendencia prictica sila constante c en f(n) $ eg(n) es demasiado grande, digamos 10*, aun cuando la funcién gen si misma parece ser prometedora. Considere que existen dos algoritmos para resolver cierto problema y suponga que el niimero de operaciones requerido por estos algoritmos es 10% y 10n?. La primera funcién es O(n) y la segunda es O(n). Utilizando solamente la informacién del orden , el segundo algoritmo se rechaza porque el n~imero de pasos aumenta demasiado ripido. Es cierto pero, de nuevo, ala larga, debido a que para n $107, que es 10 millones, el segundo algoritmo realiza menos operaciones que el primero. Aun cuando 10 mi- ones no ¢s un ntimero insblito de elementos a procesar por un algoritmo, en muchos casos el ntimero es mucho menor, por lo que es preferible el segundo algoritmo, Por estas razones, puede resultar atractivo utilizar una notacién més que incluye constantes muy grandes por razones précticas. Udi Manber propone una notacién de doble © (00) para denotar estas funciones: fes OO(¢(n)) si es Of¢(n)) y la constante € es demasiado grande para tener una relevancia préctica. Por lo tanto, 10° es OO(n). Sin embargo, la definicién “demasiado larga” depende de la aplicacién particular. EJEMPLOS DE COMPLEJIDADES Los algoritmos pueden clasificarse segiin las complejidades de tiempo y espacio, y a este respecto pueden distinguirse varias clases de algoritmos, como lo ilustra la figura 2.4. Su crecimiento también se muestra en la figura 2.5. Por ejemplo, un algoritmo se llama constante si su tiempo de ejecucién sigue siendo el mismo para cualquier mimero de elementos; se lama cuadratico si su tiempo de ejecucidn es O(n"). Para cada una de estas, Seccién 2.6 Ejemplos de complejidades Ml 63 FIGURA 2.4 Clases de algoritmos y sus tiempos de ejecucién en una computadora que realiza un millon de operaciones por segundo (1 s = 108 ys = 10% ms). Clase —-—Complejidad~—_Nimero de operaciones y tiempo de ejecucién (1 instr/ys) a 0 w w constante —O(1) 1 1s 1 Ips 1 Ips logaritmica Ollgn) 3.32, 3s 664 7s 997 10 ps lineal O(n) 10 10 ps 10 100 ys 10° Ims O(nign) —Olngn) 33.233 ps 664 664s 9970 10ms cuadritica O(n?) 10 100 ps Jo! 10ms 10" Is cubica ln?) 10 ims ws 1 167 min ‘exponencial O(2") 1024 10ms 10" 3.17710 anos_10%" a 0 w we constante O(1) 1 Ips 1 Ips 1 1s logaritmica O(lgn) 13.313 ps 166 7s 1993 20s lineal On) 108 10ms 1 Os 10° Is O(nlgn) — Olnign) 133 10" 133 ms 166 *10* 16s 199.34 10° 205 cuadritica O(n?) 10" L7 min 10 16.7 min 10" 11.6 dias cbica ——Ofn?) 10% 1.6dias_—— 1031.7 aftos 10" 31 709 anos ‘exponencial 0(2*) 19" 19 19” FIGURA 2.5 Funciones tipicas aplicadas en las estimaciones de orden O. 64 M Capitulo 2 Andlisis de la complejidad return - 77 Ya celda ocupada por 1a lave; // falla: 1a llave no est en el arreglo; Si key esti en medio del arreglo, el ciclo se ejecuta s6lo una vez. ;Cudntas veces se ejecuta el ciclo si key no esti en el arreglo? El algoritmo primero considera el arreglo entero de tamafio 1, luego una de sus mitades de tamaito después una de las mitades de esta mitad, de tamano —, y asi sucesivamente hasta que el arreglo mide 1. Por lo tanto, tenemos la secuencia n, %, +» ¥ queremos saber el valor de m. Pero el til- timo término de esta secuencia es igual a 1, a partir de lo cual tenemos m = Ig n. Ast que el hecho de que k no esté eft el arreglo puede determinarse después de Ig m itera- iones del ciclo. EL MEJOR CASO, EL PEOR CASO Y EL CASO PROMEDIO Los iiltimos dos ejemplos de la seccién anterior indican la necesidad de distinguir cuando menos tres casos para los que debe determinarse la eficiencia de los algoritmos. El peor caso es cuando un algoritmo requiere un ntimero maximo de pasos, y el mejor caso es cuando se requiere el menor miimero de pasos. El caso promedio cae dentro de estos dos extremos. En los casos simples, la complejidad promedio se establece al con- siderar entradas posibles a un algoritmo, determinando el niimero de pasos realizados por el algoritmo para cada entrada, sumando el mimero de pasos para todas las entradas y dividiendo entre el niimero de entradas. Esta definicién, no obstante, supone {que la probabilidad de la ocurrencia de cada entrada es la misma, lo cual no siempre es 1 caso, Para considerar la probabilidad de manera explicita, la complejidad promedio se define como el nimero de pasos ejecutados cuando cada entrada se pondera por medio de la probabilidad de la ocurrencia de esta entrada, 0 Coon = &,PCinput,)steps(input,) Esta es la definiciGn del valor esperado, que asume la determinacién de todas las posi- bilidades y que la distribucién de la probabilidad es conocida, lo que simplemente de- termina una probabilidad de ocurrencia de cada entrada, p(input). La funcién de Seccion 2.8 E! mejor case, el peor casoy el caso promedio @ 67 probabilidad p satisface dos condiciones: nunca es negativa, p(input,) 2 0y todas as pro- babilidades suman hasta 1, ¥,p(input,) = 1. Como ejemplo, considere realizar una bdsqueda en secuencia en un arreglo sin or- denar para encontrar un mimero. El mejor caso es cuando el nfimero se encuentra en la primera celda. E1 peor es cuando el niimero esta en la iltima celda 0 no esta en el arreglo. Cuando esto ocurre, todas las celdas se marcan para determinar este hecho. ;Y el caso promedio? Podemos suponer que existe una oportunidad igual para el ntimero que se encontrar en cualquier celda del arreglo; es deci, a distribucion dela probabilidad es uniforme. En este caso, hay una probabilidad igual a 4 de que el mimero esté en la primera celda, una probabilidad igual a + de que esté eit la segunda celda, ...,y final- mente, una probabiidad igual a de que été en la htima, la ceda n sto significa que la probabidad de encontrar el nimero después de un intento es igual a la probabilidad de encontrarlo en dos intentos es igual a ,...,yla probabilidad de encontrarlo en n in- tentos es igual a! Por consiguiente, odefnos promeiar todos estos niimeros de intentos posibles con el niimero de posibilidades y concluir que en promedio se requieren L+2t-4n ” pasos para encontrar un niimero. Pero si las probabilidades difieren, entonces el caso promedio arroja un resultado diferente. Por ejemplo, sila probabilidad de encontrar un numero en la primera cela es igual a4 la probabiidad de encontraro en In segunda celda es igual a +, yla probabilidad de localizarlo en cualquiera de las celdas restantes es Ja misma y es igual a n(n+1)-6 | nt3 + ena = tg pasos para encontrar un mimero, lo cual ¢s aproximadamente cuatro veces mejor que el =! encontrado antes para la distribucién uniforme, Observe que las probabilidades de te- ner acceso a una celda en particular no tienen ningiin impacto en el peor caso ni en el mejor de los casos. La complejidad de los tres casos fue relativamente facil de determinar para la biis- queda secuencial, pero por lo general no es tan sencilla. En particular, la complejidad del caso promedio puede plantear problemas computacionales dificiles. Sielcilculo es muy complejo, se utilizan las aproximaciones y ahi es donde resultan mas atiles las notacio- nes del orden 0, Qy @. Como ejemplo, considere el caso promedio de la bisqueda binaria, Suponga que el ta mafio del arreglo es una potencia de 2 y que un niimero que se va a buscar tiene una opor- tunidad igual de estar en cualquiera de las celdas del arreglo. La busqueda binaria puede localizarlo ya sea después de un intento en la mitad del arreglo; después de dos intentos en la mitad de la primera mitad del arreglo, o de dos intentos en la segunda mitad del mismo; después de tres intentos en la mitad del primer cuarto del arreglo,.. después de tres inten- tos en la mitad del cuarto del arreglo; después de cuatro intentos en la mitad del primer 88 WM Capitulo 2 Andlisis de la complejidad octavo del arreglo, ode cuatro intentos en la mitad de la octava parte de un octavo del arre- glo,o.... después de intentar con lg nen la primera celda, o después de probar con Ig nen Ja tercera celda....o, finalmente, después de intentar con Ig n en la iltima celda. Es decir, el niimero de todos los intentos posibles es igual a } 1.142-244-348. seoatign= S2G +0 2 io Jo cual debe dividirse entre + para determinar la complejidad del caso promedio. ;A qué es igual esta suma? Sabemos que esté entre I (el resultado del mejor caso) y Ig (el peor caso) segiin se determiné en la seccién anterior. Pero seth mis cerca del mejor caso —digamos, lg lg n— 0 del peor caso —por ejemplo, *, 0 lg 4? La suma no se presta a si misma para una simple conversién hacia una forma cerrada; por lo tanto, debe usarse su estimacién. Nuestra conjetura és que la suma no es menor que la suma de potencias de 2 en el intervalo especificado multiplicada por una mitad de lg m, ¢s deci -Srosnate So = La raz6n para esta opci6n es que s; ¢s una serie de potencias multiplicada por un factor constante, y por lo tanto, puede presentarse en forma cerrada muy ficilmente,en concreto, et aol SY tery 4 22h | _ Weng _y 2a? zy 2 Jo cual es (11 Ig n). Debido a que s, es el limite inferior para la suma s, bajo escrutinio —es decir, s, €5 Q(s,)— entonces * también es el limite inferior de la complejidad bus- cada del caso promedio *t —es decir, #1 = Q().Como * es Q(lg n), entonces debe ser 42, Como Ign es una evaluacién de la complejidad del peot caso, la complejidad del caso Promedio es igual a O(lg n). ‘Atin queda un problema por resolver: js, 2 5,? Para determinar esto, conjeturamos que la suma de cada par de términos posicionados en forma simétrica con respecto al centro de la suma s, no es menor que la suma de los términos correspondientes de s, es decir, 29-1428" Ign 220 BR yak! Co 2-242 (lgn= 122! BE gg et Ign, G+ 42 ign —j)22 ‘et geen) A donde j = 8" — 1, La tiltima desigualdad, la cual representa a todas las demas desigual- dades, se transforma a Seccion 2.9 Complejidad amortizada M69 en ( i -i}pa(t2--) yluegoa den get yh (2.5) ‘Todas estas transformaciones estin permitidas debido a que los términos que se movieron de un lado de la desigualdad conjeturada al otro, son no negativos y por Jo tanto no cambian la direccion de la desigualdad. jLa desigualdad es verdadera? Debido a que j = le ~ 1,2#"-'-2/22,y el lado derecho de la desigualdad (2.5) siempre es menor que 1, la desigualdad conjeturada es verdadera. Esto concluye nuestra investigacion sobre el caso promedio para una biisqueda binaria. El algoritmo es relativamente sencillo, pero el proceso de encontrar la complejidad para el caso promedio es bastante engorroso, incluso para distribuciones uniformes de la probabili- dad, Para algoritmos més complejos, estos cilculos implican un reto mucho mayor. COMPLEJIDAD AMORTIZADA En muchas situaciones, las estructuras de datos estan sujetas @ una secuencia de opera- ciones en vez de estarlo a una sola. En esta secuencia, una operacién posiblemente realiza ciertas modificaciones que tienen un impacto en el tiempo de ejecucion de la siguiente operacién dentro de la secuencia, Una manera de evaluar el peor caso es afadir las cficiencias para cada operacién. Pero esto puede resultar en un limite excesivamente grande ¢ irreal en el tiempo de ejecucién actual. Para ser més realistas, se utiliza el andli- sis amortizado con el fin de encontrar la complejidad promedio de una secuencia de operaciones en el peor caso. Al analizar las secuencias de operaciones en vez de ‘operaciones aisladas, el anslisis amortizado toma en cuenta la interdependencia entre las operaciones y sus resultados. Por ejemplo, si un arreglo se ordena y s6lo se afiaden unos cuantos elementos nuevos, entonces la reordenacién de este arreglo debe ser mucho mas répida que su ordenacién la primera ver debido a que, después de las nuevas adiciones, el arreglo esta casi ordenado. Por lo tanto, debe ser mas répido poner todos los elementos en perfecto orden que en un arreglo completamente desorganizado. Sin tomar esta correlacién en cuenta, el tiempo de ejecucién de las dos operaciones de ‘ordenacién puede considerarse como el doble de la eficiencia del peor caso. El anslisis amortizado, por otra parte, decide que la segunda ordenacién casi no se aplica en la situacién del peor caso, asi que la complejidad combinada de las dos operaciones de “ordenacién es mucho menor que el doble de la complejidad del peor caso. En consecuen- , (cost(op,)+ potential( ds) potential(ds, ,)) i = > (cost(op,)+ potential(ds,) — potential(ds,)) Seccién 2.10 Completitud NP mM 73 En la mayoria de los casos, la funcién potencial es cero inicialmente y siempre ¢s no negativa de modo que el tiempo amortizado es un limite superior del tiempo real. Esta forma de costo amortizado se utiliza mas adelante en el libro. El costo amortizado de incluir nuevos elementos en un vector ahora puede expre- satse en términos de la funcién potencial definida como 0 si size, = capacity, (el véctor est lleno) potential( vector,)= 2size, capacity, en caso contrario Para ver que la funcién trabaja como se pretende, considere tres casos. El primer caso es cuando una insercién barata va después de una insercién barata (el vector no se extiende justo antes de la insercién actual y no se extiende como consecuencia de a insercién actual) y amCost( push) 3 1 + 2size,, + 2—capacity, , — 2size, , + capaci ‘como la capacidad no cambia, size, = size, , + 1,y el costo actual es igual a 1. Para una in- sercién cara después de una insercién barata, amCost{push,()) = ize, ,+2+0—2size, , + capacity, debido a que size, , + 1 = capacity, , y el costo actual es igual a size, + 1 = size, + 2,10 cual ¢s el costo de copiar los elementos del vector y aftadir el nuevo elemento. Para una insercién barata que va después de una insercién cara, =3 ‘amCost(push,()) = 1+ 2size,— capacity, — dado que 2(size,~ 1) = capacity, y el costo actual es igual a 1. Observe el cuarto caso, la insercién cara que va después de la insercién cara, ocurre slo dos veces, cuando la capaci- dad cambia de cero a uno y de uno a cero. En ambos casos, el costo amortizado es igual a 3. CompLeTiTup NP Un algoritmo determinista es una secuencia de pasos definida (determinada) de manera exclusiva para una entrada en particular; es decir, dada una entrada y un paso durante la ejecucién del algoritmo, s6lo hay una manera de determinar el siguiente paso que el al- goritmo puede dar, Un algoritmo no determinista es un algoritmo que puede utilizar tuna operacién especial que realiza un célculo cuando debe tomarse una decision. Con- sidere la versi6n no determinista de la biisqueda binaria. Si tratamos de localizar el mimeo k en un arreglo de nuimeros sin ordenar, en- tonces el algoritmo primero accede al elemento medio m del arreglo. Si m = k, entonces el algoritmo regresa a la posicién de m; de lo contrario, el algoritmo hace un célculo respecto a qué camino debe seguir: ala izquierda de mo a su derecha. Una decisién pa- recida se toma en cada etapa: si el ntimero k no se localiza, continia la biisqueda en una de las dos mitades del subarreglo que se estd examinando actualmente. Es facil ver que un célculo como éste nos puede llevar por un mal camino con mucha facilidad, asi que necesitamos dotar a la maquina con la capacidad de hacer célculos correctos. Sin embargo, una implementacién de este algoritmo no determinista tendria que probar, en el peor de los casos, todas las posibilidades. Una manera de hacerlo es al exigir que la decision en cada iteracién sea en realidad ésta: si m # k, entonces va tanto a la derecha como a la izquierda de m, De esta manera, se crea un érbol que representa las decisiones tomadas por el algoritmo (Johnson & Papadimitriou, pig. 53). El algoritmo resuelve el a You have either reached 2 page thts unevalale fer vowing or reached your ievina tit for his book. Seccién 2.10 Completitud NP Ml 75 El proceso de reduccién utiliza un problema NP-completo para mostrar que otro problema también es NP-completo. Sin embargo, debe haber cuando menos un pro- blema NP-completo probado directamente por otros medios distintos a la reduccién para hacer posible el proceso de reduccién. Un problema que Stephen Cook demostré que entraba en esa categoria es el problema dela satisfacibilidad., El problema de la satisfacibilidad se refiere a expresiones booleanas en forma nor- mal conjuntiva (CNF: conjunctive normal form). Una expresién esté en CNF si es una conjuncién de alternativas donde cada alternativa implica variables booleanas y sus ne- gaciones, y cada variable es ya sea verdadera o falsa, Por ejemplo, @vyvaAwyrvayvaAGwyoy) esti en CNF. Una expresion booleana es satisfacible si existe un argumento de valores ver- daderos y falsos que interpreta toda la expresion como verdadera. Por ejemplo, nuestra ex- presién es satisfacible para x= falso, y= falso y z= verdadero. El problema de satifacibilidad consiste en determinar si una expresién booleana es satisfacible (no deben darse asigna- ciones de valor). El problema es NP, debido a que las asignaciones pueden caleularse y des- pués ala expresién se le pueden aplicar pruebas de satisfacibilidad en tiempo polinomial. ‘Cook demuestra que el problema de satisfacibilidad es NP-completo al utilizar un con- cepto teGrico de la maquina de Turing que puede realizar decisiones no deterministas (hacer ‘buenos cficulos). Las operaciones de esa maquina luego se describen en términos de las expresiones booleanas, y se demuestra que la expresién es satisfacible si y s6lo sila maquina de Turing termina para una entrada en particular (para la prueba, véase el apéndice B). Para ilustrar el proceso de reduccién, considere el problema de satisfacibilidad de tres variables, el cual se presenta cuando cada alternativa en una expresién booleana en CNF incluye s6lo tres variables distintas. Afirmamos que el problema es NP-completo. El problema es NP, debido a que una asignacidn calculada de valores verdaderos para las variables en una expresién booleana puede verificarse en tiempo polinomial. Mostramos que el problema de satisfacibilidad de tres variables es NP-completo al reducirlo a un problema de satisfacibilidad. El proceso de reduccién implica mostrar que una alterna- tiva con cualquier ntimero de variables booleanas puede convertirse en una conjuncién de alternativas, cada alternativa con tres variables booleanas tinicamente. Esto se hace al introducir nuevas variables. Considere una alternativa A=(,VP2V VP para k2 4 donde p, € {x, —x,}. Con las nuevas variables y,,. . A= PLVPV IDA PSV “1 VIIAPV “9 IIA (2 V 4 V Yes) A Par V PV Yea) Sila alternativa A ¢s satisfacible, entonces cuando menos un término p, es verdadero, ast ‘que los valores de las y, pueden elegirse de modo que A’ sea verdadera: sip, es verdadera, ‘entonces establecemos y, como verdaderas y las y;,,,..., j.5 restantes como falsas. A la inversa, si A’ es satisfacible, entonces cuando menos una p, debe ser ver- dadera, ya que si todas las p, son falsas, entonces la expresion A’= (false \ false \/ y,) (false > y, \V ¥,) /\ (false \V my, \V¥5) A+ J\ (false \V false \/ > y, ,) tiene el mismo valor de verdad que la expresion MACAVIDAGY VIDA. ACK) la cual no puede ser verdadera para ninguna eleccién de valores para y,, por lo tanto no es satisfacible. ¥,stransformamos A en HW Capitulo 2 Analisis de 1a complejidad EJERCICIOS Explique el significado de las expresiones siguientes: a. fines O(1). b. fimes@cn). & fln)esn™®, Suponiendo que fn) es O(g,(n)) y fla) €s Olg,(n)), demuestre la instrucciones si- guientes: a. filn) + flr) es Olmaxtg,(n).g,(m))). b. Siun ndmero k puede determinarse como aquel para todas las 1 > k, g,(r) Sg,(m), entonces O(¢,(1)) + O(g,(n)) ¢s O(g,(1)).. filed)" fn) es O(g, (1) * g,(n)) (regla del producto). 4. Oleg(n)) es Ofg(n)). €. cesO(l), ._Demuestre las instrucciones siguientes: ©"... es Or?) y de manera més general, D%,_, # es O(n"). ant ig n es On) pero an'/ig n no es O(n"). all + nignes O(n"). |. 2" es O(n!) yn! noes O(2"), 2m es O(2"). 2 no es O(2"). g 2¥ Hes O(n"). meen ee Haga las mismas suposiciones que en el ejercicio 2 y, por medio de contraejemplos, refute las instrucciones siguientes: a filn) fn) es Og (n) -g,(m)). be filmfiln) es Olg,(m/g,(m))- Encuentre las funcionesf,y f, tales que tanto f,(n) como f,(n) sean O(g(n), pero fi(n) noes O(f,). iEscierto que a sif(n) es O(g(n)),entonces 2%” es (26)? b. fl) + g(n) es @(min(fin) g(n)))? ce Bes O(2")? El algoritmo presentado en este capitulo para hallar la longitud del subarreglo més grande con los niimeros en orden creciente es ineficiente, debido a que no hay necesidad de continuar buscando otro arreglo sila longitud ya encontrada es mayor que la longitud del subarreglo que se va a analizar. Por lo tanto, si todo el arreglo ya esti en orden, podemos suspender la biisqueda de inmediato, convirtiendo el peor caso en el mejor aso, El cambio necesario radica en el ciclo externo, el cual ahora tiene una prueba més: Seccion 2.11 Ejercicios M77 for (i= 0, length = 1; i < nel 6& length < nesi; i++) 4Cusl es el peor caso ahora? La ineficiencia del peor caso sigue siendo O(r)? 8 Encuentrela complejidad de la funcién utilizada para encontrar el k-ésimo entero més pequefio en un arreglo de enteros sin ordenar int selectkth(int a(], int k, int n) { int i, J, mini, tmp; for (i= 0; i < ky itt) { mini = for (j = itl; 3 < ny j++) if (a{j} 9 Determine la complejidad de las siguientes implementaciones de algoritmos para sumar, multiplicar y transponer matrices nx m: for (i = 0; i a y | \ becera final A eli (a) (b) al cabecera final . Sees Los cuatro pasos se ejecutan por el método addToHead ( ) (figura 3.2). El método ejecuta los primeros tres pasos de manera indirecta al llamar al constructor Node (e1, head). El iltimo paso se ejecuta directamente en el método al asignar la direccién (referencia) del nodo creado recientemente a head. 1 3 Seccién 3.1 Listas ligadas simples Ml 87 EI método addtotiead( ) seftala un caso especial, en concreto, la insercién de un nodo nuevo en una lista ligada vacia. En una lista ligada vacia, tanto head como tail son nulos; por lo tanto, los dos se vuelven referencias al tinico nodo de la lista nueva. Cuando se hace una insercién en una lista no vacia,s6lo se necesita actualizar a head. El proceso de aftadir un nodo nuevo al final dela lista consta de cinco pasos. Se crea un nodo vacio (figura 3.5a). El campo info del nodo se inicializa en un entero el (figura 3.5b). Debido a que el nodo se incluye al final de la lista, el campo next se establece como nulo (figura 3.5¢). El nodo ahora se incluye en la lista al convertir el campo next del iltimo nodo de la lista en una referencia al nodo recién creado (figura 3.54), El nodo nuevo va después de todos los nodos de a lista, pero este hecho debe reflejarse en el valor de tai, que ahora se vuelve la referencia al nodo nuevo (figura 3.5e). FIGURA 3.5 Insercién de un nodo nuevo al final de una lista ligada simple. cabecera final Sy | @ 1 cabecera final Os ones | 3 | PA a 10. ‘cabecera. final SCs 4S AG 10 © ; 7 cabecera @ cabgoera © “t34 Todos estos pasos se ejecutan en la clusula del método addtorail() (figura 3.2). La cliusula eise de este método se ejecuta s6lo si la lista ligada esta vacia. Si este caso no se incluyera, el programa se colapsaria porque en la cldusula i¢ hacemos una asignacién al campo next del nodo referido por tail. En el caso de una lista ligada vacfa, es una referencia a un campo inexistente de un nodo inexistente, lo cual conduce a incrementar Nul 1PointerException. 88 M Capitulo 3 Listas ligadas El proceso de insertar un nodo nuevo al principio de una lista es muy parecido al proceso de insertar un nodo al final de la lista. Esto se debe a que la implementacién de IntSLList utiliza dos campos de referencia: head y tail. Por esta raz6n, tanto .d() como addToTail() pueden ejecutarse en el tiempo constante O(1); es decir, sin importar cual sea el ntimero de nodos en la lista, el ntimero de operaciones realizadas por estos dos métodos no excede un nimero constante ¢ Observe que debido a que la referencia head nos permite tener acceso a una lista ligada, la referencia ‘tail no es indispensable; su tinica funcién es tener acceso inmediato al tiltimo nodo de la lista. Con este acceso, puede afiadirse un nodo nuevo fécilmente al final de la lista. Pero como lo ilustra una implementacién de lista ligada de stList en la figura 3.9 posteriormente en este capitulo, la referencia tail no tiene que utilizarse. En este caso, afiadir un nodo nuevo al final de la lista es més complicado debido a que primero tenemos que llegar al diltimo nodo con el fin de aftadirle un nodo nuevo. Esto implica explorar la lista y requiere O(n) pasos para completarse; es decir, es linealmente proporcional ala longitud de la lista. El proceso de explorar las listas se ilustra cuando se explica la eliminacién del iltimo nodo. 3.1.2. Eliminacién Una operacién de eliminacién consiste en eliminar un nodo al principio de la lista y regresar el valor almacenado en él. Esta operacién se implementa mediante el método deleteFromiead( ). En esta operacién la informacién del primer nodo se almacena temporalmente en una variable local e1, y luego head se reinicializa de modo que el segundo nodo se vuelve el primer nodo. De esta manera, el primer nodo anterior se abandona para ser procesado posteriormente por el colector de basura (figura 3.6). Ob- serve que el primer nodo anterior atin accede a la lista ligada, pero el nodo en si mismo es inaccesible. Por lo tanto, se considera inexistente. Debido a que el nodo head es accesible de inmediato, deleteFromHead( ) toma el tiempo constante O(1) para realizar su tarea. FIGURA 3.6 Eliminacion de un nodo que esta al principio de una lista ligada ccabecera final ‘SS (@) (b) A diferencia de lo anterior, ahora hay dos casos especiales a considerar. Un caso es cuando intentamos eliminar un nodo desde una lista ligada vacia. Si se hace un intento como éste, el programa se colapsa debido a Nul 1PointerExcept ion, lo cual no queremos que ocurra. El invocador también debe saber que se hizo un intento de realizar cierta accién. Después de todo, si el invocador espera que la llamada a You have either reached 2 page thts unevalale fer vowing or reached your ievina tit for his book. 90M Capitulo 3 Listas ligadas Si se afiade una instruccién if, entonces la cliusula else también debe aftadirse; de lo contrario, el programa no compila debido a la “instruccién de regreso faltante”, Pero ahora, si se regresa 0, el invocador no sabe si el 0 devuelto es la sefal de fala o si es una literal 0 recuperada de la lista. Para evitar cualquier confusién, el invocador debe usar una instruccién i £ para probar sila lista esté vacia antes de llamar adeleteProntiead ). De esta manera, la prueba dentro del método se vuelve redundante. El segundo caso especial en deleteF romHead ) es cuando la lista tiene s6lo un nodo a eliminar. En este caso, la lista se vuelve vacia, lo que requiere establecer head y tail como nulos, La segunda operacién de eliminacién consiste en eliminar un nodo del final de la lista, y se implementa como un método deletePromtai(). El problema radica en ‘que después de eliminar un nodo, tail debe referirse al nuevo tltimo elemento de la lista;es decir, tail debe moverse hacia atrés un nodo. Pero el movimiento hacia atras es imposible debido a que no hay un enlace directo desde el dltimo nodo a su predecesor. Por consiguiente, este predecesor debe encontrarse al buscar desde el principio de la lista y detenerse justo antes de tai. Esto se logra con una variable temporal tmp que explora la lista dentro del ciclo for. La variable tmp se inicializa como la cabecera de la lista, y luego en cada iteracién del ciclo avanza al nodo siguiente. Si la lista es como FIGURA 3.7 Eliminacién de un nodo desde el final de una lista ligada simple. © 4 4a final 3 \ ae i (es = © SS cabecera ‘mp fna o E16 Hl 4 EYE cabecera eo Ts) Seccién 3.1 Listas ligadas simples MM 91 aquella de la figura 3.7a, entonces tmp primero se refiere al nodo de cabecera que contiene al niimero 6; después de ejecutar la asignacién tmp = tmp.next, tmp se tefiere al segundo nodo (figura 3.7). Después de la segunda iteracién y ejecutar la misma asignaci6n, tmp se refiere al tercer nodo (figura 3,7¢). Como este nodo también std al lado del tltimo nodo, se sale del ciclo, después de lo cual tail se vuelve la refe- rencia al siguiente dltimo nodo (figura 3.74), y luego el campo next: al lado de este nodo se establece como nulo (figura 3.7e). Después de la ultima asignacién, el que fue el Ultimo nodo ahora se quita de la lista y queda inaccesible desde la misma. A su debido tiempo, este nodo es reclamado por el colector de basura, Observe que en el ciclo for, se utiliz6 una variable temporal para examinar la lista, Sil ciclo se simplificara a for ( ; head.next != tail; head = head.next); entonces la lista se examinaria sélo una vez, y el acceso al principio de la lista se pierde debido a que head se actualizé de manera permanente al nodo que esta al lado del iiltimo nodo, mismo que esta a punto de convertirse en el tiltimo nodo. Es de funda- ‘mental importancia que, en casos como éste, se utilice una variable temporal de modo que el acceso al principio de la lista se mantenga intacto. Al liminar el ultimo nodo, los dos casos especiales son los mismos que en deleteFromHead ). Sila lista esta vacia, entonces nada se puede eliminar; lo que debe hacerse en este caso se decide en el programa del usuario precisamente como en el caso de deleteFrontiead( ). Fl segundo caso es cuando una lista de un solo nodo se vuelve vacia después de eliminar este tinico nodo, lo que también requiere establecer head y tail como nulos. La parte que més consume tiempo de deleteFromrail() es la biisqueda del nodo al lado del ultimo nodo realizada por el ciclo for. Es evidente que el ciclo realiza n~ 1 iteraciones en una lista de nodos, lo que es la raz6n principal de que este método requiera un tiempo de O(n) para eliminar el ultimo nodo. Las dos operaciones de eliminacién analizadas eliminan un nodo de cabecera o del final (es decir, siempre de la misma posicién) y regresan el entero que por casualidad esté en el nodo que se esta eliminando. Un método diferente se utiliza cuando queremos climinar un nodo que guarda un entero en particular sin importar cual es la posicién de este nodo dentro de la lista. Puede estar justo al principio, al final o en cualquier parte de la misma, En suma, un nodo primero debe localizarse y luego quitarse de la lista al ligar el predecesor de este nodo directamente con su sucesor. Como no sabemos dénde esti el nodo, el proceso de encontrar y eliminar un nodo con cierto entero es mucho mas complejo que las operaciones de eliminacién estudiadas hasta ahora. El método delete ( ) (figura 3.2) es una implementacién de este proceso. Un nodo se elimina desde el interior de una lista al ligar su predecesor con su suce- sor. Pero como la lista tiene enlaces sélo hacia adelante, no es posible legar al predecesor de un nodo desde el nodo. Una manera de realizar la tarea es encontrar el nodo que s¢ va aeliminar, al examinar primero la lista y luego examinarla de nuevo para encontrar a su. predecesor. Otra forma se presenta en delete( ), como se muestra en la figura 3.8. ‘Suponga que queremos eliminar un nodo que contiene al riimero 8. El método utiliza dos variables de referencia, pred y tmp, las cuales se inicializan en el ciclo for de modo que apuntan al primero y segundo nodos de la lista, respectivamente (figura 3.8a). Como el nodo tmp tiene el mimero 5, se ejecuta la primera iteracién en la que tanto pred como tmp se mueven a Jos nodos siguientes (figura 3.8b). Dado que la condicion del ciclo for ahora es verdad (tmp sefiala al nodo con 8), termina el ciclo y se ejecuta una 82 WM Capitulo 3 Listas ligadas FIGURA 3.8 L Eliminacién de un nodo desde una lista ligada simple. final “ e . 5 3 . re cabecera_pred ___tmp final o Vee eS a fine 7B SCs 7] asignaci6n pred.next = tmp.next (figura 3.8c). Esta asignacién realmente excluye al nodo con 8 dela lista. Aunque el nodo atin esté accesible desde la variable tmp, no seré accesible después de salir del método delete() debido a que tmp es local para el método. Por consiguiente, deja de existir después de salir de este método. Como antes, el nodo con el niimero 8 ahora inaccesible ser procesado por el colector de basura. El parrafo anterior contemplé solamente un caso. En seguida se presentan los casos restantes: Un intento por eliminar un nodo desde una lista vacia, en cuyo caso el método termina de inmediato. Eliminar el ‘inico nodo desde una lista ligada de un solo nodo: tanto head como tail se establecen como nulos. Eliminar el primer nodo de a lista con al menos dos nodos, lo cual requiere actualizar head. Eliminar el timo nodo de la lista con al menos dos nodos; lo cual conduce ala actuali- zacion de tail. Un intento de eliminar un nodo con un nimero que no esta en la lista: no hace nada. Es obvio que el mejor caso para delete( ) es cuando se va a eliminar el nodo de cabecera, lo que requiere un tiempo O(1) para realizarse. El peor caso es cuando el ‘ultimo nodo debe eliminarse, lo que reduce delete() a deleteFromTail() y a su rendimiento de O(n). ;Cudl es caso promedio? Depende de cudntas iteraciones ejecuta el ciclo for. Suponiendo que cualquier nodo de la lista tiene una oportunidad igual de eliminarse, el ciclo no realiza ninguna iteracién si es el primer nodo, realiza una itera- ci6n sies el segundo nodo,....,y finalmente n — 1 iteraciones si es el tltimo nodo, Para una secuencia larga de eliminaciones, una eliminacién requiere en promedio a You have either reached 2 page thts unevalale fer vowing or reached your ievina tit for his book. $4 M Capitulo 3 Listas ligadas FIGURA 3.9 —_implementacion de una lista ligada simple genérica. Tha ses SLLNode.java **#*H public class SLLNode { public Object info; public SLLNode next; public SLINode() { next = null; } public SLLNode(Object el) { info = el; next = null; » public SLLNode(Object el, SLLNode ptr) { info = el; next = ptr; seseeeeseees SLList.java stteeeeeesesensereseeeesess de lista ligada simple genérica sélo con cabecera public class SLList { protected SLLNode head = null; public SLList() { > public boolean isEmpty() { return head == null; ¥ public Object first() { return head. info; , public void printAll(java.io.PrintStream out) ( for (SLLNode tmp = head; tmp != null; tmp = tmp.next) out.print (tmp. info); > Public void add(Object el) { head = new SLLNode(el,head); > Public Object find(object el) { SLLNode tmp = head; for ( ; tmp != null £6 Jel.equals(tmp.info); tmp = tmp.next); if (tmp == null) return null; else return tmp. info; Continda a You have either reached 2 page thts unevalale fer vowing or reached your ievina tit for his book. 96 WM Capitulo 3 Listas ligadas FIGURA 3.11 implementacién de una lista ligada doble. [tesenesnesesesanseses IntDLLNode. java public class IntDLLNode { public int info; public IntDLLNode next, prev; public IntDLLNode(int el) ¢ this(el,nul1,nul1); , public IntDLLNode(int el, IntDLLNode n, IntDLLNode p) { info = el; next = n; prev = p; , [saeestenaeeaeennessenees IntDLbist.java ttttteeseseereseeeeeseeeees/ public class IntDLbist ( private IntDLLNode head, tail; public IntDLList() { head = tail = null; } public boolean isEmpty() { return head == null; > public void addToTail(int el) { if (1isEmpty()) ¢ tail = new IntDLINode(el,null,tail); tail.prev.next = tail; > else head = tail = new IntDLLNode(el); > public int removeFromrail() { int el = tail.info; if (head == tail) // si s6lo hay un nodo en la lista; head = tail = null; else { 7/ si hay més de un nodo en 1a lista; tail = tail.prev; tail.next = null; Seccién 3.2 Listas ligadas dobles M97 Los métodos para procesar las istas ligadas dobles son ligeramente mas complica- dos que sus homlogos debido a que existe un campo de referencia mas por mantener. Sélo se estudian dos métodos: un método para insertar un nodo al final de una lista ligada doble, y un método para climinar un nodo desde el final (figura 3.11). Para afiadir un nodo a la lista, el nodo tiene que crearse, sus campos se inicializan de manera apropiada, y luego el nodo necesita incorporarse ala lista. La insercién de un nodo al final de una lista ligada doble se ilustra en la figura 3.12. El proceso se realiza en seis pasos: 1. Unnodo nuevo se crea (figura 3.12a), y luego sus tres campos se inicializan: 2. elcampo info en el niimero e1 que se esté insertando (figura 3.12), 3. elcamponext como nulo (figura 3.12c), FIGURA 3.12 — Adicién de un nodo nuevo al final de una lista ligada doble. ‘cabecera (a) (©) @ ©) 0

También podría gustarte