stringtranslate.com

Orden del tiempo

Timsort es un algoritmo de ordenamiento híbrido y estable , derivado del ordenamiento por fusión y el ordenamiento por inserción , diseñado para funcionar bien en muchos tipos de datos del mundo real. Fue implementado por Tim Peters en 2002 para su uso en el lenguaje de programación Python . El algoritmo encuentra subsecuencias de los datos que ya están ordenados (ejecuciones) y las usa para ordenar el resto de manera más eficiente. Esto se hace fusionando ejecuciones hasta que se cumplan ciertos criterios. Timsort ha sido el algoritmo de ordenamiento estándar de Python desde la versión 2.3 (desde la versión 3.11 usando la política de fusión Powersort [5] ), y se usa para ordenar matrices de tipo no primitivo en Java SE 7 , [6] en la plataforma Android , [7] en GNU Octave , [8] en V8 , [9] Swift , [10] e inspiró el algoritmo de ordenamiento usado en Rust . [11]

Utiliza técnicas del artículo de Peter McIlroy de 1993 "Optimistic Sorting and Information Theoretic Complexity" [12] .

Operación

Timsort fue diseñado para aprovechar las ejecuciones de elementos ordenados consecutivos que ya existen en la mayoría de los datos del mundo real, las ejecuciones naturales . Itera sobre los datos recopilando elementos en ejecuciones y colocando simultáneamente esas ejecuciones en una pila. Siempre que las ejecuciones en la parte superior de la pila coincidan con un criterio de fusión, se fusionan. Esto continúa hasta que se recorren todos los datos; luego, todas las ejecuciones se fusionan de dos en dos y solo queda una ejecución ordenada. La ventaja de fusionar ejecuciones ordenadas en lugar de fusionar sublistas de tamaño fijo (como se hace con la ordenación por fusión tradicional) es que disminuye la cantidad total de comparaciones necesarias para ordenar la lista completa.

Cada ejecución tiene un tamaño mínimo, que se basa en el tamaño de la entrada y se define al comienzo del algoritmo. Si una ejecución es más pequeña que este tamaño mínimo, se utiliza la ordenación por inserción para agregar más elementos a la ejecución hasta que se alcance el tamaño mínimo.

Criterios de fusión

Las ejecuciones se insertan en una pila . Si | Z | ≤ | Y | + | X | , entonces X e Y se fusionan y se reemplazan en la pila. De esta manera, la fusión continúa hasta que todas las ejecuciones satisfacen i. | Z | > | Y | + | X | y ii. | Y | > | X | .

Timsort es un algoritmo de clasificación estable (se mantiene el orden de los elementos con la misma clave) y busca realizar fusiones equilibradas (una fusión fusiona ejecuciones de tamaños similares).

Para lograr estabilidad de ordenación, solo se fusionan ejecuciones consecutivas. Entre dos ejecuciones no consecutivas, puede haber un elemento con la misma clave dentro de las ejecuciones. La fusión de esas dos ejecuciones cambiaría el orden de las claves iguales. Ejemplo de esta situación ([] son ​​ejecuciones ordenadas): [1 2 2] 1 4 2 [0 1 2]

En la búsqueda de fusiones equilibradas, Timsort considera tres ejecuciones en la parte superior de la pila, X , Y , Z , y mantiene las invariantes:

  1. | Z | > | Y | + | X |
  2. | Y | > | X | [13]

Si se viola alguna de estas invariantes, Y se fusiona con la menor de X o Z y las invariantes se verifican nuevamente. Una vez que las invariantes se cumplen, puede comenzar la búsqueda de una nueva ejecución en los datos. [14] Estas invariantes mantienen las fusiones como aproximadamente equilibradas mientras se mantiene un compromiso entre retrasar la fusión para lograr el equilibrio, explotar la nueva ocurrencia de ejecuciones en la memoria caché y hacer que las decisiones de fusión sean relativamente simples.

Fusionar espacio en la parte superior

Para fusionar, Timsort copia los elementos de la matriz más pequeña (X en esta ilustración) a la memoria temporal, luego ordena y llena los elementos en el orden final en el espacio combinado de X e Y.

La implementación original de la ordenación por combinación no está en el lugar y tiene una sobrecarga de espacio de N (tamaño de los datos). Existen implementaciones de ordenación por combinación en el lugar, pero tienen una sobrecarga de tiempo alta. Para lograr un término medio, Timsort realiza una ordenación por combinación con una pequeña sobrecarga de tiempo y una sobrecarga de espacio menor que N.

