¿Cómo implementar algoritmos de clasificación clásicos en C ++ moderno?

331
TemplateRex 2014-07-09 23:59.

El std::sortalgoritmo (y sus primos std::partial_sorty std::nth_element) de la biblioteca estándar de C ++ es en la mayoría de las implementaciones una combinación complicada e híbrida de algoritmos de clasificación más elementales , como clasificación por selección, clasificación por inserción, clasificación rápida, clasificación por combinación o clasificación por montón.

Hay muchas preguntas aquí y en sitios hermanos como https://codereview.stackexchange.com/ relacionadas con errores, complejidad y otros aspectos de las implementaciones de estos algoritmos de clasificación clásicos. La mayoría de las implementaciones ofrecidas consisten en bucles sin procesar, usan manipulación de índices y tipos concretos, y generalmente no son triviales para analizar en términos de corrección y eficiencia.

Pregunta : ¿cómo se pueden implementar los algoritmos de clasificación clásicos mencionados anteriormente utilizando C ++ moderno?

  • sin bucles sin procesar , pero combinando los bloques de construcción algorítmicos de la biblioteca estándar de<algorithm>
  • interfaz de iterador y uso de plantillas en lugar de manipulación de índices y tipos concretos
  • Estilo C ++ 14 , incluida la biblioteca estándar completa, así como reductores de ruido sintáctico como auto, alias de plantilla, comparadores transparentes y lambdas polimórficas.

Notas :

  • Para obtener más referencias sobre implementaciones de algoritmos de clasificación, consulte Wikipedia , Rosetta Code o http://www.sorting-algorithms.com/
  • de acuerdo con las convenciones de Sean Parent (diapositiva 39), un bucle sin procesar es un forbucle más largo que la composición de dos funciones con un operador. Así que f(g(x));o f(x); g(x);o f(x) + g(x);no son bucles primas, como tampoco lo son los bucles en selection_sorty insertion_sortpor debajo.
  • Sigo la terminología de Scott Meyers para denotar el C ++ 1y actual ya como C ++ 14, y para denotar C ++ 98 y C ++ 03 ambos como C ++ 98, así que no me llames por eso.
  • Como se sugiere en los comentarios de @Mehrdad, proporciono cuatro implementaciones como un ejemplo en vivo al final de la respuesta: C ++ 14, C ++ 11, C ++ 98 y Boost y C ++ 98.
  • La respuesta en sí misma se presenta en términos de C ++ 14 únicamente. Donde sea relevante, denoto las diferencias sintácticas y de biblioteca donde difieren las diversas versiones de idiomas.

2 answers

392
TemplateRex 2014-07-09 23:59.

Bloques de construcción algorítmicos

Comenzamos ensamblando los bloques de construcción algorítmicos de la Biblioteca estándar:

#include <algorithm>    // min_element, iter_swap, 
                        // upper_bound, rotate, 
                        // partition, 
                        // inplace_merge,
                        // make_heap, sort_heap, push_heap, pop_heap,
                        // is_heap, is_sorted
#include <cassert>      // assert 
#include <functional>   // less
#include <iterator>     // distance, begin, end, next
  • las herramientas de iterador como non-member std::begin()/ std::end()así como with std::next()solo están disponibles a partir de C ++ 11 y posteriores. Para C ++ 98, es necesario escribirlos él mismo. Hay sustitutos de Boost.Range en boost::begin()/ boost::end()y de Boost.Utility en boost::next().
  • el std::is_sortedalgoritmo solo está disponible para C ++ 11 y posteriores. Para C ++ 98, esto se puede implementar en términos de std::adjacent_findun objeto de función escrito a mano. Boost.Algorithm también proporciona boost::algorithm::is_sortedun sustituto.
  • el std::is_heapalgoritmo solo está disponible para C ++ 11 y posteriores.

Objetos sintácticos

C ++ 14 proporciona ¿Qué son los comparadores transparentes? de la forma std::less<>que actúan polimórficamente sobre sus argumentos. Esto evita tener que proporcionar un tipo de iterador. Esto se puede usar en combinación con los Argumentos de plantilla predeterminados para plantillas de funciones C ++ 11 para crear una sola sobrecarga para ordenar algoritmos que toman <como comparación y aquellos que tienen un objeto de función de comparación definido por el usuario.

