java数组查找算法实现比较

时间:2021-10-07 10:58:26

java数组查找算法实现比较

本文我们看看java不同数组查找算法,并使用JMH(Java Microbenchmark Harness)比较它们的性能,确定最优算法。

数据准备

我们在数组中随机生成字符串用作示例数据:

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;
}

为在每个基准测试中使用相同的数组,我们声明一个内部类带有数量及数组,用于JMH测试:

@State(Scope.Benchmark)
public static class SearchData {
    static int count = 1000;
    static String[] strings = seedArray(1000);
}

要使用HMH测试,需引用依赖(gradle脚本):

compile group: 'org.openjdk.jmh', name: 'jmh-core', version: '1.20'
compile group: 'org.openjdk.jmh', name: 'jmh-generator-annprocess', version: '1.20'

基本搜索

三种常用的搜索方法为:数组所谓List查找,数组作为Set查找,循环数组检查每个元素。让我们看看这三种方法:

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;
}

接下来我们使用三个类注解告诉JMH输出平均时间,单位为毫秒,进行5个预热迭代确保测试的可靠性:

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)

然后在循环中运行每个测试:

@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");
    }
}

每个测试方法运行1000次,结果大概如下:

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

循环方法最有效,但至少部分原因是取决与我们如何使用集合。每次调用searchList()方法创建新的List实例,每次调用searchSet()方法同时会创建新的List和新的HashSet,创建这些对象会有额外的开销,而数组循环没有。

有效搜索

如果我们只创建依次List和Set的实例,然后重用他们,结果如何呢,让我们试试:

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");
    }
}

使用同样的JMH注解运行这些方法,并同样包裹在循环中执行并比较,结果有很大的差异:

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

List比原先稍微快点,Set下降到不足原来的1%!
现在我们删除了每次搜索需要创建新集合的耗时操作,结果有一定意义。搜索hash表,基于Hashset类,时间复杂度为O(1),而数组,基于ArrayList是O(n).

二分查找

这里介绍另一种搜索方法:二分查找。虽然非常高效,但要求数组必须有序。
让我们排序并尝试二分查找:

@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

二分查找非常快,比HashSet稍微差点,最差情况性能为O(logn),介于数组与hash表之间。

总结

我们介绍了集中方式实现搜索数组。基于测试结果,HashSet性能最佳,但必须先创建并存储其中。