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性能最佳,但必须先创建并存储其中。