quicksort - upcAnalisisAlgoritmos

Anuncio
`
QUICKSORT
JAMES GUARIN CARO
JAHIR ARDILA
GERMAN ORTEGON
DAVID GOMEZ
UNIVERSIDAD PILOTO DE COLOMBIA
INGENIERIA DE SISTEMAS
BOGOTA 2010
1
`
TABLA DE CONTENIDO
1. Descripción
2. Pseudocódigo
3. Optimizado
4. Análisis del algoritmo
5. Implementación en JAVA
6. Conclusión
7. Bibliografía
2
`
DESCRIPCIÓN
Esta es probablemente la técnica más rápida conocida. Fue desarrollada por C.A.R.
Hoare en 1960. El algoritmo original es recursivo, pero se utilizan versiones iterativas para
mejorar su rendimiento (los algoritmos recursivos son en general más lentos que los
iterativos, y consumen más recursos). El algoritmo fundamental es el siguiente:

Eliges
un
elemento
de
la
lista.
Puede
ser
cualquiera
(en Optimizando veremos una forma más efectiva). Lo llamaremos elemento
de división.

Buscas la posición que le corresponde en la lista ordenada (explicado más
abajo).

Acomodas los elementos de la lista a cada lado del elemento de división, de
manera que a un lado queden todos los menores que él y al otro los mayores
(explicado más abajo también). En este momento el elemento de división
separa la lista en dos sub-listas (de ahí su nombre).

Realizas esto de forma recursiva para cada sub-lista mientras éstas tengan
un largo mayor que 1. Una vez terminado este proceso todos los elementos
estarán ordenados.
Una idea preliminar para ubicar el elemento de división en su posición final sería contar la
cantidad de elementos menores y colocarlo un lugar más arriba. Pero luego habría que
mover todos estos elementos a la izquierda del elemento, para que se cumpla la
condición y pueda aplicarse la recursividad. Reflexionando un poco más se obtiene un
procedimiento mucho más efectivo. Se utilizan dos índices: i, al que llamaremos contador
por la izquierda, y j, al que llamaremos contador por la derecha. El algoritmo es éste:

Recorres la lista simultáneamente con i y j: por la izquierda con i (desde el
primer elemento), y por la derecha con j (desde el último elemento).

Cuando lista[i] sea mayor que el elemento de división y lista[j] sea menor los
intercambias.

Repites esto hasta que se crucen los índices.

El punto en que se cruzan los índices es la posición adecuada para colocar el
elemento de división, porque sabemos que a un lado los elementos son todos
menores y al otro son todos mayores (o habrían sido intercambiados).
Al finalizar este procedimiento el elemento de división queda en una posición en que
todos los elementos a su izquierda son menores que él, y los que están a su derecha son
mayores.
3
`
Pseudocódigo
Tabla de variables
Nombre
Tipo
lista
Cualquiera
inf
Entero
sup
Entero
El mismo que los elementos de la
elem_div
lista
El mismo que los elementos de la
temp
lista
Uso
Lista a ordenar
Elemento inferior de la lista
Elemento superior de la lista
El elemento divisor
Para realizar los intercambios
i
Entero
Contador por la izquierda
j
Entero
cont
Entero
Contador por la derecha
El ciclo continua mientras cont tenga el
valor 1
Nombre Procedimiento: ordenarQuicksort
Parámetros:
lista a ordenar (lista)
índice inferior (inf)
índice superior (sup)
// Inicialización de variables
1. elem_div = lista[sup];
2. i = inf - 1;
3. j = sup;
4. cont = 1;
// Verificamos que no se crucen los límites
5. if (inf >= sup)
6.
retornar;
// Clasificamos la sublista
7. while (cont)
8.
while (lista[++i] < elem_div);
9.
while (lista[--j] > elem_div);
10.
if (i < j)
11.
temp = lista[i];
12.
lista[i] = lista[j];
13.
lista[j] = temp;
4
`
14.
15.
else
cont = 0;
// Copiamos el elemento de división
// en su posición final
16. temp = lista[i];
17. lista[i] = lista[sup];
18. lista[sup] = temp;
// Aplicamos el procedimiento
// recursivamente a cada sublista
19. OrdRap (lista, inf, i - 1);
20. OrdRap (lista, i + 1, sup);
5
`
Optimizando.
Sólo voy a mencionar algunas optimizaciones que pueden mejorar bastante el
rendimiento de quicksort:

Hacer una versión iterativa: Para ello se utiliza una pila en que se van
guardando los límites superior e inferior de cada sub-lista.

No clasificar todas las sub-listas: Cuando el largo de las sub-listas va
disminuyendo, el proceso se va encareciendo. Para solucionarlo sólo se
clasifican las listas que tengan un largo menor que n. Al terminar la
clasificación se llama a otro algoritmo de ordenamiento que termine la labor.
El indicado es uno que se comporte bien con listas casi ordenadas, como el
ordenamiento por inserción por ejemplo. La elección de n depende de varios
factores, pero un valor entre 10 y 25 es adecuado.

