插入排序算法---插入排序与希尔排序
本文主要说明插入排序、shell排序两种排序方法。
一、插入排序
算法思想:
假定这个数组的序是排好的,然后从头往后,如果有数比当前外层元素的值大,则将这个数的位置往后挪,直到当前外层元素的值大于或等于它前面的位置为止.这具算法在排完前k个数之后,可以保证a[1…k]是局部有序的,保证了插入过程的正确性.
插入排序过程示例:
下面是对无序表[12,15,9,20,6,31,24]的排序过程:
伪代码:
INSERTION-SORT(A)
1 for j ← 2 to length[A]
2 do key ← A[j]
3 ▹ Insert A[j] into the sorted sequence A[1 ‥ j - 1].
4 i ← j - 1
5 while i > 0 and A[i] > key
6 do A[i + 1] ← A[i]
7 i ← i - 1
8 A[i + 1] ← key
代码实现:
static int count = 0;
/***
* 把n个待排序的元素看成一个有序表和一个无序表, 开始有序表只包含一个元素,无序表中包含n-1个元素,
* 排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较, 将它插入到有序表中的适当位置,使之成为新的有序表。
*/
public static void runInsertSort(int[] a) {
for (int i = 1; i < a.length; i++) {
int insertVal = a[i];
// insertValue准备和前一个数比较
int index = i - 1;
while (index >= 0 && insertVal < a[index]) {
// 将把a[index]向后移动
a[index + 1] = a[index];
// 让index向前移动一位
index--;
}
// 将insertValue插入到适当位置
a[index + 1] = insertVal;
System.out.println("indexPos>>>"+(index+1));
System.out.print("第" + (i) + "次排序结果:");
for (int k = 0; k < a.length; k++) {
System.out.print(a[k] + "\t");
}
System.out.println("");
count++;
}
System.out.print("最终排序结果:");
for (int l = 0; l < a.length; l++) {
System.out.print(a[l] + "\t");
}
}
public static void main(String[] args) {
int[] array = new int[6];
for (int k = 0; k < array.length; k++) {
array[k] = (int) (Math.random() * 100);
}
System.out.print("排序之前结果为:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + "\t");
}
System.out.println("");
runInsertSort(array);
System.out.println("交换次数:"+count);
}
打印结果如下:
排序之前结果为:66 12 90 75 43 85
indexPos>>>0
第1次排序结果:12 66 90 75 43 85
indexPos>>>2
第2次排序结果:12 66 90 75 43 85
indexPos>>>2
第3次排序结果:12 66 75 90 43 85
indexPos>>>1
第4次排序结果:12 43 66 75 90 85
indexPos>>>4
第5次排序结果:12 43 66 75 85 90
最终排序结果:12 43 66 75 85 90 排序次数:5
二分插入排序
插入排序中,总是先寻找插入位置,然后在实行挪动和插入过程;寻找插入位置采用顺序查找的方式(从前向后或者从后向前),既然需要插入的数组已经是有序的,那么可以采用二分查找方法来寻找插入位置,改善了找到插入点的速度,减少了比较的次数.但依然无法改变移动次数。
算法代码实现:
public class SortSolution {
static int count = 0;
/***
* 向有序序列中插入元素,那么插入位置可以不断地平分有序序列, 并把待插入的元素的关键字与平分有序序列的关键字比较,以确定下一步要平分的子序列,
* 直到找到合适的插入位置位置。
*/
public static void runBinaryInsertSort(int[] a) {
for (int i = 1; i < a.length; i++) {
int insertVal = a[i];// 待插入的值
int low = 0;
int high = i - 1;
while (low <= high) {
// 找出low,high的中间索引
int mid = (low + high) / 2;
// 如果要插入的值大于mid的值
if (insertVal > a[mid]) {
low = mid + 1;
}// 限制在索引大于mid的那一半中搜索
else {
high = mid - 1;
}// 限制在索引小于mid的那一半中搜索
}
// 将low到i处的所有元素向后整体移动一位
for (int j = i; j > low; j--) {
a[j] = a[j - 1];
}
// 将insertValue插入到适当位置
a[low] = insertVal;
System.out.println("indexPos>>>"+(low));
//System.out.print("\n");
System.out.print("第" + (i) + "次排序结果:");
for (int k = 0; k < a.length; k++) {
System.out.print(a[k] + "\t");
}
System.out.println("");
count++;
}
}
public static void main(String[] args) {
int[] array = new int[1500];
for (int k = 0; k < array.length; k++) {
array[k] = (int) (Math.random() * 100);
}
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date begintime = new Date();
System.out.println("开始时间: " + df.format(begintime));
System.gc();
System.out.print("排序之前结果为:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + "\t");
}
System.out.println("");
runBinaryInsertSort(array);
System.out.println("交换次数:"+count);
Date endtime = new Date();
System.out.println("结束时间:" + df.format(endtime));
Date time = new Date(endtime.getTime() - begintime.getTime());
System.out.println("总用时间:" + time.getMinutes() + "分"
+ time.getSeconds() + "秒");
}
}
算法分析:
1)时间复杂度:
折半插入排序比直接插入排序明显减少了关键字之间的比较次数,但是移动次数是没有改变。所以,折半插入排序和插入排序的时间复杂度相同都是O(N^2),在减少了比较次数方面它确实相当优秀,所以该算法仍然比直接插入排序好。
2)空间复杂度:
折半插入排序和插入排序一样只需要一个多余的缓存数据单元来放第 i 个元素,所以空间复杂度是O(1),因为排序前2个相等的数在序列的前后位置顺序和排序后它们两个的前后位置顺序相同,所以它是一个稳定排序。
二、希尔排序
希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
希尔排序实质上是一种分组插入方法。
基本思想:
对于n个待排序的数列,取一个小于n的整数gap(gap被称为步长)将待排序元素分成若干个组子序列,所有距离为gap的倍数的记录放在同一个组中;
然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。
然后减小gap的值,并重复执行上述的分组和排序。重复这样的操作,当gap=1时,即所有记录放在同一组中进行直接插入排序为止, 整个数列就是有序的。
下面以数列{80,30,60,40,20,10,50,70}为例,演示它的希尔排序过程。:
第1趟:(gap=4)
当gap=4时,意味着将数列分为4个组: {80,20},{30,10},{60,50},{40,70}。 对应数列: {80,30,60,40,20,10,50,70}
对这4个组分别进行排序,排序结果: {20,80},{10,30},{50,60},{40,70}。 对应数列: {20,10,50,40,80,30,60,70}
第2趟:(gap=2)
当gap=2时,意味着将数列分为2个组:{20,50,80,60}, {10,40,30,70}。 对应数列: {20,10,50,40,80,30,60,70}
注意:{20,50,80,60}实际上有两个有序的数列{20,80}和{50,60}组成。
{10,40,30,70}实际上有两个有序的数列{10,30}和{40,70}组成。
对这2个组分别进行排序,排序结果:{20,50,60,80}, {10,30,40,70}。 对应数列: {20,10,50,30,60,40,80,70}
第3趟:(gap=1)
当gap=1时,意味着将数列分为1个组:{20,10,50,30,60,40,80,70}
注意:{20,10,50,30,60,40,80,70}实际上有两个有序的数列{20,50,60,80}和{10,30,40,70}组成。
对这1个组分别进行排序,排序结果:{10,20,30,40,50,60,70,80}
增量序列:
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长串行都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
Donald Shell 最初建议步长选择为并且对步长取半直到步长达到 1。虽然这样取可以比类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。 可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。
已知的最好步长串行是由Sedgewick提出的 (1, 5, 19, 41, 109,...),该串行的项来自 9 * 4^i - 9 * 2^i + 1 和 4^i - 3 * 2^i + 1 这两个算式.这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长串行的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。
另一个在大数组中表现优异的步长串行是(斐波那契数列除去0和1将剩余的数以黄金分区比的两倍的幂进行运算得到的数列):(1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713, …)
我们在这里选取gap = (gap - 1)/3做增量序列
Shell排序的关键是确定gap序列的值。常用的h序列由Knuth提出,该序列从1开始,通过如下公式产生:
gap = 3 * gap +1
上面公式用于从1开始计算这个序列,可以看到序列为1, 4, 13 ,40.... 反过来,程序中还要反向计算gap序列,则gap为:
gap = (gap - 1)/3
上面公式从最大的gap开始计算,假设gap从40开始,则序列为40 , 13 , 4 , 1.
伪代码:
input: an array a of length n with array elements numbered 0 to n − 1
inc ← round(n/2)
while inc > 0 do:
for i = inc .. n − 1 do:
temp ← a[i]
j ← i
while j ≥ inc and a[j − inc] > temp do:
a[j] ← a[j − inc]
j ← j − inc
a[j] ← temp
inc ← round(inc / 2)
算法实现:
public static void shellSort(int[] data) {
System.out.println("开始排序:");
int arrayLength = data.length;
// h变量保存可变增量
int gap = 1;
// 按h * 3 + 1得到增量序列的最大值
while (gap <= arrayLength / 3) {
gap = gap * 3 + 1;
}
while (gap > 0) {
System.out.println("===gap的值:" + gap + "===");
for (int i = gap; i < arrayLength; i++) {
// 当整体后移时,保证data[i]的值不会丢失
int tmp = data[i];
// i索引处的值已经比前面所有值都大,表明已经有序,无需插入
// (i-1索引之前的数据已经有序的,i-1索引处元素的值就是最大值)
if (data[i] < data[i - gap]) {
int j = i - gap;
// 整体后移h格
for (; j >= 0 && data[j] > tmp; j -= gap) {
data[j + gap] = data[j];
}
// 最后将tmp的值插入合适位置
data[j + gap] = tmp;
}
System.out.println(java.util.Arrays.toString(data));
}
gap = (gap - 1) / 3;
}
}
public static void main(String[] args) {
int[] array = new int[10];
for (int k = 0; k < array.length; k++) {
array[k] = (int) (Math.random() * 100);
}
System.out.print("排序之前结果为:");
System.out.println(java.util.Arrays.toString(array));
System.out.println("");
shellSort(array);
}
算法执行结果:
排序之前结果为:[3, 27, 41, 16, 43, 86, 84, 46, 88, 3]
开始排序:
===gap的值:4===
[3, 27, 41, 16, 43, 86, 84, 46, 88, 3]
[3, 27, 41, 16, 43, 86, 84, 46, 88, 3]
[3, 27, 41, 16, 43, 86, 84, 46, 88, 3]
[3, 27, 41, 16, 43, 86, 84, 46, 88, 3]
[3, 27, 41, 16, 43, 86, 84, 46, 88, 3]
[3, 3, 41, 16, 43, 27, 84, 46, 88, 86]
===gap的值:1===
[3, 3, 41, 16, 43, 27, 84, 46, 88, 86]
[3, 3, 41, 16, 43, 27, 84, 46, 88, 86]
[3, 3, 16, 41, 43, 27, 84, 46, 88, 86]
[3, 3, 16, 41, 43, 27, 84, 46, 88, 86]
[3, 3, 16, 27, 41, 43, 84, 46, 88, 86]
[3, 3, 16, 27, 41, 43, 84, 46, 88, 86]
[3, 3, 16, 27, 41, 43, 46, 84, 88, 86]
[3, 3, 16, 27, 41, 43, 46, 84, 88, 86]
[3, 3, 16, 27, 41, 43, 46, 84, 86, 88]
希尔排序的时间复杂度和稳定性
希尔排序时间复杂度
希尔排序的时间复杂度与增量(即,步长gap)的选取有关。例如,当增量为1时,希尔排序退化成了直接插入排序,此时的时间复杂度为O(N²),而Hibbard增量的希尔排序的时间复杂度为O(N3/2)。
希尔排序稳定性
希尔排序是不稳定的算法,它满足稳定算法的定义。对于相同的两个数,可能由于分在不同的组中而导致它们的顺序发生变化。
算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
参考: