目录
上一篇博客分析了时间复杂度为O(N²)的三种排序方式,并且基于稳定特性的插入排序和冒泡排序做了性能对比,当数据量级比较小的使用经常会选择插入排序。然而插入排序本身还可以进行优化,让其性能进一步提升,二分插入排序和希尔排序都是在普通插入排序的基础上进一步优化。
1、二分插入排序
插入排序本身就是往后遍历,每次轮训都会把前N个元素排好序,然后再将N+1个元素插入已经排好序的“子数组”里面。直接插入排序判断往哪个下标位置插入时,是遍历了整个子数组,在数据结构 - 数组中分析过,查询的最好时间复杂度是O(1),最坏时间复杂度是O(N),平均时间复杂度是O(N)。这里我们忽略了一个重要的特性,或者说是丢弃了前面子数组有序的特性。此时我们可以将此部分的查找过程,从时间复杂度O(N) 优化成二分查询的O(logN)。
public class BinaryInsertSort {
public static void main(String[] args) {
int[] array = new int[]{12, 34, -34, 454, 657, 33, 89, 67, 68, 99, -23, 34};
System.out.println("排序前:" + Arrays.toString(array));
binaryInsertSort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
/**
* 二分插入排序
* @param array 待排序的数组
*/
public static void binaryInsertSort(int[] array) {
if (array.length < 2) {
return;
}
// 记录当轮的值
int value = 0;
int low = 0, high = 0, middle = 0;
for (int i = 1; i < array.length; i++) {
low = 0;
value = array[i];
high = i - 1;
while (low <= high) {
middle = (high + low) >> 1;
if (value < array[middle]) {
// 插入点为低半区,否则插入点在高半区
high = middle - 1;
} else {
low = middle + 1;
}
}
// 已经用临时变量value记录了需要挪动的位置值,此时从后往前将插入点后面所有元素往后挪动一位
for (int j = i - 1; j >= low; j--) {
array[j+ 1] = array[j];
}
//都往后挪动完了,再插入准确位置
array[low] = value;
}
}
}
2、希尔排序
希尔排序针对逆序度比较高的情况,一般的插入排序需要将最小元素移动的下标为0的位置,单个的时间复杂度近似 O(N) 所以希尔排序能快速的将逆序度非常高调整为近似顺序。
比如: [8, 9, 1, 7, 2, 3, 5, 4, 6, 0] 这样的数组需要从小到大排列, 一般的插入排序,时间复杂度就是 O(N)
第一次外层循环:
初始增量 gap = length / 2 = 5 , 那么整个数组将被分到5个组中, (8,3) (9,5) (1,4) (7,6) (2,0) 再进行比较交换
当前状态为: [3, 1, 0, 9, 7, 5, 6, 8, 4, 2]
第二次外层循环:
缩小增量 gap = 5 / 2 = 2, 那么 将分成两个组 (3, 1, 0, 9, 7) (5, 6, 8, 4, 2) 再进行比较交换
当前的状态为: [3, 5, 1, 6, 0, 8, 4, 9, 2, 7]
第三次外层循环:
继续缩小增量: gap = 2 / 2 = 1, 那么整个被分成一个组 (3, 5, 1, 6, 0, 8, 4, 9, 2, 7)
public class ShellSort {
public static void main(String[] args) {
int[] array = new int[]{8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
System.out.println("排序前:" + Arrays.toString(array));
// shellChangeSort(array);
shellSort(array);
// shellMoveSort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
/**
* 希尔排序 针对有序序列在插入时采用交换法
* @param arr 待排序数组
*/
public static void shellChangeSort(int[] arr) {
//增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
// 从第gap个元素,逐个对其所在组进行直接插入排序操作
for (int i = gap; i < arr.length; i++) {
int j = i;
while (j - gap >= 0 && arr[j] < arr[j-gap]) {
// 插入排序采用交换法
swap(arr, j,j - gap);
j -= gap;
}
}
}
}
/**
* 希尔排序 针对有序序列在插入时采用移动法
* @param arr 待排序数组
*/
public static void shellMoveSort(int[] arr) {
// 增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
// 从第gap个元素,逐个对其所在组进行直接插入排序操作
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while(j - gap >= 0 && temp < arr[j - gap]) {
// 移动法
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}
/**
* 交换数组元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr, int a, int b) {
arr[a] = arr[a] + arr[b];
arr[b] = arr[a] - arr[b];
arr[a] = arr[a] - arr[b];
}
/**
* 另一种希尔排序写法
* @param arr 待排序数组
*/
private static void shellSort(int[] arr) {
int len = arr.length;
if (len == 1) {
return;
}
int step = len / 2;
while (step >= 1) {
for (int i = step; i < len; i++) {
int value = arr[i];
int j = i - step;
for (; j >= 0; j -= step) {
if (value < arr[j]) {
arr[j + step] = arr[j];
} else {
break;
}
}
arr[j + step] = value;
}
step = step / 2;
}
}
}