插入排序及优化

1,762 阅读1分钟

冒泡排序及优化

选择排序(SelectionSort)

堆排序(HeapSort)

插入排序及优化

升序

插入排序类似于打牌时码牌行为的的排序

平均时间复杂度是 O(N²)

核心思想

  1. 将序列分成两部分,头部是已排序好的,尾部是待排序的。

  2. 从头开始扫描每个元素,每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然有序。

代码实现

public class InsertionSort<E extends Comparable<E>> extends Sort<E> {
    @Override
    protected void sort() {
        for (int begin = 1; begin < array.length; begin++) {
            int cur = begin;
            while (cur > 0 && cmp(cur, cur - 1) < 0) {
                swap(cur, --cur);
            }
        }
    }
}

逆序对

逆序对个数和插入排序的时间复杂度是成正比的。

如果元素全逆行,那么它的时间复杂度可以达到O(n*n)

如果没有逆序对,即数组完全升序,时间复杂度是O(n)

测试输入:

【HeapSort】
稳定性:false 	耗时:0.002s(2ms) 	比较:1.68万	 交换:999
------------------------------------------------------------------
【InsertionSort】
稳定性:true 	耗时:0.006s(6ms) 	比较:24.39万	 交换:24.29万
------------------------------------------------------------------
【SelectionSort】
稳定性:false 	耗时:0.007s(7ms) 	比较:49.95万	 交换:999
------------------------------------------------------------------
【BubbleSort】
稳定性:true 	耗时:0.008s(8ms) 	比较:21.81万	 交换:13.84万
------------------------------------------------------------------

Process finished with exit code 0

优化一

可以看出,上面的写法性能很差,主要是交换次数太多了。

解决思路:将 交换 改成 挪动

  1. 将待插入的元素备份。
  2. 将头部元素中比待插入元素大的都往后 挪动一位。
  3. 将备份元素插入合适的位置。

挪动交换代码量少多了,交换需要三步,挪动只需要一步。

private void sort2() {
        for (int begin = 1; begin < array.length; begin++) {
            int cur = begin;
            E element = array[cur];
            while (cur > 0 && cmp(element, array[cur - 1]) < 0) {
                array[cur] = array[--cur];
            }
            array[cur] = element;
        }
    }

优化后的输出:

【InsertionSort2】 使用挪位代替交换
稳定性:true 	耗时:0.006s(6ms) 	比较:25.43万	 交换:0
------------------------------------------------------------------
【InsertionSort1】最原始的
稳定性:true 	耗时:0.009s(9ms) 	比较:25.43万	 交换:25.33万
------------------------------------------------------------------

Process finished with exit code 0

可以看到是有提升的,交换次数没了。

while循环执行的越多,优化的性能越多。其实就是 逆序对越多,优化越明显。

优化二

可以通过优化 比较次数来达到优化目的。

插入排序第二部:从头开始扫描每个元素,每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然有序。

之前找到合适的位置都是通过for循环遍历一遍,时间复杂度是O(n)。我们可以通过优化这查找位置的方法来提升时间。

注意到 **头部序列其实是排序好的,所以我们可以通过二分搜索来查找合适的位置。**二分查找的时间复杂度是O(logN)。

		/**
     * 找到index元素待插入的位置
     * <p>
     * 已经排好的区域是[0,index)
     *
     * @param index 待插入元素的位置
     * @return 应该插入的位置
     */
    private int search(int index) {
        E v = array[index];
        int begin = 0;
        int end = index;
        while (begin < end) {
            int mid = (begin + end) >> 1;
            if (cmp(v, array[mid]) < 0) {
                end = mid;
            } else {
                begin = mid + 1;
            }
        }
        return begin;

    }

注意上面的代码和二分搜索有点不一样。搜索插入的位置,应该是最近大于待插入元素的位置。

知道应该插入的位置之后,我们还需要将从该位置到尾部的数据都往后移动一位,腾出位置插入目标值。

 private void insert(int begin, int insertIndex) {
        //备份
        E element = array[begin];
        //将[insertIndex,begin)位置像后挪
        for (int i = begin; i > insertIndex; i--) {
            array[i] = array[i - 1];
        }
        array[insertIndex] = element;
    }


整体代码是:

		private void sort3() {
        for (int begin = 1; begin < array.length; begin++) {
            //头部是有序的,使用二分查找,复杂度就是O(logn)
            int insertIndex = search(begin);
            insert(begin, insertIndex);
        }
    }

    private void insert(int begin, int insertIndex) {
        //备份
        E element = array[begin];
        //将[insertIndex,begin)位置像后挪
        for (int i = begin; i > insertIndex; i--) {
            array[i] = array[i - 1];
        }
        array[insertIndex] = element;
    }

    /**
     * 找到index元素待插入的位置
     * <p>
     * 已经排好的区域是[0,index)
     *
     * @param index 待插入元素的位置
     * @return 应该插入的位置
     */
    private int search(int index) {
        E v = array[index];
        int begin = 0;
        int end = index;
        while (begin < end) {
            int mid = (begin + end) >> 1;
            if (cmp(v, array[mid]) < 0) {
                end = mid;
            } else {
                begin = mid + 1;
            }
        }
        return begin;

    }

对比下三种方式的输出:

【InsertionSort】 二分搜索优化的
稳定性:true 	耗时:0.003s(3ms) 	比较:8559	 交换:0
------------------------------------------------------------------
【InsertionSort2】 使用挪位代替交换
稳定性:true 	耗时:0.006s(6ms) 	比较:25.43万	 交换:0
------------------------------------------------------------------
【InsertionSort1】最原始的
稳定性:true 	耗时:0.009s(9ms) 	比较:25.43万	 交换:25.33万
------------------------------------------------------------------

Process finished with exit code 0

使用了二分搜索后,只是减少了查找插入位置的比较次数,但是挪动位置的时间复杂度仍然是O(N),所以插入排序的平均时间复杂度仍然是O(n²)