插入排序
插入排序的思想很简单,它的基本操作就是将一个记录插入到已经排好序的有序序列中,从而得到一个新的、记录数增1的有序序列。根据查找插入位置的实现思路不同,它又可以分为:直接插入排序、折半插入排序、希尔排序。
直接插入排序
是最基本的插入排序方法,也是一种最简单的排序方法。其基本思想如下:
1、首先把第一个元素单独看做一个有序序列,依次将后面的元素插入到该有序序列中;
2、插入的时候,将该元素逐个与前面有序序列中的元素进行比较,找到合适的插入位置,形成新的有序序列;
3、当有序序列扩大为整个原始序列的大小时,排序结束。
元素比较次数:O(n^2)
元素交换次数:
最好情况:对于一个从小到大的序列,要求以从小到大顺序输出,那么每次插入时只需比较一次,所以总共比较O(n),元素移动次数为0;
最坏情况:对于一个从小到大的序列,要求反序输出,那么需要比较的次数为1,2,3,4......n-1,总的比较次数为n(n-1)/2,元素移动次数与比较次数相等。
空间占用情况:只需利用一个临时变量,故空间占用率为O(1);
实现代码如下:
public class InsertSort1 {
// 直接插入排序实现代码
public static int[] insertSort(int[] array) {
int i, j, len = array.length;
int temp; // 设置临时变量
for (i = 1; i < len; i++) {
temp = array[i];
for (j = i - 1; j >= 0 && array[j] > temp; j--) {
array[j + 1] = array[j];
}
array[j + 1] = temp;
}
return array;
}
// 打印排序结果
public static void print(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
}
public static void main(String[] args) {
int[] array = { 1, 5, 9, 3, 4, 18, 7, 6 };
InsertSort1.print(InsertSort1.insertSort(array));
}
}
这是最常见的实现形式,如果在面试中要手写直接插入排序的话,直接把这种实现代码写出来就可以了。
另外,对于长度为n的待排序列,直接插入排序的平均时间复杂度为O(n^2),而且直接插入排序的比较次数与原始序列的中各元素的位置密切相关,待排序的序列越接近于有序,需要比较的次数就越小,时间复杂度也就越小。
折半插入排序
直接插入排序算法简单,且容易实现,当待排序的长度n很小时,是一种很好的排序方法,尤其当原始序列接近有序时,效率更好。如果待排序的长度n很大,则不适宜采用直接排序。这时我们可以考虑对其做些改进,可以从减少比较和移动的次数入手,因此可以采用折半插入排序,其基本思想类似于折半查找,即:在将一个新元素插入到一个已经排好的序列中时,采用折半的方式找到元素的位置,是对直接插入排序的改进。
元素比较次数:元素比较的次数好于直接插入排序,平均比较次数为n*logn;
元素交换次数:折半排序元素移动的次数与直接插入元素移动的次数相同,均与元素的初始序列有关,
最差情况下,元素需要移动的次数为n*logn,即每次插入在有序序列的开头,所有元素都需要后移一位;
空间占用情况:只占用一个临时变量,故空间占用率为O(1)。
实现代码如下:
public class BinaryInsertSort {
// 折半插入排序实现代码
public static int[] binaryInsertSort(int[] array) {
int i, j, len = array.length;
int temp;
int low, high, middle;
for (i = 1; i < len; i++) {
low = 0;
high = i - 1;
temp = array[i];
while (high >= low) {
middle = (low + high) / 2; // 去中间点
if (temp > array[middle]) {
low = middle + 1; // 向右缩进
} else {
high = middle - 1; // 向左缩进
}
}
for (j = i - 1; j >= low; j--) // 将low与i-1之间的元素右移
{
array[j + 1] = array[j];
}
array[low] = temp; // 插入
}
return array;
}
// 打印排序结果
public static void print(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
}
public static void main(String[] args) {
int[] array = { 1, 5, 9, 3, 4, 18, 7, 6 };
BinaryInsertSort.print(BinaryInsertSort.binaryInsertSort(array));
}
}
从代码中可以看出,折半插入排序所需附加的存储空间与直接插入排序相等,时间上来看,折半插入排序减少了比较的次数,但是元素的移动次数并没有减少。因此,折半插入排序的平均时间复杂度仍为O(n^2)。
希尔排序
希尔排序(shell排序),又称缩小增量排序。前面我们提到,直接插入排序在原始序列越接近有序的情况下,排序效率越高,希尔排序正是利用了直接插入排序的这个优势。希尔排序的基本思想如下:
它将序列按照某个增量间隔分为几个子序列,分别对子序列进行插入排序,而后再取另一增量间隔,对划分的子序列进行插入排序,依次往后……待序列已经大致有序,最后再对整个序列进行插入排序(即增量间隔为1),从而得到有序序列。即:设置间隔变量,将数组进行分组,组内排序,然后缩小间隔变量,直至间隔变量为1;希尔排序的空间占用率为O(1)。
当n较大时,比较和移动的次数约在n^l.25到n^1.6之间。
由于希尔排序要用到插入排序,因此,我们根据前面基本插入排序的实现方法来书写希尔排序的代码。
实现代码如下:
public class ShellSort {
// 希尔排序实现代码
public static int[] shellSort(int[] array) {
int len = array.length;// 获取数组长度
int i, j, gap = len;// gap为初始间隔变量
int temp;
do {
gap = gap / 3 + 1;// 设置每次比较的循环变量
for (i = gap; i < len; i++) {
temp = array[i];
for (j = i - gap; j > 0 && array[j] > temp; j -= gap)// 此处执行的是直接插入排序
{
array[j + gap] = array[j];
}
array[j + gap] = temp;
}
} while (gap > 1);
return array;
}
// 打印排序结果
public static void print(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
}
public static void main(String[] args) {
int[] array = { 1, 5, 9, 3, 4, 18, 7, 6 };
ShellSort.print(ShellSort.shellSort(array));
}
}
PS:希尔排序的性能是完全可以接受的,即使是对数以万计的n来说也是如此。编程的简单特点使得它成为对适度的大量输入数据进行排序时经常选用的算法。需要注意,希尔排序是一种不稳定的内部排序算法。
比较:希尔排序的时间性能优于直接插入排序
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和n^2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n^2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插入排序有较大的改进。