1. Vue d’ensemble
Dans cet article, nous allons examiner différentes façons de rechercher une valeur spécifiée dans un tableau.
Nous allons également comparer leurs performances à l’aide de JMH (le harnais Java Microbenchmark) pour déterminer la méthode la mieux adaptée.
2. Installer
Pour nos exemples, nous allons utiliser un tableau contenant Strings généré aléatoirement pour chaque test:
String[]seedArray(int length) {
String[]strings = new String[length];
Random value = new Random();
for (int i = 0; i < length; i++) {
strings[i]= String.valueOf(value.nextInt());
}
return strings;
}
Pour réutiliser le tableau dans chaque test, nous allons déclarer une classe interne qui contiendra le tableau et le nombre afin que nous puissions déclarer sa portée pour JMH:
@State(Scope.Benchmark)
public static class SearchData {
static int count = 1000;
static String[]strings = seedArray(1000);
}
3. Recherche de base
-
Trois méthodes couramment utilisées pour rechercher un tableau sont les suivantes: List, a Set, ou avec une boucle ** qui examine chaque membre jusqu’à ce qu’il trouve une correspondance.
Commençons par trois méthodes qui implémentent chaque algorithme:
boolean searchList(String[]strings, String searchString) {
return Arrays.asList(SearchData.strings)
.contains(searchString);
}
boolean searchSet(String[]strings, String searchString) {
Set<String> stringSet = new HashSet<>(Arrays.asList(SearchData.strings));
return stringSet.contains(searchString);
}
boolean searchLoop(String[]strings, String searchString) {
for (String string : SearchData.strings) {
if (string.equals(searchString))
return true;
}
return false;
}
Nous allons utiliser ces annotations de classe pour indiquer à JMH de générer le temps moyen en microsecondes et d’exécuter cinq itérations de préchauffage pour garantir la fiabilité de nos tests:
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
Et lancez chaque test dans une boucle:
@Benchmark
public void searchArrayLoop() {
for (int i = 0; i < SearchData.count; i++) {
searchLoop(SearchData.strings, "T");
}
}
@Benchmark
public void searchArrayAllocNewList() {
for (int i = 0; i < SearchData.count; i++) {
searchList(SearchData.strings, "T");
}
}
@Benchmark
public void searchArrayAllocNewSet() {
for (int i = 0; i < SearchData.count; i++) {
searchSet(SearchData.strings, "S");
}
}
Lorsque nous exécutons 1000 recherches pour chaque méthode, nos résultats ressemblent à ceci:
SearchArrayTest.searchArrayAllocNewList avgt 20 937.851 ± 14.226 us/op
SearchArrayTest.searchArrayAllocNewSet avgt 20 14309.122 ± 193.844 us/op
SearchArrayTest.searchArrayLoop avgt 20 758.060 ± 9.433 us/op
-
La recherche de boucle est plus efficace que d’autres. ** Mais c’est au moins en partie à cause de la façon dont nous utilisons les collections.
Nous créons une nouvelle instance List avec chaque appel à searchList () , ainsi qu’un nouveau List et un nouveau HashSet à chaque appel de searchSet () .
La création de ces objets crée un coût supplémentaire que la boucle dans le tableau n’a pas.
4. Recherche plus efficace
Que se passe-t-il lorsque nous créons des instances uniques de List et Set , puis les réutilisons pour chaque recherche?
Essayons:
public void searchArrayReuseList() {
List asList = Arrays.asList(SearchData.strings);
for (int i = 0; i < SearchData.count; i++) {
asList.contains("T");
}
}
public void searchArrayReuseSet() {
Set asSet = new HashSet<>(Arrays.asList(SearchData.strings));
for (int i = 0; i < SearchData.count; i++) {
asSet.contains("T");
}
}
Nous allons exécuter ces méthodes avec les mêmes annotations JMH que ci-dessus et inclure les résultats de la boucle simple à des fins de comparaison.
Nous voyons des résultats très différents:
SearchArrayTest.searchArrayLoop avgt 20 758.060 ± 9.433 us/op
SearchArrayTest.searchArrayReuseList avgt 20 837.265 ± 11.283 us/op
SearchArrayTest.searchArrayReuseSet avgt 20 14.030 ± 0.197 us/op
-
Bien que la recherche dans la liste soit légèrement plus rapide qu’auparavant, le jeu diminue à moins de 1% du temps requis pour la boucle! **
Maintenant que nous avons supprimé le temps nécessaire à la création de nouvelles collections de chaque recherche, ces résultats ont du sens.
La recherche dans une table de hachage, la structure sous-jacente à un HashSet , a une complexité temporelle de 0 (1), tandis qu’un tableau, qui sous-tend le ArrayList , vaut 0 (n).
5. Recherche binaire
Une autre méthode de recherche dans un tableau est un lien:/java-binary-search[recherche binaire]. Bien que très efficace, une recherche binaire nécessite que le tableau soit trié à l’avance.
Trions le tableau et essayons la recherche binaire:
@Benchmark
public void searchArrayBinarySearch() {
Arrays.sort(SearchData.strings);
for (int i = 0; i < SearchData.count; i++) {
Arrays.binarySearch(SearchData.strings, "T");
}
}
SearchArrayTest.searchArrayBinarySearch avgt 20 26.527 ± 0.376 us/op
La recherche binaire est très rapide, bien que moins efficace que le HashSet: les performances dans le pire des cas pour une recherche binaire sont 0 (log n), ce qui place ses performances entre celles d’une recherche par tableau et d’une table de hachage.
6. Conclusion
Nous avons vu plusieurs méthodes de recherche dans un tableau.
Basé sur nos résultats, un HashSet fonctionne mieux pour effectuer une recherche dans une liste de valeurs. Cependant, nous devons les créer à l’avance et les stocker dans le Set.
Comme toujours, le code source complet des exemples est disponible à l’adresse over sur GitHub .