En primer lugar, Timsort realiza una búsqueda binaria para encontrar la ubicación donde se insertaría el primer elemento de la segunda ejecución en la primera ejecución ordenada, manteniéndola ordenada. Luego, realiza el mismo algoritmo para encontrar la ubicación donde se insertaría el último elemento de la primera ejecución en la segunda ejecución ordenada, manteniéndola ordenada. Los elementos antes y después de estas ubicaciones ya están en su lugar correcto y no necesitan ser fusionados. Luego, la ejecución reducida más pequeña se copia en la memoria temporal y los elementos copiados se fusionan con la ejecución reducida más grande en el espacio ahora libre. Si la ejecución reducida más a la izquierda es más pequeña, la fusión procede de izquierda a derecha. Si la ejecución reducida más a la derecha es más pequeña, la fusión procede de derecha a izquierda (es decir, comenzando con los elementos en los extremos del espacio temporal y la ejecución más a la izquierda, y llenando el espacio libre desde su final). Esta optimización reduce la cantidad de movimientos de elementos necesarios, el tiempo de ejecución y la sobrecarga de espacio temporal en el caso general.

Ejemplo: se deben fusionar dos ejecuciones [1, 2, 3, 6, 10] y [4, 5, 7, 9, 12, 14, 17]. Nótese que ambas ejecuciones ya están ordenadas individualmente. El elemento más pequeño de la segunda ejecución es 4 y se tendría que agregar en la cuarta posición de la primera ejecución para preservar su orden (asumiendo que la primera posición de una ejecución es 1). El elemento más grande de la primera ejecución es 10 y se tendría que agregar en la quinta posición de la segunda ejecución para preservar su orden. Por lo tanto, [1, 2, 3] y [12, 14, 17] ya están en sus posiciones finales y las ejecuciones en las que se requieren movimientos de elementos son [6, 10] y [4, 5, 7, 9]. Con este conocimiento, solo necesitamos asignar un buffer temporal de tamaño 2 en lugar de 4.

Dirección de fusión

La fusión se puede realizar en ambas direcciones: de izquierda a derecha, como en la fusión tradicional, o de derecha a izquierda.

Modo galope durante la fusión

Se comparan los elementos (señalados por la flecha azul) y el elemento más pequeño se mueve a su posición final (señalada por la flecha roja).

Una fusión individual de las ejecuciones R1 y R2 mantiene el recuento de elementos consecutivos seleccionados de una ejecución. Cuando este número alcanza el umbral de galope mínimo ( min_gallop ), Timsort considera que es probable que aún se puedan seleccionar muchos elementos consecutivos de esa ejecución y cambia al modo de galope. Supongamos que R1 es responsable de activarlo. En este modo, el algoritmo realiza una búsqueda en dos etapas para el lugar en la ejecución R1 donde se insertaría el siguiente elemento x de la ejecución R2. En la primera etapa realiza una búsqueda exponencial , también conocida como búsqueda galopante, hasta encontrar un k tal que R1[2 k−1 − 1] < x <= R1[2 k − 1], es decir, una región de incertidumbre que comprende 2 k−1 − 1 elementos consecutivos de R1. La segunda etapa realiza una búsqueda binaria directa de esta región para encontrar la ubicación exacta en R1 para x . El modo galopante es un intento de adaptar el algoritmo de fusión al patrón de intervalos entre elementos en las ejecuciones.

Todos los elementos rojos son más pequeños que los azules (aquí, 21). Por lo tanto, se pueden mover en un fragmento a la matriz final.

El galope no siempre es eficiente. En algunos casos, el modo de galope requiere más comparaciones que una simple búsqueda lineal . Según los puntos de referencia realizados por el desarrollador, el galope es beneficioso solo cuando el elemento inicial de una ejecución no es uno de los primeros siete elementos de la otra ejecución. Esto implica un umbral inicial de 7. Para evitar los inconvenientes del modo de galope, se toman dos acciones: (1) Cuando se descubre que el galope es menos eficiente que la búsqueda binaria , se sale del modo de galope. (2) El éxito o el fracaso del galope se utiliza para ajustar min_gallop . Si el elemento seleccionado es de la misma matriz que devolvió un elemento anteriormente, min_gallop se reduce en uno, lo que fomenta el regreso al modo de galope. De lo contrario, el valor se incrementa en uno, lo que desalienta el regreso al modo de galope. En el caso de datos aleatorios, el valor de min_gallop se vuelve tan grande que el modo de galope nunca se repite. [15]

