插入排序->{直接插入排序,二分插入排序,希尔插入排序}
直接插入排序
原理:设有一组关键字{K1, K2,…, Kn};排序开始就认为 K1 是一个有序序列;让 K2 插入上述表长为 1 的有序序列,使之成为一个表长为 2 的有序序列;然后让 K3 插入上述表长为 2 的有序序列,使之成为一个表长为 3 的有序序列;依次类推,最后让 Kn 插入上述表长为 n-1 的有序序列,得一个表长为 n 的有序序列。
具体算法描述:
- 从第一个元素开始,该元素可以认为已经被排序
- 取下第一个元素, 在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2-5
现有一组数组 arr = [5, 6, 3, 1, 8, 7, 2, 4],共有八个记录,排序过程如下:
[5] 6 3 1 8 7 2 4
↑ │
└───┘
[5, 6] 3 1 8 7 2 4
↑ │
└────────┘
[3, 5, 6] 1 8 7 2 4
↑ │
└──────────┘
[1, 3, 5, 6] 8 7 2 4
↑ │
└──┘
[1, 3, 5, 6, 8] 7 2 4
↑ │
└────┘
[1, 3, 5, 6, 7, 8] 2 4
↑ │
└────────────────┘
[1, 2, 3, 5, 6, 7, 8] 4
↑ │
└─────────────┘
[1, 2, 3, 4, 5, 6, 7, 8]
public static void insertSort(int[] a) {
for (int i = 1; i < a.length; i++) //n-1此扫描,依次向前插入n-1个元素
{
int temp = a[i]; //每趟将a[i]插入到前面的排序子序列中
int j;
for (j = i - 1; j >= 0 && temp < a[j]; j--) {
a[j + 1] = a[j]; //将前面较大的元素向后移动
}
a[j + 1] = temp; //temp值到达插入位置
System.out.println(Arrays.toString(a));
}
}
此处内层循环是是a[j+1]=a[j]而不是a[i]=a[j]的原因:在内层循环的第一次是俩者相等,之后j+1将不等于i。所以只能是前者。
二分(折半)插入排序
在直接插入排序的基础上,如果数据量比较大,为了减少关键码的比较次数,可以使用折半插入来寻找要插入的位置。
折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快,它所需要的关键码比较次数与待排序记录的初始排列无关,仅依赖与记录个数,比较次数约等于nlogn次。
当n较大时,总关键码比较次数比直接插入比较次数的最坏情况(n平方)/4要好很多,但比其最好情况2(n-1)要差,所以在记录的初始排列已经接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少
public static void insertSort2(int[] a) {
for (int i = 1; i < a.length; i++) {
int left = 0;
int right = i - 1;
int temp = a[i];
while (left <= right) { // 利用折半查找插入位置
int mid = (left + right) / 2; // 取中点
if (a[mid] > temp) // 插入值小于中点值
right = mid - 1; // 向左缩小区间
else
left = mid + 1; // 向右缩小区间
}
// left即为找到的要插入的位置,所以下边的循环将left-(i-1)位置的元素依次向后移动
for (int j = i - 1; j >= left; j--) {
a[j + 1] = a[j];
}
a[left] = temp; // 将temp插入到left位置
System.out.println(Arrays.toString(a));
}
}
希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是一种非稳定性排序
可以使得性能提升至O(n log2 n)。这比最好的比较算法的O(n log n)要差一些。
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。
这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序。
希尔排序又称为缩小增量排序,基本思想是分组的直接插入排序。
由直接插入排序算法分析可知,若数据序列接近有序,则时间效率越高;当n较小时,时间效率也很高。希尔排序基于这两点对直接插入排序进行了改进。
- 采用间隔4创建四个位置所有值的虚拟列表{35,14},{33,19},{42,27},{10,44}
- 比较每个子列表中的值,如需要则在原始数组中交换
- 采用间隔1,产生两个子列表,{14,27,35,42},{19,10,33,44}
- 比较子列表中的值,如有需要则在原始数组中交换
- 最后使用间隔1对数组其余部分进行排序
JAVA代码
public static void shellSort(int[] arr) {
int i, j, gap;
// 获得步长
for (gap = arr.length / 2; gap > 0; gap /= 2) {
// 进行插入排序
for (i = 0; i < gap; i++) {
// j每个子列表元素下表
for (j = i + gap; j < arr.length; j += gap) {
if (arr[j] < arr[j - gap]) {
int tmp = arr[j];
int k = j - gap;
while (k >= 0 && arr[k] > tmp) {
arr[k + gap] = arr[k];
k -= gap;
}
arr[k + gap] = tmp;
System.out.println(Arrays.toString(arr));
}
}
}
}
}
由上可知,希尔排序共有三重循环:
(1)最外层循环for语句以增量d变化控制进行若干趟扫描,d的初值为序列长度的一半,以后每趟减半,直至为1;
(2)中间for循环进行一趟扫描,序列分为d组,每组由相距为d远的n/d个元素组成,每组元素分别进行直接插入排序;
(3)最内层循环for语句进行一组直接插入排序,将一个a[i]插入到其所在组前面的排序子序列中。
希尔排序与直接插入排序的区别其实就是希尔排序多了一层循环用来控制扫描躺数,并且在每次的直接插入排序中,间隔为d而不是间隔为1,所以将i=1,j=i-1等改为了i=d,j=i-d等。