2009-01-26 27 views
7

Tengo una lista de cientos de cadenas únicas en C++, necesito verificar si existe un valor en esta lista, pero preferiblemente rápido como un rayo.Búsqueda rápida a través de una lista ordenada de cadenas en C++

estoy usando un currenly hash_set con std :: cuerdas (ya que no pude conseguir que funcione con const char *) de esta manera:

stdext::hash_set<const std::string> _items; 
_items.insert("LONG_NAME_A_WITH_SOMETHING"); 
_items.insert("LONG_NAME_A_WITH_SOMETHING_ELSE"); 
_items.insert("SHORTER_NAME"); 
_items.insert("SHORTER_NAME_SPECIAL"); 

stdext::hash_set<const std::string>::const_iterator it = _items.find("SHORTER_NAME")); 

if(it != _items.end()) { 
    std::cout << "item exists" << std::endl; 
} 

¿Alguien más tiene una buena idea para una búsqueda más rápida método sin construir una tabla hash completa?


La lista es una lista fija de cadenas que no cambiará. Contiene una lista de nombres de elementos que se ven afectados por un determinado error y deben repararse sobre la marcha cuando se abre con una versión más nueva.

He creado hashtables antes de usar Aho-Corasick, pero no estoy dispuesto a agregar demasiada complejidad.


Me sorprendió el número de respuestas. Terminé probando algunos métodos para su rendimiento y terminé usando una combinación de respuestas de Kirkus y Rob K. Intenté una búsqueda binaria antes, pero creo que tuve un pequeño error al implementarla (qué tan difícil puede ser ...).

Los resultados fueron impactantes ... Pensé que tenía una implementación rápida usando un hash_set ... bueno, al final no lo hice. He aquí algunas estadísticas (y el código eventual):

de búsqueda aleatoria de 5 llaves existentes y 1 llave inexistente, 50.000 veces

Mi algoritmo original tuvo un promedio de 18,62 segundo
Una búsqueda de lineair tomó en promedio 2,49 segundos
Una búsqueda binaria tomó en promedio 0,92 segundos.
Una búsqueda utilizando una tabla hash perfecta generada por gperf tomó en promedio 0,51 segundos.

Aquí está el código que uso ahora:

bool searchWithBinaryLookup(const std::string& strKey) { 
    static const char arrItems[][NUM_ITEMS] = { /* list of items */ }; 

    /* Binary lookup */ 
    int low, mid, high; 

    low = 0; 
    high = NUM_ITEMS; 
    while(low < high) { 
     mid = (low + high)/2; 
     if(arrAffectedSymbols[mid] > strKey) { 
     high = mid - 1; 
     } 
     else if(arrAffectedSymbols[mid] < strKey) { 
     low = mid + 1; 
     } 
     else { 
     return true; 
     } 
    } 

    return false; 
} 

NOTA: Este es Microsoft VC++, así que no estoy usando el std :: hash_set de SGI.


Hice algunas pruebas esta mañana usando gperf como VardhanDotNet sugerido y esto es un poco más rápido de hecho.

Respuesta

10

Si su lista de cadenas se fijan en tiempo de compilación, utilice gperf http://www.gnu.org/software/gperf/ CITA: gperf es un generador de funciones hash perfecta. Para una lista dada de cadenas, produce una función hash y una tabla hash, en forma de código C o C++, para buscar un valor dependiendo de la cadena de entrada. La función hash es perfecta, lo que significa que la tabla hash no tiene colisiones, y la búsqueda de la tabla hash solo necesita una sola cadena de comparación.

La salida de gperf no se rige por gpl o lgpl, afaik.

+0

Hmm ... Supongo que mi implementación actual es lo suficientemente rápida pero, sin embargo, probaré a gperf solo por la experiencia y el material de comparación. – Huppie

2

Dudo que se te ocurra una mejor hashtable; Si la lista varía de vez en cuando, es probable que haya obtenido la mejor manera.

La manera más rápida sería construir una máquina de estado finito para escanear la entrada. No estoy seguro de cuáles son las mejores herramientas modernas (han pasado más de diez años desde que hice algo así en la práctica), pero Lex/Flex era el constructor estándar de Unix.

Un FSM tiene una tabla de estados y una lista de estados aceptables. Comienza en el estado inicial y realiza un escaneo de la entrada por caracteres. Cada estado tiene una entrada para cada posible carácter de entrada. La entrada podría ser pasar a otro estado o abortar porque la cadena no está en la lista. Si el FSM llega al final de la cadena de entrada sin abortar, verifica el estado final en el que se encuentra, que es un estado de aceptación (en cuyo caso ha coincidido con la cadena) o no (en cuyo caso no tiene acceso) 't).

Cualquier libro sobre compiladores debe tener más detalles, o puede encontrar, sin duda, más información en la web.

+0

I figurado una máquina de estado Haría un mejor trabajo aquí, pero no estoy dispuesto a agregar mucha más complejidad para ese rendimiento adicional. – Huppie

+0

Así es como funciona el procedimiento de búsqueda de Patricia Trie. Pero es mucho más directo y fácil de implementar. – user21714