Carreras descendentes

Para aprovechar también los datos ordenados en orden descendente, Timsort invierte las ejecuciones estrictamente descendentes cuando las encuentra y las agrega a la pila de ejecuciones. Dado que las ejecuciones descendentes se invierten luego ciegamente, excluir las ejecuciones con elementos iguales mantiene la estabilidad del algoritmo; es decir, los elementos iguales no se invertirán.

Tamaño mínimo de ejecución

El algoritmo Timsort busca secuencias ordenadas de tamaño mínimo, minruns, para realizar su clasificación.

Debido a que la fusión es más eficiente cuando el número de ejecuciones es igual o ligeramente menor que una potencia de dos, y notablemente menos eficiente cuando el número de ejecuciones es ligeramente mayor que una potencia de dos, Timsort elige minrun para intentar garantizar la primera condición. [13]

Minrun se elige del rango de 32 a 64 inclusive, de modo que el tamaño de los datos, dividido por minrun , sea igual o ligeramente menor que una potencia de dos. El algoritmo final toma los seis bits más significativos del tamaño de la matriz, suma uno si alguno de los bits restantes está establecido y utiliza ese resultado como minrun . Este algoritmo funciona para todas las matrices, incluidas aquellas más pequeñas que 64; para matrices de tamaño 63 o menos, esto establece minrun igual al tamaño de la matriz y Timsort se reduce a una ordenación por inserción. [13]

Análisis

En el peor de los casos , Timsort realiza comparaciones para ordenar una matriz de n elementos. En el mejor de los casos, que ocurre cuando la entrada ya está ordenada, se ejecuta en tiempo lineal, lo que significa que es un algoritmo de ordenamiento adaptativo . [3]

Es superior a Quicksort para ordenar referencias o punteros de objetos porque estos requieren una costosa indirección de memoria para acceder a los datos y realizar comparaciones y los beneficios de coherencia de caché de Quicksort se reducen en gran medida.

Verificación formal

En 2015, investigadores holandeses y alemanes del proyecto ENVISAGE del 7PM de la UE encontraron un error en la implementación estándar de Timsort. [16] Se solucionó en 2015 en Python, Java y Android.

En concreto, las invariantes en los tamaños de ejecución apilados garantizan un límite superior estricto en el tamaño máximo de la pila requerida. La implementación asignó previamente una pila suficiente para ordenar 2 64 bytes de entrada y evitó más controles de desbordamiento.

Sin embargo, la garantía requiere que los invariantes se apliquen a cada grupo de tres ejecuciones consecutivas, pero la implementación solo lo verificó para los tres primeros. [16] Al utilizar la herramienta KeY para la verificación formal del software Java, los investigadores descubrieron que esta verificación no es suficiente y pudieron encontrar longitudes de ejecución (y entradas que generaron esas longitudes de ejecución) que darían como resultado que los invariantes se violaran más profundamente en la pila después de que se fusionara la parte superior de la pila. [17]

Como consecuencia, para ciertas entradas, el tamaño asignado no es suficiente para contener todas las ejecuciones no fusionadas. En Java, esto genera para esas entradas una excepción de matriz fuera de límites. La entrada más pequeña que activa esta excepción en Java y Android v7 es de tamaño67 108 864 (2 26 ). (Las versiones anteriores de Android ya activaban esta excepción para ciertas entradas de tamaño65 536 (2 16 ))

La implementación de Java se corrigió aumentando el tamaño de la pila preasignada en función de un análisis actualizado del peor de los casos. El artículo también mostró mediante métodos formales cómo establecer el invariante deseado comprobando que las cuatro ejecuciones superiores de la pila satisfacen las dos reglas anteriores. Este enfoque fue adoptado inicialmente por Python [18] hasta que cambió a Powersort en 2022 con el lanzamiento de Python 3.11. [5]

