插入排序是一种简单直观的排序方法,其基本思想是在于每次将一个待排序的记录, 按其关键字大小插入到前面已经排好序的子序列中,直到全部记录插入完成。
插入排序可分为:直接插入排序、折半插入排序和希尔排序。
1、直接插入排序
插入排序在实现上通常采用就地排序(空间复杂度为O(1)),因此在从后面的比较过程中,需要反复的把已经排好序的元素逐步向后挪位,为新元素插入空间。
public class InsertSort {
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 8, 6, 7, 9};
insertSort(arr);
}
public static void insertSort(int[] arr) {
int i, j, temp;
for (i = 1; i < arr.length; i++) {
if(arr[i] < arr[i-1]) {
temp = arr[i]; // temp为哨兵
for (j = i-1; temp < arr[j]; j--) {
arr[j+1] = arr[j];
}
arr[j+1] = temp;
}
}
System.out.println(Arrays.toString(arr));
}
}
空间效率:仅使用了常数个辅助空间,因而空间复杂度为O(1)
时间效率:在排序过程中,向有序子表中逐个的插入元素的操作进行了 n-1 趟,每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。
在最好的情况下,表中的元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因此时间复杂度为O(n).
在最坏的情况下,表中的元素顺序刚好与排序结果中的元素顺序相反(逆序)时,总的比较次数达到最大 ,总的移动数也达到最大。
平均情况下,考虑待排序的表中的元素是随机的,此时可以取上述最好与最坏的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数约为n^(2)/4。
由此,直接插入排序的时间复杂度为O(n^(2)),虽然折半查找排序算法的时间复杂度也有O(n^(2)),但对于数据量比较小的排序表,折半插入排序往往能表现出很好的性能。
直接插入排序是一个稳定的插入排序。适用于顺序排序存储和链式存储的线性表,当为链式存储时,可以从前往后查找指定元素的位置。注意:大部分排序算法都仅适用于顺序存储的线性表。
2、折半插入排序
直接插入排序中,总是边比较边移动元素。下面将比较和移动查找分离开来,即先折半查找出元素的待插入位置,然后再统一的移动待插入位置之后的所有元素。当排序表为顺序存储的线性表时,可以对直接插入排序算法作如下改进:
由于是顺序存储的线性表,所以查找有序子表时可以用折半查找来实现。在确定出待插入位置后,既可以统一的向后移动元素了。
public class InsertSort {
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 8, 6, 7, 9};
insertSort(arr);
}
public static void insertSort(int[] arr) {
int i, j, high, low, mid, temp;
for (i = 1; i < arr.length; i++) {
low = 0;
high = i - 1;
temp = arr[i];
while(low <= high) {
mid = (low + high)/2;
if(temp < arr[mid]) {
high = mid - 1;
}else {
low = mid + 1;
}
}
for (j = i-1; j > high; j--) {
arr[j+1] = arr[j];
}
arr[high+1] = temp;
}
System.out.println(Arrays.toString(arr));
}
}
不难看出折半插入排序仅仅是减少了比较元素的个数,约为O(Nlog2N),该比较次数与待排序表的初始状态无关,仅仅取决于表中的元素个数n;而元素移动的次数并没有改变,他依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为O(n2)。折半插入排序是一个稳定的排序方法。
3、希尔排序
直接插入排序适用于基本有序的排序表和数据量不大的排序表。基于这两点,提出了希尔排序,又称为缩小增量排序。
希尔排序的基本思想是:先将待排序表分割成若干个子表,分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。过程如下:
先取一个小于n的步长d1,把表中的全部记录分成d1个组,所有的距离为d1的倍数的记录放在同一组中,在各组中进行直接插入排序;然后取第二个步长d2<d1,重复上述过程,直到所取到的dt=1;即所有的记录已放在同一个组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故很快可以得到结果。希尔排序提出的增量序列是:d1=n/2,d(i+1)=di/2(向下取 整),并且最后一个增量为1。
public class ShellSort {
@Test
public void shellSortTest() {
int[] arr = new int[]{1, 10, 15, 2, 3, 8, 6, 7, 9};
shellSort(arr);
}
public void shellSort(int[] arr) {
int length = arr.length;
int k = 0;
for (int i = length/2; i > 0; i=i/2) {
// 对子表进行插入排序
for (int j = i; j < length; j++) {
if(arr[j] < arr[j-i]) {
int temp = arr[j];
for (k = j-i; k>-1 && temp<arr[k]; k-=i) {
arr[k+i] = arr[k];
}
arr[k+i] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
}
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1);
时间效率:由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当n在某个特定范围内,希尔排序的时间复杂度约为O( n^(1.3)),在最坏的情况下时间复杂度是O(n^(2))。
希尔排序是不稳定的排序算法,仅适用于当线性表为顺序存储的情况。