Elección del elemento de división: Se elige desde un conjunto de tres
elementos: lista[inferior], lista[mitad] y lista[superior]. El elemento elegido es el
que tenga el valor medio según el criterio de comparación. Esto evita el
comportamiento degenerado cuando la lista está prácticamente ordenada.
6
`
Análisis del algoritmo.



Estabilidad: No es estable.
Requerimientos de Memoria: No requiere memoria adicional en su forma
recursiva. En su forma iterativa la necesita para la pila.
Tiempo de Ejecución:
o Caso promedio. La complejidad para dividir una lista de n es
O(n). Cada sub-lista genera en promedio dos sub-listas más de
largo n/2. Por lo tanto la complejidad se define en forma
recurrente como:
f(1) = 1
f(n) = n + 2 f(n/2)
La forma cerrada de esta expresión es:
f(n) = n log2n
Es decir, la complejidad es O(n log2n).
o El peor caso ocurre cuando la lista ya está ordenada, porque
cada llamada genera sólo una sub-lista (todos los elementos son
menores que el elemento de división). En este caso el
rendimiento se degrada a O(n2). Con las optimizaciones
mencionadas arriba puede evitarse este comportamiento.
Ventajas:

Muy rápido

No requiere memoria adicional.
Desventajas:

Implementación un poco más complicada.

Recursividad (utiliza muchos recursos).

Mucha diferencia entre el peor y el mejor caso.
La mayoría de los problemas de rendimiento se pueden solucionar con las optimizaciones
mencionadas arriba (al costo de complicar mucho más la implementación). Este es un
algoritmo que se emplea muy a menudo para ordenamientos.
7
`
Análisis del algoritmo
Entrada: La secuencia o arreglo a0, ..., an-1con n elementos
Salida:
Método:
Se cambian los elementos de la secuencia de tal manera que los elementos
a0, ..., aj son menores o iguales a todos los elementos donde ai, ..., an-1 (i > j)
elegir el elemento en el medio de la secuencia como elemento de comparación x
1. Tomando i = 0 and j = n-1
while i
j
1. Buscar el primer elemento ai el cual es mayor o igual que x
Buscar el último elemento aj que es menor o igual que x
if i
j
1. Intercambiar ai y aj
Hacer i = i+1 y j = j-1
Después de dividir la secuencia, quicksort trata a las dos partes de forma recursiva
mediante el mismo procedimiento. La recursión termina cuando una parte se compone de
un único elemento.
El tiempo total necesario para reordenar un arreglo es siempre O (n)^1, o αn en donde α
es una constante Supongamos que el pivote que acaba de elegir ha dividido al arreglo en
dos partes - una de tamaño k y la otra de tamaño n - k. hay que tener en cuenta que las
portes necesitan ser ordenadas.
Esto nos da la siguiente relación:
T(n) = T(1) + T(n − 1) + αn
ANÁLISIS DEL PEOR DE LOS CASOS
Ahora se analizara el caso, cuando el pivote resultó ser el menor elemento de la matriz,
de modo que tuvimos k = 1 y n - k = n - 1. En tal caso, tenemos:
T(n) = T(1) + T(n − 1) + αn
8
`
A continuación analizaremos el tiempo de complejidad de quicksort:
T(n) = T(n − 1) + T(1) + αn
= [T(n − 2) + T(1) + α (n − 1)] + T(1) + αn
= T(n − 2) + 2T(1) + (n − 1 + n) (simplificando y agrupando términos)
= [T(n − 3) + T(1) + α (n − 2)] + 2T(1) + α (n − 1 + n)
= T(n − 3) + 3T(1) + α (n − 2 + n − 1 + n)
= [T(n − 4) + T(1) + α (n − 3)] + 3T(1) + α (n − 2 + n − 1 + n)
= T(n − 4) + 4T(1) + α (n − 3 + n − 2 + n − 1 + n)
= T(n − i) + iT (1) + α (n − i + 1 + ..... + n − 2 + n − 1 + n) (y así sucesivamente hasta el
paso i-esimo)
=T(n − i) + iT (1) + α(
(n − j))
T(n) = T(1) + (n − 1)T(1) + α
= nT(1) + α(n(n − 2) − (n − 2)(n − 1)/2)
Observamos entonces
=
= (n − 2)(n − 1)/2 lo cual según teorema es O(n^2)
ANÁLISIS DEL MEJOR DE LOS CASOS
El mejor de los casos sucede cuando se divide el pivote que escogemos divide el arreglo
en dos partes iguales en cada paso Así pues, tenemos k = n / 2 y n-k = n / 2 de la matriz
original de tamaño n.
T(n) = 2T(n/2) + α n
= 2(2T(n/4) + α n/2) + α n
= 22T(n/4) + 2 α n (simplificando y agrupando términos semejantes).
= 22(2T(n/8) + α n/4) + 2 α n
= 23T(n/8) + 3 α n
= 2kT(n/2k) + k α n (continuando hasta el paso k- esimo)
Debemos tener en cuenta que esta recurrencia se mantendrá sólo hasta que n = 2^k (de
lo contrario tenemos n/2^k <1), es decir,
hasta k = log n. Así, al poner k = log n, tenemos la siguiente ecuación
T(n) = nT(1) + α n log n, el cual es O(n log n).
Este sería el mejor de los casos.
9
`
Implementación en Java
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
/*
* Principal.java
*
* Created on Oct 4, 2010, 7:25:38 PM
*/
package jahir;
import java.util.Random;
import javax.swing.*;
/**
*
* @author darkgayOn
*/
public class Principal extends javax.swing.JFrame {
/** Creates new form Principal */
private int vec[];
public Principal() {
initComponents();
this.setSize(500, 250);
ButtonGroup b = new ButtonGroup();
b.add(b1);
b.add(b2);
b.add(b3);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GENBEGIN:initComponents
10
`
private void initComponents() {
jPanel1 = new javax.swing.JPanel();
b1 = new javax.swing.JRadioButton();
b2 = new javax.swing.JRadioButton();
b3 = new javax.swing.JRadioButton();
jButton1 = new javax.swing.JButton();
jPanel2 = new javax.swing.JPanel();
jScrollPane1 = new javax.swing.JScrollPane();
s1 = new javax.swing.JTextArea();
jScrollPane2 = new javax.swing.JScrollPane();
s2 = new javax.swing.JTextArea();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
b1.setSelected(true);
b1.setText("10");
jPanel1.add(b1);
b2.setText("100");
jPanel1.add(b2);
b3.setText("1000");
jPanel1.add(b3);
jButton1.setText("Iniciar");
jButton1.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButton1ActionPerformed(evt);
}
});
jPanel1.add(jButton1);
getContentPane().add(jPanel1, java.awt.BorderLayout.PAGE_START);
jPanel2.setLayout(new java.awt.GridLayout());
s1.setColumns(20);
s1.setRows(5);
jScrollPane1.setViewportView(s1);
jPanel2.add(jScrollPane1);
s2.setColumns(20);
s2.setRows(5);
jScrollPane2.setViewportView(s2);
jPanel2.add(jScrollPane2);
getContentPane().add(jPanel2, java.awt.BorderLayout.CENTER);
11
`
pack();
}// </editor-fold>//GEN-END:initComponents
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GENFIRST:event_jButton1ActionPerformed
int n = 0;
if(b1.isSelected())
n = 10;
else if(b2.isSelected())
n = 100;
else
n = 1000;
this.vec = new int[n];
Random r = new Random();
for(int x=0;x<n;x++)
{
this.vec[x] = r.nextInt(1000);
}
this.p(s1);
double t1 = System.nanoTime();
ordenarQuicksort(this.vec,0,this.vec.length-1);
double t2 = System.nanoTime();
this.p(s2);
JOptionPane.showMessageDialog(this, "Se demoro: " + (t2-t1) + " Nano segundos");
}//GEN-LAST:event_jButton1ActionPerformed
void ordenarQuicksort(int[] vector, int primero, int ultimo){
int i=primero, j=ultimo;
int pivote=vector[(primero + ultimo) / 2];
int auxiliar;
do{
while(vector[i]<pivote) i++;
while(vector[j]>pivote) j--;
if (i<=j){
auxiliar=vector[j];
vector[j]=vector[i];
vector[i]=auxiliar;
i++;
j--;
}
12
`
} while (i<=j);
if(primero<j) ordenarQuicksort(vector,primero, j);
if(ultimo>i) ordenarQuicksort(vector,i, ultimo);
}
public void p(JTextArea s)
{
s.setText("");
for(int x=0;x<this.vec.length;x++)
{
s.append("\n" + this.vec[x]);
}
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new Principal().setVisible(true);
}
});
}
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JRadioButton b1;
private javax.swing.JRadioButton b2;
private javax.swing.JRadioButton b3;
private javax.swing.JButton jButton1;
private javax.swing.JPanel jPanel1;
private javax.swing.JPanel jPanel2;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JScrollPane jScrollPane2;
private javax.swing.JTextArea s1;
private javax.swing.JTextArea s2;
// End of variables declaration//GEN-END:variables
}
13
`
CONCLUSIONES
RENDIMIENTO DEL QUICKSORT
1400000
1200000
timpo (nano seg)
1000000
800000
600000
Series1
400000
200000
0
-200
-200000
0
200
400
600
800
1000
1200
tama;o del vector
Analizamos que entre más grande el vector, el
método QUICKSORT se tarda más en su
procesamiento.
14
`
Bibliografía





http://www.iti.fh-flensburg.de/lang/algorithmen/sortieren/quick/quicken.htm
http://www.conclase.net/c/orden/?cap=quicksort
http://es.wikipedia.org/wiki/Quicksort
H.M. Deitel, P.J. Deitel: "Cómo programar en C/C++". Editorial Prentice Hall.
Charles Bowman: "Algoritmos y estructuras de datos: Aproximación en C".
15
Descargar