算法 - 插入排序和希尔排序

今天来讲一下算法中的插入排序。

  1. 数组的插入排序
  2. 链表的插入排序
  3. 希尔排序

一、最简单的插入排序

9->3->4->2->6->7->5->1

先将9视为有序,然后把它的后一个元素插入有序序列中,有序序列长度加一。

3->9->4->2->6->7->5->1

继而将4插入,

3->4->9->2->6->7->5->1

然后将2插入,以此类推。

代码实现如下:

public int[] insertionSort(int[] arr){
    for(int i = 1; i < arr.length; i++){
        int key = arr[i];    //待插入元素         int j = i - 1;       //待插入元素的前一个元素,作为有序序列的边界         while (j >= 0 && arr[j] > key){
            arr[j + 1] = arr[j];       //有序序列元素向右移动,覆盖了待插入元素的位置。             j--;
        }
        arr[j + 1] = key; //此边界处已经执行了j--,所以应当插入j + 1的位置     }
    return arr;
}

插入排序的几个注意点,

  1. 从i = 1开始
  2. 进入循环前,拷贝带插入元素
  3. 循环体结束后,j– 已经执行了,所以应当插入到下标为 j+1 的位置。

二、单链表的插入排序

Question : sort a linked list using insertion sort.

链表插入排序的难点在于,我们无法通过下标获取到有序序列的长度,更无法找到插入节点的上一个节点倒着进行大小比较,但是链表的优点在于插入方便。

所以您可以先不看答案,想一下怎样解决单链表的插入排序。

Answer:

class ListNode{
        ListNode next;
        int val;
        ListNode(int val){this.val = val;}
}
public ListNode insertionSort(ListNode head){
    ListNode newHead = new ListNode(0);
    ListNode cur = head;
    ListNode pre = newHead;
    ListNode next;
    while(cur != null){
        next = cur.next;
        //找到正确的插入位置         while(pre.next!=null && pre.next.val < cur.val){
            pre = pre.next;
        }
        //把cur插入到pre和pre.next之间         cur.next = pre.next;
        pre.next = cur;
        //重置pre         pre = newHead;
        //cur前进,为下一轮循环做准备         cur = next;
    }
    return newHead.next;
}

list status before sorting:

Unordered list : 9->3->4->2->6->7->5->1->null

Ordered list : newHead->null

We think 9 is ordered , so:

Unordered list : 3->4->2->6->7->5->1->null

Ordered list : newHead->9->null

and then:

Unordered list : 4->2->6->7->5->1->null

Ordered list : newHead->3->9->null

We look like get a new linked list, thanks for the help of newHead!

我们通过一个新的链表来储存已排序的元素,未排序的元素放在原先的链表中,通过这种方式,我们实现了链表的插入排序。

以上两种插入排序,最好的情况时间复杂度是O(n);最差的情况时间复杂度是O(n^2);平均时间复杂度也是O(n^2);空间复杂度都是O(1)。

三、希尔排序

这次我们把数组分组,对每组单独进行插入排序,每排序一次之后,适当扩大分组大小,最后直到整个数组被分成一组。

这就是希尔排序。

请注意,分组是根据某个增量(间隔),从第一个元素开始分组。这里我们第一次分组时取增量为数组长度的一半,每次排序后,增量减为原来的一半。

第一次分组:

9->3->4->2->6->7->5->1

增量为4,分为 9 6,3 7,4 5,2 1 四组。

排序后:

6->3->4->1->9->7->5->2

第二次分组,增量为2:

分为6495,3172两组

排序后:

4->1->5->2->6->3->9->7

最后一次分组,增量为1,分为一组,进行插入排序后,排序完成。

public void shellSort(Integer[] sortList) {  
            int i, j, step;  
            int len = sortList.length;  
            // 步长除以2             for (step = len / 2; step > 0; step /= 2)  {
                //分别对每个分组进行直接插入排序                 for (i = 0; i < step; i++)   
                {  
                    for (j = i + step; j < len; j += step)  
                        if (sortList[j] < sortList[j - step]) {  
                            int temp = sortList[j];  
                            int k = j - step;  
                            while (k >= 0 && sortList[k] > temp) {  
                                sortList[k + step] = sortList[k];  
                                k -= step;  
                            }  
                            sortList[k + step] = temp;  
                        }  
                }  
            }
        }

希尔排序为什么好呢?

我们可以定性的思考一下:设想这样一种情况,1到100,这100个数随机的散落在数组中,而元素5在靠近末尾的位置,经过几次分组排序后,5将有很大可能放进靠近开头的位置,除非一个分组中,异常巧合的包含了1,2,3,4,5。

所以希尔排序克服了插入排序的一大缺点:一次只能给元素移动一个位置,即使我们可以”预测“到这个元素将处于较为靠前或靠后的位置,我们也只能慢慢移动。

希尔排序的过程,提供了一个”宏观有序“的状态,为最后的插入排序打下了基础。

希尔排序一定好么?

答案是不一定,设想一个有序序列:1,2,3,4。

插入排序进行了3次比较(1>2? 2>3? 3>4?)

而希尔排序进行了5次比较(1>3? 2>4?加上普通插入的3次)

但是大部分情况下,希尔排序要比直接插入排序好得多。

对于希尔排序,最好的情况,时间复杂度是O(n^1.3);最坏的情况,时间复杂度是O(n^2);空间复杂度是O(1)。

    原文作者:李井瑞
    原文地址: https://zhuanlan.zhihu.com/p/34391850
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