今天来讲一下算法中的插入排序。
- 数组的插入排序
- 链表的插入排序
- 希尔排序
一、最简单的插入排序
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;
}
插入排序的几个注意点,
- 从i = 1开始
- 进入循环前,拷贝带插入元素
- 循环体结束后,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)。