【问题标题】:Heap Sort vs Insertion Sort JMH benchmark: why my insertion impl. takes less time?堆排序与插入排序 JMH 基准测试:为什么我的插入实现。需要更少的时间?
【发布时间】: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 方法调用 maxHeapify size 次,并且每次调用都会进行 size/2 迭代。所以你实现的是一个 O(n^2) 排序算法。您应该只需要拨打maxHeapify 一次。

标签: java algorithm sorting insertion-sort heapsort


【解决方案1】:

您的堆排序实现不正确。您发布的代码似乎正在进行选择排序。也就是说,对于它调用maxHeapify 的每个项目,获取堆中的第一个项目,将其放在最后,并减少计数。所以maxHeapify 被称为size 次,每次都减小大小。 maxHeapify 中内部循环的迭代次数最终类似于 (n^2)/4

您已经实现了复杂度为 O(n^2) 的美化选择排序。

进行就地堆排序的技巧是首先构建堆——一次——然后重新排列它以进行排序。你拨打maxHeapify一次:

maxHeapify(size, arr);

完成后,您将拥有一个有效的最大堆,其中最大的项目位于 arr[0] 等处。这需要 O(n) 时间。

你想要的是一个升序排列的数组。为此,您构建一个循环,从堆中复制最大的项目(即arr[0])并临时保存它。然后,取出堆中的最后一项,将计数减一,然后将该项重新插入顶部,根据需要将其向下筛选。最后,将前一个最大的项目放在最后一个项目之前占据的位置。当 count 为 0 时,你有一个排序数组:

int count = size;
while (count > 0)
{
    int save = arr[0];      // save the largest item
    arr[0] = arr[count-1];  // move last item to top
    arr[count-1] = save;    // and place the largest item
    count = count - 1;      // reduce the count
    SiftDown(0);            // sift item into place
}

您所做的只是在堆上连续调用removeMax,并将结果存储回数组中空出的位置。

SiftDown 与将项插入堆时使用的方法相同。

有关使用 O(n) heapify 方法构建堆的完整示例,请参阅我的博客文章 A Simple Heap of Integers。它在 C# 中,但我认为很简单,如果你了解 Java,你就可以理解它。我没有展示如何进行排序部分,但是使用该代码和上面的几行,您应该可以做到。

【讨论】:

  • @MaxG:查看更正后的代码。和以前一样,它可能会导致错误。
猜你喜欢
  • 2014-12-04
  • 2011-09-01
  • 2014-12-11
  • 1970-01-01
  • 1970-01-01
  • 2016-02-18
  • 1970-01-01
  • 1970-01-01
  • 2013-12-15
相关资源
最近更新 更多