【发布时间】:2016-05-21 17:14:11
【问题描述】:
我已经实现了插入排序和堆排序。理论上,堆排序的时间复杂度为 nlogn,插入的时间复杂度为 n^2。为什么,那么我的 Insertion 实现对 100,000 长的数组进行排序大约要快 6 倍?
我使用 JMH 对每种排序算法的平均时间进行基准测试。 这是我的基准代码:
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
public class MyBenchmark {
// setup the benchmark - create a new array for each iteration
@State(Scope.Thread)
public static class MyState {
int[] array = null;
@Setup(Level.Iteration)
public void doSetup() {
array = createArray(100000, 0, 100);
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
public void insertionSort(MyState state) {
int[] array = state.array;
for (int i = 1; i < array.length; i++) {
int element = array[i];
for (int j = i - 1; j >= 0; j--) {
if (element < array[j]) {
int temp = array[j];
array[j] = element;
array[j + 1] = temp;
} else {
break;
}
}
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
public void heapSort(MyState state) {
int[] array = state.array;
sort(array, array.length);
}
public static void sort(int[] arr, int size) {
for (int i = 0; i < size;) {
maxHeapify(size, arr);
int temp = arr[0];
arr[0] = arr[size - 1];
arr[size - 1] = temp;
size--;
}
}
private static void maxHeapify(int size, int[] arr) {
int nonLeafs = size / 2;
for (int i = nonLeafs; i > 0; i--) {
int arrayPos = heapToArrayPos(i), leftChild = heapToArrayPos(leftChild(i)),
rightChild = heapToArrayPos(rightChild(i));
if (rightChild < size) {
if (arr[rightChild] < arr[leftChild]) {
if (arr[arrayPos] < arr[leftChild]) {
switchWithLeftChild(arrayPos, arr);
}
} else if (arr[arrayPos] < arr[rightChild]) {
switchWithRightChild(arrayPos, arr);
}
} else if (arr[arrayPos] < arr[leftChild]) {
switchWithLeftChild(arrayPos, arr);
}
}
}
private static int heapToArrayPos(int heap) {
return heap - 1;
}
private static int rightChild(int pos) {
return pos * 2 + 1;
}
private static int leftChild(int pos) {
return pos * 2;
}
private static void switchWithRightChild(int pos, int[] arr) {
int father = arr[pos];
int childPos = heapToArrayPos(rightChild(pos + 1)), child = arr[childPos];
arr[childPos] = father;
arr[pos] = child;
}
private static void switchWithLeftChild(int pos, int[] arr) {
int father = arr[pos];
int childPos = heapToArrayPos(leftChild(pos + 1)), child = arr[childPos];
arr[childPos] = father;
arr[pos] = child;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(MyBenchmark.class.getSimpleName()).forks(1).build();
new Runner(opt).run();
}
public static int[] createArray(int length, int minValue, int maxValue) {
return IntStream.generate(() -> ThreadLocalRandom.current().nextInt(minValue, maxValue)).limit(length)
.toArray();
}
public static int[] createArray(int length) {
return createArray(length, 0, 10);
}
public static int[] createArray(int minValue, int maxValue) {
return createArray(10, minValue, maxValue);
}
}
这是基准测试输出:
JMH 1.12(51 天前发布) 虚拟机版本:JDK 1.8.0_65,虚拟机 25.65-b01 VM 调用程序:C:\Program Files\Java\jdk1.8.0_65\jre\bin\java.exe 虚拟机选项:-Dfile.encoding=UTF-8 -Xbootclasspath:C:\Program 文件\Java\jdk1.8.0_65\jre\lib\resources.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\rt.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\jsse.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\jce.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\charsets.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\jfr.jar;C:\Program 文件\Java\jdk1.8.0_65\lib\tools.jar
预热:20 次迭代,每次 1 秒
测量:20 次迭代,每次 1 秒
超时:每次迭代 10 分钟
线程:1 个线程,将同步迭代
基准模式:平均时间,时间/操作
基准测试:org.sample.MyBenchmark.heapSort运行进度:完成 0.00%,预计到达时间 00:01:20
叉子:1 个中的 1 个
预热迭代 1:17.651 秒/操作
预热迭代 2:16.004 秒/操作
预热迭代 3:14.640 秒/操作
预热迭代 4:14.699 秒/操作
预热迭代 5:14.836 秒/操作
预热迭代 6:14.900 秒/操作
预热迭代 7:14.758 秒/操作
预热迭代 8:15.084 秒/操作
预热迭代 9:15.652 秒/操作
预热迭代 10:15.121 秒/操作
预热迭代 11:15.315 秒/操作
预热迭代 12:15.299 秒/操作
预热迭代 13:15.234 秒/操作
预热迭代 14:14.822 秒/操作
预热迭代 15:15.078 秒/操作
预热迭代 16:15.565 秒/操作
预热迭代 17:15.509 秒/操作
预热迭代 18:15.189 秒/操作
预热迭代 19:14.748 秒/操作
预热迭代 20:14.902 秒/操作
迭代 1:14.888 秒/操作
迭代 2:15.381 秒/操作
迭代 3:16.099 秒/操作
迭代 4:15.536 秒/操作
迭代 5:15.635 秒/操作
迭代 6:16.446 秒/操作
迭代 7:16.034 秒/操作
迭代 8:15.828 秒/操作
迭代 9:15.666 秒/操作
迭代 10:16.071 秒/操作
迭代 11:15.962 秒/操作
迭代 12:15.777 秒/操作
迭代 13:15.757 秒/操作
迭代 14:15.424 秒/操作
迭代 15:15.449 秒/操作
迭代 16:15.920 秒/操作
迭代 17:14.609 秒/操作
迭代 18:14.651 秒/操作
迭代 19:14.661 秒/操作
迭代 20:14.607 秒/操作结果“堆排序”: 15.520 ±(99.9%) 0.486 s/op [平均值] (min, avg, max) = (14.607, 15.520, 16.446), stdev = 0.560 CI (99.9%): [15.034, 16.006](假设正态分布)
JMH 1.12(51 天前发布) 虚拟机版本:JDK 1.8.0_65,虚拟机 25.65-b01 VM 调用程序:C:\Program Files\Java\jdk1.8.0_65\jre\bin\java.exe 虚拟机选项:-Dfile.encoding=UTF-8 -Xbootclasspath:C:\Program Files\Java\jdk1.8.0_65\jre\lib\resources.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\rt.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\jsse.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\jce.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\charsets.jar;C:\Program 文件\Java\jdk1.8.0_65\jre\lib\jfr.jar;C:\Program 文件\Java\jdk1.8.0_65\lib\tools.jar 预热:20 次迭代,每次 1 秒 测量:20 次迭代,每次 1 秒 超时:每次迭代 10 分钟 线程:1个线程,将同步迭代 基准模式:平均时间,时间/操作 基准测试:org.sample.MyBenchmark.insertionSort
运行进度:完成 50.00%,预计到达时间 00:10:15 叉子:1 个中的 1 个 预热迭代 1:1.726 s/op 预热迭代 2:1.636 s/op 预热迭代 3:1.968 s/op 预热迭代 4:1.970 秒/操作 预热迭代 5:1.961 s/op 预热迭代 6:1.966 s/op 热身迭代 7:1.962 s/op 预热迭代 8:1.961 s/op 热身迭代 9:1.959 秒/操作 热身迭代 10:1.965 s/op 热身迭代 11:1.966 s/op 预热迭代 12:1.970 s/op 热身迭代 13:1.964 s/op 热身迭代 14:1.952 s/op 预热迭代 15:1.955 s/op 预热迭代 16:1.956 s/op 预热迭代 17:1.972 s/op 预热迭代 18:1.966 s/op 热身迭代 19:1.954 s/op 热身迭代 20:1.956 秒/操作
迭代 1:1.969 秒/操作
迭代 2:1.963 秒/操作
迭代 3:2.050 秒/操作
迭代 4:2.019 秒/操作 迭代 5:1.934 秒/操作
迭代 6:1.953 秒/操作
迭代 7:1.961 秒/操作
迭代 8:1.972 秒/操作
迭代 9:1.957 秒/操作
迭代 10:1.956 秒/操作
迭代 11:1.975 秒/操作
迭代 12:1.950 秒/操作
迭代 13:1.965 秒/操作
迭代 14:1.961 秒/操作
迭代 15:1.950 秒/操作
迭代 16:1.956 秒/操作
迭代 17:1.975 秒/操作
迭代 18:1.966 秒/操作
迭代 19:1.959 秒/操作
迭代 20:1.965 秒/操作结果“插入排序”:
1.968 ±(99.9%) 0.022 s/op [平均] (min, avg, max) = (1.934, 1.968, 2.050), stdev = 0.025 CI (99.9%): [1.946, 1.990](假设正态分布)运行完成。总时间:00:09:55
基准模式 Cnt 得分误差单位
MyBenchmark.heapSort avgt 20 12.692 ± 0.282 s/op
MyBenchmark.insertionSort avgt 20 2.024 ± 0.020 s/op
编辑:因为我已经发布了问题,所以我在基准测试之前添加了@setup 来设置数组,所以数组创建操作不会是一个因素。我再次运行基准测试,插入排序的结果几乎相同。堆排序基准的平均速度提高了 3 秒。我只发布了结果的更新摘要。
【问题讨论】:
-
尽管有您的评论,但这是重复的;因为为了对 Java 进行基准测试——你正在做的事情——你首先需要学习如何。由于缺乏 JVM 的预热和迭代,你真正需要的是类加载器和 JIT。当您在 JMH 中编写了一些基准后,再回来提出另一个问题。
-
你没有违反任何复杂性,你只是没有按照你认为的时间来计时。很抱歉,但如果你想用 Java 进行有意义的基准测试,你将不得不学习如何编写它们。
-
Break 只留下外部 for 循环。两者都暗示。是正确的。我已经测试过了。
-
MaxG,您是否尝试过针对不同阵列大小运行基准测试?对于 10 倍大的阵列,趋势是否会保持不变?
-
堆排序很可能会花费大量时间,因为您错误地实现了它。您的
sort方法调用maxHeapifysize次,并且每次调用都会进行size/2迭代。所以你实现的是一个 O(n^2) 排序算法。您应该只需要拨打maxHeapify一次。
标签: java algorithm sorting insertion-sort heapsort