插入排序

目录

一、基本原理

二、代码实现

1、插入排序的基本实现(直接插入排序)

2、寻找插入位置时,利用已排序部分的有序性,采用二分查找进行优化(二分插入排序)

3、递归版本的直接插入排序

4、希尔排序


 

一、基本原理

插入排序的基本原理为,将数组分成已排序和待排序的两部分,初始时已排序部分只有一个元素([0 ... 0]),其他元素均在未排序部分中([1 ... arr.length-1])。每一轮将一个待排序的元素(从左到右进行,稳定排序)插入到已排序部分的合适位置中,直到待排序部分为空。具体的插入流程为,从右到左遍历已排序部分,如果当前待排序的元素小于遍历到的元素,则将遍历到的元素向后移动一位,直到腾出一个位置用于存放当前元素。

插入排序的本质在于不断往已经排好的序列中插入一个数,要求插入后序列仍然有序。其基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据序列。

插入排序其实就像玩扑克牌发牌时一样,手里的牌是已经排序的部分,桌上的牌是待排序的部分。插入排序适用于少量数据的排序,时间复杂度为O(n^2),是稳定的排序算法。

插入排序是稳定的排序,其时间复杂度是O(n^2)。所以,元素数量较少的时候其工作量较小。此外,当数组基本有序的时候,其工作量也较小,因为不用进行频繁的比较交换。

 

二、代码实现

1、插入排序的基本实现(直接插入排序)

其伪代码为:

Loop from i = 1 to n-1.
    Pick element arr[i] and insert it into sorted sequence arr[0…i-1]

Java代码实现为:

public class Sort {

    //直接插入排序
    public static void insertionSort(int[] arr) {
        for (int i=1; i<arr.length; i++) {  //遍历未排序部分的每一个元素
            int cur = arr[i];
            int k = i - 1;  //已排序部分的最后一个元素
            while (k >= 0 && cur < arr[k]) {
                arr[k+1] = arr[k];
                k--;
            }
            //将当前元素插入到腾出的位置
            arr[k + 1] = cur;
        }
    }
}

2、寻找插入位置时,利用已排序部分的有序性,采用二分查找进行优化(二分插入排序)

如果比较操作的代价比交换操作大的话,可以采用二分查找来减少比较操作的次数。

public class Sort {

    //二分插入排序
    public static void insertionSort1(int[] arr) {
        for (int i=1; i<arr.length; i++) {
            int cur = arr[i];
            //查找已排序部分中最后一个小于等于当前元素的元素的索引
            int index = lastSmallerIndex(arr, 0, i-1, cur);
            //应该移动的索引范围为[index+1, i-1]
            for (int j=i-1; j>=index+1; j--) {
                arr[j+1] = arr[j];
            }
            //存放当前元素到腾出的位置
            arr[index+1] = cur;
        }
    }

    private static int lastSmallerIndex(int[] arr, int left, int right, int target) {
        while (left <= right) {
            int mid = (right - left) / 2 + left;

            if (arr[mid] <= target) {
                //if (mid + 1 < arr.length && arr[mid+1] > target) {    //error
                //if (mid == right || (mid + 1 <= right && arr[mid+1] > target)) {
                if (mid == right || arr[mid+1] > target) {
                    return mid;
                }
                //不是最后一个小于等于target的元素
                left = mid + 1;

            } else {
                right = mid - 1;
            }
        }

        //如果指定范围中没有小于目标元素的元素,则返回-1
        return -1;
    }

}

3、递归版本的直接插入排序

现在实现一下递归版本的直接插入排序

public class Sort {

    //递归版本的插入排序
    public static void insertionSort2(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }

