Permitiendo contracciones
Supongamos ahora una situación más compleja en la que también se pueden eliminar elementos en el arreglo. Un elemento eliminado se reemplaza por el que está en último lugar, de modo que el arreglo se almacene en forma compacta. Queremos evitar el estar almacenando demasiada memoria para un arreglo que alguna vez fue grande pero ahora tiene pocos elementos. Para ello, podemos contraer el arreglo cuando queda muy vacío después de un borrado. Esta es la acción contraria a la expansión que se realiza durante la inserción, y supondremos que también cuesta escrituras.
El primer impulso puede ser que, una vez que el arreglo tenga tamaño , lo contraigamos a tamaño . Sin embargo, si volvemos a insertar inmediatamente, el arreglo se volverá a expandir. Por ello, el costo de esta alternativa puede ser muy alto: cada operación costará escrituras si se elimina un elemento inmediatamente después de expandir, lo que provoca una contracción, luego se vuelve a insertar, lo que provoca otra expansión, luego se vuelve a eliminar, lo que provoca otra contracción, etc.
--> imagen
Para seguir teniendo costo amortizado constante, establecemos que el arreglo se contraerá solamente cuando , para cierto , y entonces se contraerá a tamaño , para un . Esto garantiza que la memoria máxima a ser usada es . Nos preguntamos ahora cuál es el costo amortizado de una operación.
Esta vez no funciona una función de tipo . Debemos elegir un esquema algo más complejo: . Tenemos ahora las siguientes operaciones:
- Inserción elemental, a costo . El valor de es a lo sumo , por lo que una cota superior al costo amortizado es .
- Borrado elemental (es decir, sin considerar la contracción), a costo . El valor de es también a lo sumo , por lo que tenemos la misma cota superior .
- Expansión, a costo . Esta ocurre cuando , y pasaremos de a . Para que nuestro esquema funcione, la expansión debe darse cuando , es decir, que debe valer . En este caso, tenemos , y tendremos siempre que .
- Contracción, a costo . Esta ocurre cuando , y pasaremos de a . Para que nuestro esquema funcione, la contracción debe darse cuando , es decir, que debe valer . En este caso, tenemos y , y entonces . Por lo tanto, tendremos siempre que .
Tenemos entonces las condiciones , y . Dado un fijo (relacionado con la memoria máxima que permitimos desperdiciar), queremos minimizar (pues el costo amortizado es ). El menor posible es . Usar el menor posible implica usar el menor posible, pues . Como nuestras desigualdades implican , elegimos . Las nuevas cotas inferiores son y , que decrecen y crecen, respectivamente, con . Por lo tanto, el óptimo se da cuando , es decir, . El costo amortizado es entonces .
Concluyendo, podemos garantizar un uso máximo de celdas para almacenar elementos, permitiendo inserciones y borrados, a un costo amortizado de por operación. Para lograrlo, cuando insertamos y , expandimos el arreglo a , y cuando borramos y , contraemos el arreglo a . Por ejemplo, podemos elegir para obtener un costo amortizado de .
--> imagen