Referencias

  1. ^ Peters, Tim (20 de julio de 2002). "[Python-Dev] Sorting". Lista de correo de desarrolladores de Python . Consultado el 24 de febrero de 2011 . [Timsort] también tiene aspectos positivos: es estable (los elementos que se comparan de forma igual conservan su orden relativo, por lo que, por ejemplo, si ordenas primero por código postal y una segunda vez por nombre, las personas con el mismo nombre siguen apareciendo en orden ascendente de código postal; esto es importante en aplicaciones que, por ejemplo, refinan los resultados de las consultas en función de la entrada del usuario). ... No tiene casos malos (O(N log N) es el peor caso; las comparaciones N−1 son las mejores).
  2. ^ Barrena, Nicolás; Jugé, Vicente; Nicaud, Cirilo; Pivoteau, Carine (2018). [GOTAS]. doi : 10.4230/LIPIcs.ESA.2018.4 . ISBN 9783959770811. S2CID  44091254 . Consultado el 1 de septiembre de 2018 . TimSort es un intrigante algoritmo de ordenamiento diseñado en 2002 para Python, cuya complejidad en el peor de los casos se anunció, pero no se demostró hasta nuestra reciente preimpresión.
  3. ^ ab Chandramouli, Badrish; Goldstein, Jonathan (2014). La paciencia es una virtud: reconsiderando la combinación y la clasificación en los procesadores modernos . SIGMOD/PODS.
  4. ^ Munro, J. Ian; Salvaje, Sebastián (2018). Combinaciones casi óptimas: métodos de clasificación rápidos y prácticos que se adaptan de manera óptima a las ejecuciones existentes . Schloss Dagstuhl – Leibniz-Zentrum für Informatik. págs. 63:1–63:16. arXiv : 1805.04154 . doi : 10.4230/lipics.esa.2018.63 . S2CID  21678081.
  5. ^ ab James, Mike. "Python ahora usa Powersort". I Programmer . Consultado el 21 de junio de 2024 .
  6. ^ "[#JDK-6804124] (coll) Reemplazar "modified mergesort" en java.util.Arrays.sort con timsort". Sistema de errores del JDK . Consultado el 11 de junio de 2014 .
  7. ^ "Clase: java.util.TimSort<T>". Documentación de Android Gingerbread . Archivado desde el original el 16 de julio de 2015. Consultado el 24 de febrero de 2011 .
  8. ^ "liboctave/util/oct-sort.cc". Repositorio Mercurial del código fuente de Octave . Líneas 23-25 ​​del bloque de comentarios inicial . Consultado el 18 de febrero de 2013. Código robado en gran parte de listobject.c de Python, que no tenía encabezado de licencia. Sin embargo, gracias a Tim Peters por las partes del código que robé.
  9. ^ "Cómo solucionar problemas en V8 · V8". v8.dev . Consultado el 21 de diciembre de 2018 .
  10. ^ "¿Sort() es estable en Swift 5?". Foros de Swift . 4 de julio de 2019. Consultado el 4 de julio de 2019 .
  11. ^ "slice - Rust". doc.rust-lang.org . Consultado el 8 de diciembre de 2022 . El algoritmo actual es un ordenamiento por fusión adaptativo e iterativo inspirado en timsort. Está diseñado para ser muy rápido en los casos en los que el segmento está casi ordenado o consta de dos o más secuencias ordenadas concatenadas una tras otra.
  12. ^ McIlroy, Peter (enero de 1993). "Ordenamiento optimista y complejidad teórica de la información". Actas del cuarto simposio anual ACM-SIAM sobre algoritmos discretos. pp. 467–474. ISBN 0-89871-313-7.
  13. ^ abc "listsort.txt". Código fuente de Python . 18 de mayo de 2022. Archivado desde el original el 28 de enero de 2016.
  14. ^ MacIver, David R. (11 de enero de 2010). "Understanding timsort, Part 1: Adaptive Mergesort" (Entender la ordenación temporal, parte 1: ordenación por fusión adaptativa) . Consultado el 5 de diciembre de 2015 .
  15. ^ Peters, Tim. "listsort.txt". Repositorio git de CPython . Consultado el 5 de diciembre de 2019 .
  16. ^ ab de Gouw, Stijn; Rot, Jurriaan; de Boer, Frank S.; Bubel, Richard; Hähnle, Reiner (julio de 2015). "Java.utils.Collection.sort() de OpenJDK está roto: lo bueno, lo malo y lo peor". Verificación asistida por computadora (PDF) . Apuntes de conferencias sobre informática. vol. 9206, págs. 273–289. doi :10.1007/978-3-319-21690-4_16. ISBN 978-3-319-21689-8.
  17. ^ de Gouw, Stijn (24 de febrero de 2015). "Demostrando que el algoritmo de ordenamiento de Android, Java y Python está roto (y mostrando cómo solucionarlo)" . Consultado el 6 de mayo de 2017 .
  18. ^ Rastreador de problemas de Python: problema 23515: Lógica incorrecta en merge_collapse de timsort

Lectura adicional

Enlaces externos