        //insertionSortHelper(arr, 1);
        //
        insertionSortHelper1(arr, arr.length);
    }

    //辅助函数,n为剩余元素个数
    private static void insertionSortHelper1(int[] arr, int n) {
        //出口:
        if (n <= 1) {
            return;
        }

        //子问题:缩小数据规模,先将除了最后一个元素的所有元素都排序好
        insertionSortHelper1(arr, n-1);

        //基本操作:将最后一个元素插入前面已经排好序的有序序列中[0 ... n-2]
        int temp = arr[n-1];    //暂存最后一个元素
        int i = 0;
        for (i=n-2; i>=0 && arr[i]>temp; i--) {
            arr[i+1] = arr[i];

        }
        arr[i+1] = temp;
    }

    //辅助函数,cur为当前要插入的元素
    private static void insertionSortHelper(int[] arr, int cur) {
        //出口:
        if (cur == arr.length) {
            return;
        }

        //基本操作:将当前元素插入已排序序列中
        int temp = arr[cur];
        int i = 0;
        for (i=cur-1; i>=0 && arr[i]>temp; i--) {
            arr[i+1] = arr[i];
        }
        arr[i+1] = temp;

        //子问题:缩小数据规模
        insertionSortHelper(arr, cur+1);
    }

}

4、希尔排序

希尔排序是对插入排序进行优化,由于直接插入排序在数组大部分元素之间有序、元素数量较少时工作量较小,希尔排序就是将数组预处理成基本有序,再使用直接插入排序。具体预处理的方式是分组,将一定间隔的元素分为一组,分别对各个组进行直接插入排序(此时每个组的数据量较少)。不断缩小间隔,当间隔缩小为1时,也就是整个数组分为一组,再进行一次直接插入排序即可(此时数组基本有序)。

希尔排序实际上是一种分组插入算法。直接插入排序的增量是1,而希尔排序则是不断改变增量,直到增量变为1。由于希尔排序中划分在不同组的相同元素可能因为各自组的排序而改变相对次序(各个组内部依然是稳定排序),希尔排序是不稳定排序,与直接插入排序不同。

public class Sort {

    //希尔排序
    public static void shellSort(int[] arr) {
        //希尔排序的增量
        int add = arr.length;

        while (add > 1) {
            //每次折半
            add = add / 2;

            for (int i=0; i<add; i++) {    //各个分组
                /*
                for (int j=i+add; j<arr.length; j=j+add) {    //对各个分组分别执行带间隔的插入排序
                    int temp = arr[j];
                    int k = j - add;
                    for (; k>=0 && arr[k]>temp; k=k-add) {
                        arr[k+add] = arr[k];
                    }
                    arr[k+add] = temp;
                }
                 */
                insertionSort(arr, i, add);
            }
        }
    }

    //辅助插入排序(带间隔),对各个分组分别执行带间隔的插入排序
    private static void insertionSort(int[] arr, int start, int add) {
        for (int j=start+add; j<arr.length; j=j+add) {
            int temp = arr[j];
            int k = j - add;
            for (; k>=0 && arr[k]>temp; k=k-add) {
                arr[k+add] = arr[k];
            }
            arr[k+add] = temp;
        }
    }

}

希尔排序可以有效减少直接插入排序的工作量,不过,在极端的情况下可能增加额外的分组所带来的工作量。比如当数组为2, 1, 5, 3, 7, 6, 9, 8时,无论是以4为增量,还是以2为增量,每组内部的元素都没有任何交换。一直到我们把增量缩减为1,数组才会按照直接插入排序的方式进行调整。

而这跟我们分组的方式有关,像上面的分组方式是比较粗略的等比方式。其实有更合理的分组方式,如Hibbard增量和Sedgewick增量。Hibbard增量的序列为:1, 3, 7, 15, ...,(2^k-1),利用此种增量方式的希尔排序,最坏时间复杂度是O(n^(3/2))。Sedgewick增量的序列为:1, 5, 19, 41, 109, ...,( 9*4^k - 9*2^k + 1 / 4^k - 3*2^k + 1),利用此种增量方式的希尔排序,最坏时间复杂度是O(n^(4/3))。

 

参考:

https://baike.baidu.com/item/%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F

https://baike.baidu.com/item/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值