这是插入排序算法的可接受实现吗?

时间:2015-04-04 13:59:25

标签: java algorithm sorting insertion-sort

插入排序算法的以下Java实现出现在Noel Markham的 Java Programming Interviews Exposed 的第28页上:

public static List<Integer> insertSort(final List<Integer> numbers) {
    final List<Integer> sortedList = new LinkedList<>();
    originalList: for (Integer number : numbers) {
        for (int i = 0; i < sortedList.size(); i++) {
            if (number < sortedList.get(i)) {
                sortedList.add(i, number);
                continue originalList;
            }
        }
        sortedList.add(sortedList.size(), number);
    }
    return sortedList;
}

我的一位同事回顾了这段代码后发现,作为对“请实施插入排序算法”的面试问题的回答,这是不可接受的。他认为数组对于排序列表来说是更合适的数据结构。但是,正如马克姆在同一页上解释的那样:

  

链接列表在添加元素中非常有效   列表,只需重新排列列表中节点的指针即可。   如果使用了ArrayList,则向中间添加元素   昂贵。 ArrayList由数组支持,因此插入   列表的前面或中间意味着所有后续元素必须是   沿着一个移位到阵列中的新插槽。这可能非常   如果你有一个包含数百万行的列表,那就太贵了,特别是如果   你是在列表的早期插入的。

这是可接受的实施吗?

3 个答案:

答案 0 :(得分:1)

考虑以下用于插入排序的伪代码:

for i ← 1 to length(A) - 1
    j ← i
    while j > 0 and A[j-1] > A[j]
        swap A[j] and A[j-1]
        j ← j - 1
    end while
end for

来源:http://en.wikipedia.org/wiki/Insertion_sort

1)所以,在这个过程中,你持有每个元素,并将它与前一个元素进行比较,如果前一个元素大于当前元素,则交换,并且这个元素会一直发生,直到条件不满足为止。

2)算法的工作原理是逐个元素交换,而不是将元素插入到应该放置的位置。注意: - 每个交换是o(1)。

3)因此,在这种形式中,如果使用列表,则需要执行2个操作,将前任和当前元素连接起来,反之亦然。另一方面,排序数组只需要一步。

因此,在这种方法中,排序数组比列表更有意义。

  

现在,如果插入排序的方法是直接将当前元素插入到它所适合的位置,那么链表就会更好。

注意: - 排序数组或排序链表,整个过程是相同的,它是中间步骤,而不是排序。

答案 1 :(得分:1)

理论上,Markham可能是一个很好的常识:在链表中插入不应该花费太多(分配新节点,一些参考分配),甚至列表末尾的插入也很便宜,因为{ {1}}实际上是一个双链表,并保留对最后一个元素的引用。

插入新节点(用于LinkedList)和移动部分数组(用于LinkedList)之间的争论至少要进行测试,因为ArrayList使用{{ 1}}应该真正优化这种工作。

你可以随时随地看到“谨防微观基准”。好吧,我想说,当你想大致了解发生了什么时,微基准可以给你一些提示......

以下是您想要比较的两种方法的微观工作台,加上ArrayList#add(int i, E)有一些参考时间。注意插入排序平均为System.arrayCopy(),与Collections.sort()的{​​{1}} tim sort 进行比较。

插入排序的建议实现中,我只是传递排序列表实现,以便为两个测试使用相同的函数。

O(N^2)

接下来是一个方法,它将测量排序随机整数列表并打印它所花费的时间:

Collections

函数O(Nlog(N))将循环增加数组大小,并使用3种方法对相同的随机数组进行排序,并比较排序列表。

public static List<Integer> insertSort(final List<Integer> numbers,
                                       final List<Integer> sortedList) {
    //final List<Integer> sortedList = new ArrayList<>();
    originalList: for (Integer number : numbers) {
        for (int i = 0; i < sortedList.size(); i++) {
            if (number < sortedList.get(i)) {
                sortedList.add(i, number);
                continue originalList;
            }
        }
        sortedList.add(sortedList.size(), number);
    }
    return sortedList;
}

所有这些都来自public static List<Integer> bench(List<Integer> ints, String tag, Function<List<Integer>, List<Integer>> sortf) { long start = System.nanoTime(); List<Integer> sortedInts = sortf.apply(ints); long end = System.nanoTime(); System.out.println(String.format("type: %6s size: %7d time(ms): %5d", tag, ints.size(), (end-start)/1000000)); return sortedInts; } 。从500的数组开始,然后增加大小直到 5,000 (确实非常小的尺寸!)。执行环境是MBP 2,5 GHz英特尔酷睿i7。

microBench()

无需绘制图片以了解插入排序 public static void microBench(int start, int end, int step) { for (int m = start; m <= end; m+=step) { List<Integer> ints = new Random() .ints(m, 0, m).boxed() .collect(Collectors.toList()); List<Integer> l1 = bench(ints, "coll", (List<Integer> l) -> { List<Integer> list = new ArrayList<>(l); Collections.sort(list); return list; }); List<Integer> l2 = bench(ints, "array", (List<Integer> l) -> insertSort(l, new ArrayList<Integer>())); if (!l1.equals(l2)) { System.out.println("Oooops array"); } List<Integer> l3 = bench(ints, "linked", (List<Integer> l) -> insertSort(l, new LinkedList<Integer>())); if (!l1.equals(l3)) { System.out.println("Oooops linked"); } } } 不是赢家! 14秒排序5000英寸。但是使用 main插入并不是那么糟糕。

我删除了public static void main(String[] args) { microBench(1000, 5000, 1000); } type: coll size: 1000 time(ms): 1 type: array size: 1000 time(ms): 8 type: linked size: 1000 time(ms): 66 type: coll size: 2000 time(ms): 1 type: array size: 2000 time(ms): 2 type: linked size: 2000 time(ms): 507 type: coll size: 3000 time(ms): 2 type: array size: 3000 time(ms): 4 type: linked size: 3000 time(ms): 2283 type: coll size: 4000 time(ms): 1 type: array size: 4000 time(ms): 9 type: linked size: 4000 time(ms): 6866 type: coll size: 5000 time(ms): 1 type: array size: 5000 time(ms): 13 type: linked size: 5000 time(ms): 14842 上的工作台,并推动了最大数组 100,000

LinkedList

4.5秒vs 15毫秒。毫无疑问,与tim排序/合并排序O(NlogN)相比,插入排序仍为O(N ^ 2)......

由于Markham正在编写大约1,000,000个元素数组,所以我只选择了实验中的唯一实现(来自3个测试版),可以正确地执行它,并使用ArrayList删除插入排序

LinkedList
1,000,000的

223毫秒

结论,要小心人们可以写什么,并在可以做到的时候测试自己! - 顺便说一句,你的同事是对的。 并且,如果你必须排序,插入排序,通常不是你要去的方式。

答案 2 :(得分:0)

由于get(i)不支持LinkedList,因此您必须线性迭代内部循环中的链接列表而不是RandomAccess。 否则,每个元素访问都需要i个步骤才能找到相应的元素和基准,因为在比较实现例如ArrayList时,另一个答案中的元素和基准会产生误导性结果。

public static List<Integer> insertSort(final List<Integer> numbers) {
    final LinkedList<Integer> sortedList = new LinkedList<>();
    originalList: for (Integer number : numbers) {
        int i = 0;
        for (Integer compare : sortedList) {
            if (number < compare) {
                sortedList.add(i, number);
                continue originalList;
            }
            ++i;
        }
        sortedList.addLast(number);
    }
    return sortedList;
}