Vérifier si un tableau Java contient une valeur

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);
}
  • 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).

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 .