template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

En C ++ 11, se puede definir un Typedef de plantilla de C ++ reutilizable para extraer el tipo de valor de un iterador que agrega un desorden menor a las firmas de los algoritmos de ordenación:

template<class It>
using value_type_t = typename std::iterator_traits<It>::value_type;

template<class It, class Compare = std::less<value_type_t<It>>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

En C ++ 98, es necesario escribir dos sobrecargas y usar la typename xxx<yyy>::typesintaxis detallada

template<class It, class Compare>
void xxx_sort(It first, It last, Compare cmp); // general implementation

template<class It>
void xxx_sort(It first, It last)
{
    xxx_sort(first, last, std::less<typename std::iterator_traits<It>::value_type>());
}
  • Otra sutileza sintáctica es que C ++ 14 facilita envolver comparadores definidos por el usuario a través de lambdas polimórficas (con autoparámetros que se deducen como argumentos de plantilla de función).
  • C ++ 11 solo tiene lambdas monomórficas, que requieren el uso del alias de plantilla anterior value_type_t.
  • En C ++ 98, uno necesita escribir un objeto de función independiente o recurrir al tipo de sintaxis detallado std::bind1st/ std::bind2nd/ std::not1.
  • Boost.Bind mejora esto con la sintaxis boost::bindy _1/ _2placeholder.
  • C ++ 11 y posteriores también lo tienen std::find_if_not, mientras que C ++ 98 necesita std::find_ifcon un std::not1objeto de función alrededor.

Estilo C ++

Todavía no existe un estilo C ++ 14 generalmente aceptable. Para bien o para mal, sigo de cerca el borrador Effective Modern C ++ de Scott Meyers y el renovado GotW de Herb Sutter . Utilizo las siguientes recomendaciones de estilo:

  • La recomendación "Almost Always Auto" de Herb Sutter y "Preferir auto a declaraciones de tipo específico " de Scott Meyers , cuya brevedad es insuperable, aunque su claridad a veces se discute .
  • "Distinguir ()y {}al crear objetos" de Scott Meyers y elegir constantemente la inicialización {}entre paréntesis ()en lugar de la inicialización entre paréntesis (para evitar todos los problemas de análisis más molestos en el código genérico).
  • "Preferir declaraciones de alias a typedefs" de Scott Meyers . Para las plantillas, esto es imprescindible de todos modos, y usarlo en todas partes en lugar de typedefahorra tiempo y agrega consistencia.
  • Utilizo un for (auto it = first; it != last; ++it)patrón en algunos lugares, para permitir la verificación invariante de bucle para subrangos ya ordenados. En el código de producción, el uso de while (first != last)y en ++firstalgún lugar dentro del ciclo podría ser un poco mejor.

Orden de selección

La clasificación de selección no se adapta a los datos de ninguna manera, por lo que su tiempo de ejecución es siempreO(N²). Sin embargo, el ordenamiento por selección tiene la propiedad de minimizar el número de intercambios . En aplicaciones donde el costo de intercambiar artículos es alto, la ordenación por selección puede ser el algoritmo de elección.

Para implementarlo usando la Biblioteca estándar, use repetidamente std::min_elementpara encontrar el elemento mínimo restante y iter_swappara cambiarlo en su lugar:

template<class FwdIt, class Compare = std::less<>>
void selection_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const selection = std::min_element(it, last, cmp);
        std::iter_swap(selection, it); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

Tenga en cuenta que selection_sorttiene el rango ya procesado [first, it)ordenado como su invariante de bucle. Los requisitos mínimos son los iteradores de avance , en comparación con std::sortlos iteradores de acceso aleatorio.

Detalles omitidos :

  • La ordenación por selección se puede optimizar con una prueba temprana if (std::distance(first, last) <= 1) return;(o para iteradores hacia adelante / bidireccionales :) if (first == last || std::next(first) == last) return;.
  • para los iteradores bidireccionales , la prueba anterior se puede combinar con un bucle sobre el intervalo [first, std::prev(last)), porque se garantiza que el último elemento es el elemento restante mínimo y no requiere un intercambio.

Tipo de inserción

