` 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