插入排序(直接插入排序、二分插入排序和希尔排序)
插入排序算法思想:每趟将一个元素,按其关键字值的大小插入到它前面已排序的子序列中,依此重复,直到插入全部元素。
插入排序算法有直接插入排序、二分法插入排序和希尔排序。
1、直接插入排序
直接插入排序(Straight Insertion Sort)算法描述如下:
- 1、第i (1<i<n)趟,线性序列为{a0,a1,…,ai-1,ai,…,an-1},设前i个元素构成的子序列{a0,a1,…,ai-1}是排序的,将元素a,插入到子序列{a0,a1,…,ai-1}的适当位置,使插入后的子序列仍然是排序的,ai的插入位置由关键字比较大小确定。
- 2、重复执行第一步,n个元素共需n-1趟,每趟将一个元素ai,插入到它前面的子序列中。关键字序列{32,26,87,72,26,17}的直接插入排序(升序) 过程 如下图所示,以“*” 区别两个关键字相同元素,表示排序子序列。

直接插入排序时间复杂度分析:
-
最好情况,已排序{1,2,3,4,5,6},O(n)
-
最坏情况,反序排列{6,5,4,3,2,1},O(n*n)
-
随机排列,O(n*n)
空间复杂度为:O(1)。
直接插入排序是稳定的。
直接插入排序的算法如下:
package book;
import java.util.Arrays;
/**
* 直接插入排序(升序)
*/
public class directInsertSort {
public static void main(String[] args) {
int[] keys = {32,26,8,26,17,1,90,32};
//insertSort(keys);
//insertSort2(keys);
//insertSort3(keys);
insertSort4(keys);
print(keys);
}
//直接插入排序4(我觉得这个直接插入排序比另外三个要好)
//因为在比较时,它从后面一个一个依次比较,前面的序列是已经排好序的,这一点很关键
public static void insertSort4(int[] keys){
//从第二个元素开始
for (int i = 1; i < keys.length; i++) {
//从下标为i的开始,依次和前面的元素做对比
//注意此处 j > 0, 不能j >= 0,否则会越界
for (int j = i; j > 0; j--) {
if (keys[j - 1] > keys[j]) {
//前面的元素比后面的元素大,交换
swap(keys, j, j-1);
}
}
}
}
static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
static void print(int[] arr){
for (int i : arr) {
System.out.print(i+",");
}
}
//直接插入排序1
public static void insertSort(int[] keys){
for (int i = 0; i < keys.length; i++) {
int temp = keys[i];
int j;
//注意temp = keys[i],这里的条件就是 keys[i-1] >keys[i] , 因为升序,所以交换
for (j = i-1; j >= 0 && keys[j] > temp ; j--) {
//将大的元素往后移一位
keys[j+1] = keys[j];
}
//注意此时j的值(直到找到比带插入小的数据,或者到达数组头部,把数据插入)
keys[j+1] = temp;
System.out.println("第"+i+"趟 temp="+temp+"\t");
}
System.out.println("------------");
for (int key : keys) {
System.out.println(key);
}
}
//直接插入排序2
public static void insertSort2(int[] keys){
//从第二个元素开始
for (int i = 1; i < keys.length; i++) {
//待排序的元素
int temp = keys[i];
int j;
//遍历已排好序的子序列,范围是从0到i-1
for (j = i-1; j >= 0; j--) {
//升序排序,如果前面的元素大于后面的元素,就需要调整元素位置
if (keys[j] > temp) {
//将比较大的元素向后移一位,空出来是待插入的位置。
keys[j + 1] = keys[j];
} else {
break;
}
}
//当跳出子序列的循环时,此时j是要比上一次满足条件的j少1,所以此处是j+1
//直到找到比带插入小的数据,或者到达数组头部,把数据插入
keys[j + 1] = temp;
}
System.out.println("------------------------");
for (int key : keys) {
System.out.println(key);
}
}
//直接插入排序3
public static void insertSort3(int[] keys){
//从第二个元素开始
for (int i = 1; i < keys.length; i++) {
//待排序的元素
int temp = keys[i];
int j = i - 1;
//遍历已排好序的子序列,范围是从0到i-1
while (j >= 0 && keys[j] > temp) {
//升序排序,如果前面的元素大于后面的元素,就需要调整元素位置
//将比较大的元素向后移一位
keys[j + 1] = keys[j];
j--;
}
//当跳出子序列的循环时,此时j是要比上一次满足条件的j少1
//直到找到比带插入小的数据,或者到达数组头部,把数据插入
keys[j + 1] = temp;
}
System.out.println("------------------------");
for (int key : keys) {
System.out.println(key);
}
}
}
2、折半插入排序(二分插入排序)
直接插入排序的每一趟,将一个元素a,插入到它前面的一个排序子序列中,其中采用顺序查找算法寻找a的插入位置。此时,子序列是顺序存储且排序的,这两条正好符合二分法查找要求。
因此,用二分法查找代替直接插入排序中的顺序查找,则构成二分法插入排序。
-
二分插入排序和直接插入排序的时间复杂度一致,也是O(n^2)
-
二分插入排序是稳定的
折半插入排序的算法如下:
package book;
import java.util.Arrays;
/**
* 插入排序(升序)
*
* @auther wlw
* @date 2021/10/25
*/
public class insertSort {
public static void main(String[] args) {
int[] keys = {32,26,8,26,17};
binaryInsertionSort(keys);
}
//二分插入排序
public static void binaryInsertionSort(int[] a) {
int left, right, temp, j;
for (int i = 1; i < a.length; i++) {
temp = a[i];//右手边抓到一张扑克牌
right = i - 1; //数组的右边界
left = 0;//初始化
//采用二分法定位新牌的位置
while (left <= right){
int mid = (left + right) / 2;
if (a[mid] >= temp) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
//找到left就是我要插入的位置,将left及其之后的元素向右移动一位
for (j = i - 1; j >= left; j--) {
a[j + 1] = a[j];
}
//插入 待排序的元素
a[left] = temp;
System.out.println("第"+i+"趟 temp="+temp+"\t");
}
System.out.println("------------");
for (int key : a) {
System.out.println(key);
}
}
}
3、希尔排序(改进的插入排序)
希尔排序,又称缩小增量排序,基本思想就是分组的直接插入排序
由直接插入排序算法分析可知,若数据序列越接近有序,则时间效率越高;再者,当n较小时,时间效率也高。希尔排序正是基于这两点对直接插入排序进行改进的。
希尔排序的算法描述如下:
- ①将一个数据序列分成若干组,每组由若干相隔一段距离 (称为增量) 的元素组成,在一个组内采用直接插入排序算法进行排序。
- ②增量初值通常为数据序列长度的一半, 以后每趟增量减半, 最后值为1。随着增量逐渐减小,组数也减少,组内元素个数增加,数据序列接近有序。
关键字序列{38, 55, 65, 97, 27, 76, 27, 13, 19}的希尔排序(升序)过程如下图所示,序列长度为9,增量delta初值为4,序列分为4组进行直接插入排序,之后每趟增量以减半规律变化,经过3趟完成排序。

希尔排序算法实现如下:
package book;
import java.util.Arrays;
/**
* 插入排序(升序)
*
*/
public class insertSort {
public static void main(String[] args) {
int[] keys2 = {38,55,65,97,27,76,27,13,19};
//shellSort1(keys2);
shellSort2(keys2);
print(keys2);
}
//希尔排序1
public static void shellSort(int[] keys){
//delta为增量,增量初值通常为数据序列长度的一半,之后每趟增量减半,最后值为1
for (int delta = keys.length/2; delta > 0; delta = delta/2 ) {
/**
* 接下来就是进行在增量确定的情况下,进行组内的直接插入排序
*/
//先进行分组后的第一组的排序,因第一组第一个元素下标为0,直接插入排序从第二个元素开始
//并且其他组也是要从第二个元素开始进行直接插入排序,
//所以此处i = delta,从下标为delta的元素开始,并且i++
for (int i = delta; i < keys.length; i++) {
//当前待插入元素
int temp = keys[i];
int j;
for (j = i-delta; j >=0 && keys[j] > temp; j = j-delta) {
//较大元素移到改组内的下一个位置上。
// 这里j + delta,是因为每组元素相距delta远
keys[j + delta] = keys[j];
}
//找到位置,将待插入元素插入到此位置上。
keys[j + delta] = temp;
}
System.out.println("delta: "+delta);
}
for (int key : keys) {
System.out.println(key);
}
}
//希尔排序2
public static void shellSort2(int[] keys){
//delta为增量,增量初值通常为数据序列长度的一半,之后每趟增量减半,最后值为1
for (int delta = keys.length/2; delta > 0; delta = delta/2 ) {
/**
* 接下来就是进行在增量确定的情况下,进行组内的直接插入排序
*/
//先进行分组后的第一组的排序,因第一组第一个元素下标为0,直接插入排序从第二个元素开始
//并且其他组也是要从第二个元素开始进行直接插入排序,
//所以此处i = delta,从下标为delta的元素开始,并且i++
for (int i = delta; i < keys.length; i++) {
//注意j的取值范围,要把每个元素都要比较,又要考虑数据越界
// 所以j > delta-1 ,要把j - delta = 0这个情况也要想到
for (int j = i; j > delta-1 ; j = j-delta) {
if (keys[j - delta] > keys[j]) {
//交换
swap(keys, j, j-delta);
}
}
}
}
}
static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
static void print(int[] arr){
for (int i : arr) {
System.out.print(i+",");
}
}
}
希尔排序算法共有三重循环:
- 最外层循环for语句以增量delta变化控制进行若干趟扫描,delta初值为序列长度n/2,以后每趟减半,直至1
- 中间循环for语句进行一趟扫描,序列分为delta组,每组由相距delta远的n/delta个元素组成,每组元素分别进行直接插入排序。
- 最内层循环for语句进行一组直接插入排序,将一个元素keys[i]插入到其所在组前面的排序子序列中。
希尔排序算法增量的变化规律有多种方案。上述增量减半是一种可行方案。一旦确定增量的变化规律,则一个数据序列的排序趟数就确定了。初始当增量较大时,一个元素与较远的另一个元素进行比较,移动距离较远;当增量逐渐减小时,元素比较和移动距离较近,数据序列则接近有序。最后一次,再与相邻位置元素比较,决定排序的最终位置。下面举个例子(Knuth序列)
//Knuth序列
h = 1;
h = 3*h +1;
//希尔排序2
public static void shellSort2(int[] keys){
int h = 1;
while (h <= keys.length/3) {
h = 3*h +1;
}
for (int delta = h; delta > 0; delta = (delta-1)/3 ) {
/**
* 接下来就是进行在增量确定的情况下,进行组内的直接插入排序
*/
//先进行分组后的第一组的排序,因第一组第一个元素下标为0,直接插入排序从第二个元素开始
//并且其他组也是要从第二个元素开始进行直接插入排序,
//所以此处i = delta,从下标为delta的元素开始,并且i++
for (int i = delta; i < keys.length; i++) {
//注意j的取值范围,要把每个元素都要比较,又要考虑数据越界
// 所以j > delta-1 ,要把j - delta = 0这个情况也要想到
for (int j = i; j > delta-1 ; j = j-delta) {
if (keys[j - delta] > keys[j]) {
//交换
swap(keys, j, j-delta);
}
}
}
}
}
//经比较,采用Knuth序列的效率是要比数组长度一半然后折半的效率是要高的。
3.1、希尔排序算法分析
希尔排序算法的时间复杂度分析比较复杂,实际所需的时间取决于具体的增量序列。希尔排序的平均时间复杂度为n的1.3次方。即O(n^1.3)
希尔排序算法的空间复杂度为O(1)。
希尔排序算法在比较过程中,会错过关键字相等元素的比较,如上图的第1趟,将27*插入到前面的子序列中,则跳过关键字相等元素27,两者没有机会比较,算法不能控制稳定。因此,希尔排序算法不稳定
1万+