Aunque es uno de los algoritmos de clasificación elementales con el O(N²)peor de los casos, la clasificación por inserción es el algoritmo de elección, ya sea cuando los datos están casi ordenados (porque son adaptables ) o cuando el tamaño del problema es pequeño (porque tiene una sobrecarga baja). Por estas razones, y debido a que también es estable , la ordenación por inserción se usa a menudo como el caso base recursivo (cuando el tamaño del problema es pequeño) para algoritmos de ordenación de división y conquista superiores, como la ordenación combinada o la ordenación rápida.

Para implementar insertion_sortcon la Biblioteca estándar, use repetidamente std::upper_boundpara encontrar la ubicación donde el elemento actual debe ir, y use std::rotatepara desplazar los elementos restantes hacia arriba en el rango de entrada:

template<class FwdIt, class Compare = std::less<>>
void insertion_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const insertion = std::upper_bound(first, it, *it, cmp);
        std::rotate(insertion, it, std::next(it)); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

Tenga en cuenta que insertion_sorttiene el rango ya procesado [first, it)ordenado como su invariante de bucle. La ordenación por inserción también funciona con iteradores hacia adelante.

Detalles omitidos :

  • la ordenación por inserción se puede optimizar con una prueba temprana if (std::distance(first, last) <= 1) return;(o para iteradores hacia adelante / bidireccionales :) if (first == last || std::next(first) == last) return;y un bucle sobre el intervalo [std::next(first), last), porque se garantiza que el primer elemento estará en su lugar y no requiere una rotación.
  • para los iteradores bidireccionales , la búsqueda binaria para encontrar el punto de inserción se puede reemplazar con una búsqueda lineal inversa utilizando el std::find_if_notalgoritmo de la biblioteca estándar .

Cuatro ejemplos en vivo ( C ++ 14 , C ++ 11 , C ++ 98 y Boost , C ++ 98 ) para el siguiente fragmento:

using RevIt = std::reverse_iterator<BiDirIt>;
auto const insertion = std::find_if_not(RevIt(it), RevIt(first), 
    [=](auto const& elem){ return cmp(*it, elem); }
).base();
  • Para entradas aleatorias, esto proporciona O(N²)comparaciones, pero esto mejora a las O(N)comparaciones para entradas casi ordenadas. La búsqueda binaria siempre usa O(N log N)comparaciones.
  • Para rangos de entrada pequeños, la mejor localidad de memoria (caché, captación previa) de una búsqueda lineal también podría dominar una búsqueda binaria (uno debería probar esto, por supuesto).

Ordenación rápida

Cuando se implementa con cuidado, la clasificación rápida es robusta y tiene la O(N log N)complejidad esperada, pero con la O(N²)complejidad del peor de los casos que se puede activar con datos de entrada elegidos de manera adversa. Cuando no se necesita una clasificación estable, la clasificación rápida es una excelente clasificación de uso general.

Incluso para las versiones más simples, la ordenación rápida es un poco más complicada de implementar utilizando la Biblioteca estándar que los otros algoritmos de ordenación clásicos. El siguiente enfoque usa algunas utilidades de iterador para ubicar el elemento medio del rango de entrada [first, last)como pivote, luego usa dos llamadas a std::partition(que son O(N)) para dividir en tres direcciones el rango de entrada en segmentos de elementos que son más pequeños que, iguales a, y mayor que el pivote seleccionado, respectivamente. Finalmente, los dos segmentos externos con elementos más pequeños y más grandes que el pivote se ordenan de forma recursiva:

template<class FwdIt, class Compare = std::less<>>
void quick_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;
    auto const pivot = *std::next(first, N / 2);
    auto const middle1 = std::partition(first, last, [=](auto const& elem){ 
        return cmp(elem, pivot); 
    });
    auto const middle2 = std::partition(middle1, last, [=](auto const& elem){ 
        return !cmp(pivot, elem);
    });
    quick_sort(first, middle1, cmp); // assert(std::is_sorted(first, middle1, cmp));
    quick_sort(middle2, last, cmp);  // assert(std::is_sorted(middle2, last, cmp));
}

Sin embargo, la ordenación rápida es bastante complicada de hacer correcta y eficiente, ya que cada uno de los pasos anteriores debe ser revisado y optimizado cuidadosamente para el código de nivel de producción. En particular, por O(N log N)complejidad, el pivote tiene que resultar en una partición equilibrada de los datos de entrada, que no se puede garantizar en general para un O(1)pivote, pero que se puede garantizar si se establece el pivote como la O(N)mediana del rango de entrada.

