一、算法介绍
希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。希尔排序又称缩小增量排序,因 DL.Shell 于 1959 年提出而得名。它通过比较相距一定间隔的元素来进行,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。希尔排序不是稳定性排序算法。
二、适用场景说明
希尔排序(Shell Sort)适用于以下场景:
-
中到大型数据集:
希尔排序在处理中等或大型数据集时相比简单的插入排序有显著的性能提升,因为其减少了元素间的比较和移动次数。 -
数据初始顺序:
如果数据已经部分有序,希尔排序的效率会更高,因为它利用了这一特性来减少排序所需的时间。 -
时间复杂度要求:
虽然希尔排序的平均和最坏情况时间复杂度不是线性的,但在实践中,对于某些增量序列,它可以在接近线性的时间内完成排序,这对于对时间复杂度有中等要求的场合是有益的。 -
内存限制:
希尔排序是原地排序算法,不需要额外的存储空间,这在内存有限或者管理内存成本较高的情况下是有优势的。 -
对稳定性不敏感的应用:
由于希尔排序是非稳定的排序算法,即相等的元素可能会改变相对顺序,所以在那些需要保持相等元素原有顺序的场景下不适用。 -
简单实现:
希尔排序的实现相对简单,尽管选择合适的增量序列可以提高性能,但即使使用默认的简单序列,希尔排序也能提供优于直接插入排序的性能。 -
需要快速响应的初步排序:
在需要快速得到大致排序结果,但不强求完全排序的场合,希尔排序可以作为一个有效的预处理步骤。
需要注意的是,希尔排序在处理非常大的数据集时,可能不如其他高级排序算法(如快速排序、归并排序或堆排序)表现得好。在现代编程实践中,通常会选择这些具有更好平均和最坏情况时间复杂度保证的算法。然而,希尔排序仍然因其灵活性和在某些特定情况下的高效性而有价值。
三、画图演示
希尔排序目的为了加快速度改进了插入排序,交换不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
在此我们选择增量 gap=length/2,缩小增量以 gap = gap/2 的方式,用序列 {n/2,(n/2)/2…1} 来表示。
- 初始增量第一趟 gap = length/2 = 4
- 第二趟,增量缩小为 2
- 第三趟,增量缩小为 1,得到最终排序结果
当增量gap按照减半的速度递减为1时,排序结束,得到正确的排序结果。
四、java代码
package com.datastructures;
import java.util.Arrays;
/**
* 希尔排序算法示例
* @author hulei
* @date 2024/5/13 14:25
*/
public class ShellSort {
public static void main(String[] args) {
Integer[] arr = new Integer[]{7,6,9,3,1,5,2,4};
System.out.println("排序前:"+Arrays.toString(arr));
shellSort(arr);
System.out.println("排序后:"+Arrays.toString(arr));
}
/**
* 实现Shell排序算法的静态方法。
* <p>
* 该方法接受一个泛型数组作为输入,该数组的元素必须实现Comparable接口,以便进行排序比较。
* Shell排序是一种改进的插入排序算法,通过逐渐增加元素间的比较距离来减少排序过程中的交换次数。
*
* @param array 要排序的泛型数组,数组元素必须实现Comparable接口。
* @param <E> 数组元素的类型,该类型需extends Comparable,以确保元素可比较。
*/
public static <E extends Comparable<? super E>> void shellSort(E[] array) {
int len = array.length;
// 通过逐渐减小的间隔对数组进行多趟排序
for (int gap = len / 2; gap > 0; gap /= 2) {
// 对每个间隔的元素进行插入排序
for(int i = gap; i < len; i++){
// 将当前元素与前面的已排序元素比较并交换位置,直到找到合适的位置
int j = i-gap;
while(j>=0){
if(array[j].compareTo(array[j + gap]) > 0){
swap(array,j+gap,j);
}
j -= gap;
}
}
}
}
/**
* 交换数组中两个元素的位置。
*
* @param array 要进行交换的数组。
* @param index1 要交换的第一个元素的索引。
* @param index2 要交换的第二个元素的索引。
* @param <E> 数组元素的类型。
*/
private static <E> void swap(E[] array, int index1, int index2) {
// 临时变量用于存储第一个元素,以便后续交换
E temp = array[index1];
array[index1] = array[index2]; // 将第二个元素的值赋给第一个元素
array[index2] = temp; // 将之前存储的第一个元素的值赋给第二个元素
}
}
核心方法如下:
public static <E extends Comparable<? super E>> void shellSort(E[] array) {
int len = array.length;
// 通过逐渐减小的间隔对数组进行多趟排序
for (int gap = len / 2; gap > 0; gap /= 2) {
// 对每个间隔的元素进行插入排序
for(int i = gap; i < len; i++){
// 将当前元素与前面的已排序元素比较并交换位置,直到找到合适的位置
int j = i-gap;
while(j>=0){
if(array[j].compareTo(array[j + gap]) > 0){
swap(array,j+gap,j);
}
j -= gap;
}
}
}
}
代码逻辑比较简单了,具体结合上面的图片分析即可
第一层for循环是增量gap除以2逐层递减,初始gap为4,后续是2,1,当gap递减为1时内部循环结束后,停止排序
因为1/2 = 0 ,不满足下一次循环gap>0的条件
中间的for循环是不同的增量间隔,所有元素照间隔当前增量比较
中间一层即使比较增量间隔的两个元素之间比较决定是否并交换位置,大的元素移动到后面,不清楚的话可以在while内部交换后打印出数组内容