0

No sé qué tipo de función hash utiliza MS para las picaduras, pero tal vez podría encontrar algo más simple (= más rápido) que funcione en su caso especial. El contenedor debería permitirle usar una clase de hashing personalizada.

Si se trata de un problema de implementación del contenedor, también puedes probar si los impulsos std::tr1::unordered_set dan mejores resultados.

6

Puede probar una Trie PATRICIA si ninguno de los contenedores estándar satisface sus necesidades.

La búsqueda en el peor de los casos está limitada por la longitud de la cadena que está buscando. Además, las cadenas comparten prefijos comunes por lo que es realmente fácil en la memoria. Así que si tienes muchas cadenas relativamente cortas, esto podría ser beneficioso.

Check it out here.

Nota: PATRICIA = Algoritmo práctica para recuperar información se codifica en formato alfanumérico

3

Si se trata de una lista fija, ordenar la lista y hacer una búsqueda binaria? No me puedo imaginar, con solo un centenar de cadenas en una CPU moderna, realmente verá una diferencia apreciable entre los algoritmos, a menos que su aplicación no haga más que buscar dicha lista el 100% del tiempo.

1

Si el conjunto de cadenas para comprobar los números en los cientos que dices, y esto es cuando se hace E/S (cargando un archivo, que supongo que proviene de un disco, comúnmente), entonces diría: perfil de lo que tienes, antes de buscar soluciones más exóticas/complejas.

Por supuesto, podría ser que sus "documentos" contengan cientos de millones de estas cadenas, en cuyo caso supongo que realmente comienza a tomarse el tiempo ... Sin más detalles, es difícil decirlo con certeza.

Lo que digo se reduce a "considerar el caso de uso y los escenarios típicos, antes de (sobre) la optimización", que supongo que es solo una especialización de lo antiguo sobre las raíces del mal ... :)

0

una tabla hash es una buena solución, y al usar una implementación preexistente es probable que obtenga un buen rendimiento. una alternativa, aunque creo que se llama "indexación".

mantenga algunos indicadores en ubicaciones convenientes. p.ej. si está usando letras para la clasificación, mantenga un puntero a todo comenzando aa, ab, ac ... ba, bc, bd ... esto es unos pocos cientos de punteros, pero significa que puede saltarse a una parte de la lista que es bastante cerca del resultado antes de continuar. p.ej. si una entrada es "afunctionname", entonces puede realizar una búsqueda binaria entre los punteros para af y ag, mucho más rápido que buscar todo el lote ... si tiene un millón de registros en total, es probable que solo tenga que buscar binarios en una lista de algunos miles

reinventé esta rueda en particular, pero puede que ya existan muchas implementaciones, lo que le ahorrará el dolor de cabeza de la implementación y probablemente sea más rápido que cualquier código que pueda pegar aquí. :)

1

100 cuerdas únicas? Si esto no se llama con frecuencia, y la lista no cambia dinámicamente, probablemente usaría una matriz directa const con una búsqueda lineal. A menos que lo busque mucho, algo tan pequeño simplemente no vale la pena el código adicional. Algo como esto:

const char _items[][MAX_ITEM_LEN] = { ... }; 
int i = 0; 
for (; strcmp(a, _items[i]) < 0 && i < NUM_ITEMS; ++i); 
bool found = i < NUM_ITEMS && strcmp(a, _items[i]) == 0; 

Para obtener una lista pequeña que, creo que sus costes de implementación y mantenimiento con algo más compleja, probablemente superen los costes en tiempo de ejecución, y usted no está realmente va a obtener menores costos espacio que esta . Para ganar un poco más de velocidad, puede hacer una tabla hash del primer índice char -> list para establecer el valor inicial de i;

Para una lista tan pequeña, es probable que no obtenga mucho más rápido.

+0

Prefiero una solución simple. Es por eso que mi solución actual es así. El código se llama bastante, así que quiero asegurarme de obtener todo el rendimiento posible de las menos líneas de código posibles. – Huppie

+0

Por supuesto, me gustaría envolverlo en una buena clase para ocultar todo eso, también. –

4

¿Qué pasa con std :: vector? Cargarlo, ordenar (v.begin(), v.end()) una vez y luego usar lower_bound() para ver si la cadena está en el vector. lower_bound se garantiza que sea O (log2 N) en un iterador de acceso aleatorio ordenado. No puedo entender la necesidad de un hash si los valores son fijos. Un vector ocupa menos espacio en la memoria que un hash y hace menos asignaciones.

0

Está utilizando la búsqueda binaria, que es O (log (n)). Debería mirar la búsqueda de interpolación, que no es tan buena "peor caso", pero es el caso promedio es mejor: O (log (log (n)).

0

Corté & pegué el código de búsqueda binario desde arriba. . Hay un problema con el código binario de búsqueda original, por ejemplo, no puede encontrar el segundo elemento de una lista de 100 artículos

la línea:.

high = mid - 1; 

debe ser:

high = mid; 
Cuestiones relacionadas