Detalles omitidos :

  • la implementación anterior es particularmente vulnerable a entradas especiales, por ejemplo, tiene O(N^2)complejidad para la entrada del " tubo de órgano " 1, 2, 3, ..., N/2, ... 3, 2, 1(porque el medio siempre es más grande que todos los demás elementos).
  • ¿Número de comparaciones realizadas en la mediana de 3 funciones? selección de pivote¿Número de comparaciones realizadas en la mediana de 3 funciones? partir de¿Cómo obtener un elemento aleatorio de un contenedor C ++? del rango de entrada protege contra entradas casi clasificadas para las cuales la complejidad se deterioraría de otra maneraO(N^2).
  • La partición de 3 vías (separar elementos más pequeños, iguales y más grandes que el pivote) como se muestra en las dos llamadas astd::partitionno es elO(N)algoritmomás eficientepara lograr este resultado.
  • para los iteradores de acceso aleatorio , O(N log N)se puede lograr una complejidad garantizada mediante el uso de la selección de pivote medio std::nth_element(first, middle, last), seguido de llamadas recursivas a quick_sort(first, middle, cmp)y quick_sort(middle, last, cmp).
  • esta garantía tiene un costo, sin embargo, porque el factor constante de la O(N)complejidad de std::nth_elementpuede ser más costoso que el de la O(1)complejidad de un pivote mediano de 3 seguido de una O(N)llamada a std::partition(que es un pase directo sencillo compatible con caché los datos).

Combinar ordenación

Si el uso de O(N)espacio adicional no es un problema, la ordenación por combinación es una excelente opción: es el único algoritmo de ordenación estable O(N log N) .

Es fácil de implementar usando algoritmos estándar: use algunas utilidades de iterador para ubicar el medio del rango de entrada [first, last)y combine dos segmentos ordenados recursivamente con std::inplace_merge:

template<class BiDirIt, class Compare = std::less<>>
void merge_sort(BiDirIt first, BiDirIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;                   
    auto const middle = std::next(first, N / 2);
    merge_sort(first, middle, cmp); // assert(std::is_sorted(first, middle, cmp));
    merge_sort(middle, last, cmp);  // assert(std::is_sorted(middle, last, cmp));
    std::inplace_merge(first, middle, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

La ordenación por fusión requiere iteradores bidireccionales, siendo el cuello de botella std::inplace_merge. Tenga en cuenta que al ordenar listas vinculadas, la ordenación por combinación solo requiere O(log N)espacio adicional (para recursividad). El último algoritmo se implementa std::list<T>::sorten la biblioteca estándar.

Tipo de pila

La ordenación de montón es simple de implementar, realiza unaO(N log N)ordenación en el lugar, pero no es estable.

El primer ciclo, la O(N)fase "heapify", coloca la matriz en orden de pila. El segundo ciclo, la O(N log Nfase de "clasificación", extrae repetidamente el máximo y restaura el orden del montón. La biblioteca estándar hace que esto sea extremadamente sencillo:

template<class RandomIt, class Compare = std::less<>>
void heap_sort(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    lib::make_heap(first, last, cmp); // assert(std::is_heap(first, last, cmp));
    lib::sort_heap(first, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

En caso de que considere que usar std::make_heapy es "trampa" std::sort_heap, puede ir un nivel más profundo y escribir esas funciones usted mismo en términos de std::push_heapy std::pop_heap, respectivamente:

namespace lib {

// NOTE: is O(N log N), not O(N) as std::make_heap
template<class RandomIt, class Compare = std::less<>>
void make_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last;) {
        std::push_heap(first, ++it, cmp); 
        assert(std::is_heap(first, it, cmp));           
    }
}

template<class RandomIt, class Compare = std::less<>>
void sort_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = last; it != first;) {
        std::pop_heap(first, it--, cmp);
        assert(std::is_heap(first, it, cmp));           
    } 
}

}   // namespace lib

La biblioteca estándar especifica tanto push_heapy pop_heapcomo la complejidad O(log N). Sin embargo , tenga en cuenta que el bucle externo sobre el rango [first, last)resulta en O(N log N)complejidad para make_heap, mientras que std::make_heapsolo tiene O(N)complejidad. Porque la O(N log N)complejidad general heap_sortno importa.

Detalles omitidos : O(N)implementación demake_heap

Pruebas

Aquí hay cuatro ejemplos en vivo ( C ++ 14 , C ++ 11 , C ++ 98 y Boost , C ++ 98 ) que prueban los cinco algoritmos en una variedad de entradas (no pretende ser exhaustivo ni riguroso). Solo tenga en cuenta las enormes diferencias en el LOC: C ++ 11 / C ++ 14 necesita alrededor de 130 LOC, C ++ 98 y Boost 190 (+ 50%) y C ++ 98 más de 270 (+ 100%).

14
Morwenn 2016-05-09 12:55.

Otro pequeño y bastante elegante que se Contando ordenar usando STL . Pensé que valía la pena compartirlo.

Contando ordenar

Si bien es bastante especializado, la ordenación de conteo es un algoritmo de ordenación de enteros simple y, a menudo, puede ser muy rápido siempre que los valores de los enteros a ordenar no estén muy separados. Probablemente sea ideal si alguna vez se necesita ordenar una colección de un millón de enteros que se sabe que están entre 0 y 100, por ejemplo.

Para implementar una ordenación de conteo muy simple que funcione con enteros con signo y sin signo, es necesario encontrar los elementos más pequeños y más grandes de la colección para ordenar; su diferencia dirá el tamaño de la matriz de recuentos a asignar. Luego, se realiza una segunda pasada por la colección para contar el número de apariciones de cada elemento. Finalmente, volvemos a escribir el número requerido de cada entero en la colección original.

template<typename ForwardIterator>
void counting_sort(ForwardIterator first, ForwardIterator last)
{
    if (first == last || std::next(first) == last) return;

    auto minmax = std::minmax_element(first, last);  // avoid if possible.
    auto min = *minmax.first;
    auto max = *minmax.second;
    if (min == max) return;

    using difference_type = typename std::iterator_traits<ForwardIterator>::difference_type;
    std::vector<difference_type> counts(max - min + 1, 0);

    for (auto it = first ; it != last ; ++it) {
        ++counts[*it - min];
    }

    for (auto count: counts) {
        first = std::fill_n(first, count, min++);
    }
}

Si bien solo es útil cuando se sabe que el rango de los enteros a ordenar es pequeño (generalmente no más grande que el tamaño de la colección a ordenar), hacer que la ordenación de conteo sea más genérica lo haría más lento en los mejores casos. Si no se sabe que el rango sea pequeño, se puede usar otro algoritmo como el ordenamiento por radix , ska_sort o spreadsort .

Detalles omitidos :

  • Podríamos haber pasado los límites del rango de valores aceptados por el algoritmo como parámetros para deshacernos totalmente del primer std::minmax_elementpaso a través de la colección. Esto hará que el algoritmo sea aún más rápido cuando se conozca un límite de rango pequeño útil por otros medios. (No tiene que ser exacto; pasar una constante de 0 a 100 es mucho mejor que una pasada adicional sobre un millón de elementos para descubrir que los límites verdaderos son de 1 a 95. Incluso de 0 a 1000 valdría la pena; el los elementos adicionales se escriben una vez con cero y se leen una vez).

  • Cultivar countssobre la marcha es otra forma de evitar un primer paso por separado. Duplicar el countstamaño cada vez que tiene que crecer da un tiempo O (1) amortizado por elemento clasificado (consulte el análisis del costo de inserción de la tabla hash para comprobar que el crecimiento exponencial es la clave). Crecer al final de un nuevo maxes fácil con std::vector::resizeagregar nuevos elementos a cero. El cambio minsobre la marcha y la inserción de nuevos elementos con cero en la parte delantera se pueden realizar std::copy_backwarddespués de hacer crecer el vector. Luego, std::fillponer a cero los nuevos elementos.

  • El countsbucle de incremento es un histograma. Si es probable que los datos sean muy repetitivos y el número de bins es pequeño, puede valer la pena ¿Cómo optimizar las estadísticas del histograma con intrínsecos de neón? para reducir el cuello de botella de la dependencia de datos de serialización del almacenamiento / recarga en el mismo bin. Esto significa más recuentos hasta cero al principio y más para repetir al final, pero debería valer la pena en la mayoría de las CPU para nuestro ejemplo de millones de números de 0 a 100, especialmente si la entrada ya podría estar (parcialmente) ordenada y tienen tiradas largas del mismo número.

  • En el algoritmo anterior, usamos un min == maxcheque para regresar temprano cuando cada elemento tiene el mismo valor (en cuyo caso la colección está ordenada). En realidad, es posible verificar completamente si la colección ya está ordenada mientras se encuentran los valores extremos de una colección sin perder tiempo adicional (si la primera pasada todavía tiene un cuello de botella en la memoria con el trabajo adicional de actualizar el mínimo y el máximo). Sin embargo, tal algoritmo no existe en la biblioteca estándar y escribir uno sería más tedioso que escribir el resto del tipo de conteo en sí. Se deja como ejercicio para el lector.

  • Dado que el algoritmo solo funciona con valores enteros, las aserciones estáticas podrían usarse para evitar que los usuarios cometan errores de tipo obvios. En algunos contextos, std::enable_if_tpodría preferirse un fallo de sustitución con .

  • Si bien el C ++ moderno es genial, el C ++ futuro podría ser aún más genial: Contando ordenar usando STL y algunas partes de Ranges TS harían que el algoritmo fuera aún más limpio.

Related questions

MORE COOL STUFF

La estrella de HGTV, Christina Hall, revela que tiene 'envenenamiento por mercurio y plomo' probablemente por voltear 'casas asquerosas'

La estrella de HGTV, Christina Hall, revela que tiene 'envenenamiento por mercurio y plomo' probablemente por voltear 'casas asquerosas'

La estrella de HGTV, Christina Hall, revela que le diagnosticaron envenenamiento por mercurio y plomo, probablemente debido a su trabajo como manipuladora de casas.

La estrella de 'Love Is Blind' Brennon Lemieux responde a los cargos de violencia doméstica

La estrella de 'Love Is Blind' Brennon Lemieux responde a los cargos de violencia doméstica

Recientemente salió a la luz un informe policial que acusa a la estrella de 'Love Is Blind', Brennon, de violencia doméstica. Ahora, Brennon ha respondido a los reclamos.

Wynonna Judd se dio cuenta de que ahora es la matriarca de la familia Judd en un momento festivo de pánico

Wynonna Judd se dio cuenta de que ahora es la matriarca de la familia Judd en un momento festivo de pánico

Conozca cómo Wynonna Judd se dio cuenta de que ahora es la matriarca de la familia mientras organizaba la primera celebración de Acción de Gracias desde que murió su madre, Naomi Judd.

Experto en lenguaje corporal explica los 'paralelos' entre Kate Middleton y la princesa Diana

Experto en lenguaje corporal explica los 'paralelos' entre Kate Middleton y la princesa Diana

Descubra por qué un destacado experto en lenguaje corporal cree que es fácil trazar "tales paralelismos" entre la princesa Kate Middleton y la princesa Diana.

Los láseres arrojan luz sobre por qué necesita cerrar la tapa antes de descargar

Los láseres arrojan luz sobre por qué necesita cerrar la tapa antes de descargar

Los inodoros arrojan columnas de aerosol invisibles con cada descarga. ¿Como sabemos? La prueba fue capturada por láseres de alta potencia.

The Secrets of Airline Travel Quiz

The Secrets of Airline Travel Quiz

Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?

Where in the World Are You? Take our GeoGuesser Quiz

Where in the World Are You? Take our GeoGuesser Quiz

The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!

¿Caduca el repelente de insectos?

¿Caduca el repelente de insectos?

¿Sigue siendo efectivo ese lote de repelente de insectos que te quedó del verano pasado? Si es así, ¿por cuánto tiempo?

Esta mujer murió por el derecho al voto hace 104 años hoy

Esta mujer murió por el derecho al voto hace 104 años hoy

Las mujeres del Reino Unido acuden a las urnas para las elecciones especiales de hoy en un aniversario particularmente apropiado. Han pasado 104 años desde que Emily Wilding Davison murió por la causa del sufragio femenino, pisoteada hasta la muerte en medio de una carrera de caballos de alto perfil, frente al Rey y la Reina.

Ponle una tapa. En realidad, ponle una tapa a todo. Consigue 12 tapas de cocina elásticas de silicona por $14. [Exclusivo]

Ponle una tapa. En realidad, ponle una tapa a todo. Consigue 12 tapas de cocina elásticas de silicona por $14. [Exclusivo]

Tapas elásticas de silicona de Tomorrow's Kitchen, paquete de 12 | $14 | Amazonas | Código promocional 20OFFKINJALids son básicamente los calcetines de la cocina; siempre perdiéndose, dejando contenedores huérfanos que nunca podrán volver a cerrarse. Pero, ¿y si sus tapas pudieran estirarse y adaptarse a todos los recipientes, ollas, sartenes e incluso frutas en rodajas grandes que sobran? Nunca más tendrás que preocuparte por perder esa tapa tan específica.

Cuéntanos tus mejores trucos de Washington, DC

Cuéntanos tus mejores trucos de Washington, DC

Hemos pirateado algunas ciudades industriales en esta columna, como Los Ángeles y Las Vegas. Ahora es el momento de una ciudad militar-industrial-compleja.

Un minorista está eliminando su sección de tallas grandes y mezclando tallas más grandes con todo lo demás

Un minorista está eliminando su sección de tallas grandes y mezclando tallas más grandes con todo lo demás

Un minorista está enlatando su sección de tallas grandes. Pero no están tomando la categoría solo en línea o descontinuándola por completo.

Patinaje artístico de EE. UU. 'frustrado' por falta de decisión final en evento por equipos, pide una decisión justa

Patinaje artístico de EE. UU. 'frustrado' por falta de decisión final en evento por equipos, pide una decisión justa

El equipo está a la espera de las medallas que ganó en los Juegos Olímpicos de Invierno de 2022 en Beijing, ya que se está resolviendo un caso de dopaje que involucra a la patinadora artística rusa Kamila Valieva.

Los compradores de Amazon dicen que duermen 'como un bebé mimado' gracias a estas fundas de almohada de seda que cuestan tan solo $ 10

Los compradores de Amazon dicen que duermen 'como un bebé mimado' gracias a estas fundas de almohada de seda que cuestan tan solo $ 10

Miles de compradores de Amazon recomiendan la funda de almohada de seda Mulberry, y está a la venta en este momento. La funda de almohada de seda viene en varios colores y ayuda a mantener el cabello suave y la piel clara. Compre las fundas de almohada de seda mientras tienen hasta un 46 por ciento de descuento en Amazon

Se busca al corredor de los Bengals Joe Mixon por orden de arresto emitida por presuntamente apuntar con un arma de fuego a una mujer

Se busca al corredor de los Bengals Joe Mixon por orden de arresto emitida por presuntamente apuntar con un arma de fuego a una mujer

El jueves se presentó una denuncia de delito menor amenazante agravado contra Joe Mixon.

Profesor de la Universidad de Purdue arrestado por presuntamente traficar metanfetamina y proponer favores sexuales a mujeres

Profesor de la Universidad de Purdue arrestado por presuntamente traficar metanfetamina y proponer favores sexuales a mujeres

El Departamento de Policía de Lafayette comenzó a investigar a un profesor de la Universidad de Purdue en diciembre después de recibir varias denuncias de un "hombre sospechoso que se acercaba a una mujer".

Concept Drift: el mundo está cambiando demasiado rápido para la IA

Concept Drift: el mundo está cambiando demasiado rápido para la IA

Al igual que el mundo que nos rodea, el lenguaje siempre está cambiando. Mientras que en eras anteriores los cambios en el idioma ocurrían durante años o incluso décadas, ahora pueden ocurrir en cuestión de días o incluso horas.

India me está pateando el culo

India me está pateando el culo

Estoy de vuelta por primera vez en seis años. No puedo decirte cuánto tiempo he estado esperando esto.

ℝ

“And a river went out of Eden to water the garden, and from thence it was parted and became into four heads” Genesis 2:10. ? The heart is located in the middle of the thoracic cavity, pointing eastward.

¿Merrick Garland le ha fallado a Estados Unidos?

Es más de la mitad de la presidencia de Biden. ¿Qué está esperando Merrick Garland?

¿Merrick Garland le ha fallado a Estados Unidos?

Creo, un poco tarde en la vida, en dar oportunidades a la gente. Generosamente.

Language