跳到主要内容

08、数据结构与算法 - 实战:排序算法之希尔排序

1、基本思想

回顾插入排序,当待排序的序列逆序程度越高,在从有序部分找位置插入时,后移的次数明显增多,对效率有影响。
因此,希尔排序就是在简单插入排序基础上进行改进,得到更高效的版本,也成为缩小增量排序。对比简单插入排序,希尔排序对较大规模的数据或者逆序程度很高的数据都具有很高的效率。
希尔排序把待排序序列按下标的一定增量进行分组,对每组使用插入排序。随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个序列恰好被分成一组,算法结束。

2、图解

一开始,对待排序序列进行分组,分组数为:length / 2。
假如,数组元素个数为8,则一开始分组数为:8 / 2 = 4.
 
称它为逻辑上分组意思是操作的只是索引,我们不会真的创建四个数组来分别放这四组的元素。比如第一组,我们只需要操作index:0+4×0,0+4×1。操作第二组,就操作idnex:1+4×0,1+4×1。这里的4我们称之为步长,代码用变量gap表示。
 
每个分组进行插入排序后,各个分组就变成了有序的了(整体不一定有序)。
 
此时,整个数组变的部分有序了(有序程度可能不是很高)
 
然后缩小增量为上个增量的一半,即此时分组数为:length / 2 / 2。
如果数组个数为8,第二轮分组个数为:8 / 2 / 2 = 2。继续划分分组,此时,每个分组元素个数多了,但是,数组变的部分有序了,插入排序效率更高。
 
继续对这两组进行插入排序。
第三轮再进行幅度更大的分组:length / 2 / 2 / 2。
如果数组有8个元素,此时分组数就变为1了。
 
对这一个分组还是进行插入排序,此时效率是最高的,因为此时基本有序。

3、代码实现

package cn.klb.datastructures.sort;

/**
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @Description: 实现对数组的希尔排序
 * @Date: Create in 2023/4/4 13:57
 * @Modified By:
 */
public class Shell {
   
     

    // 交换法的希尔排序
    public static void sort(int[] arr) {
   
     
        int temp = 0;
        // 每一轮分组数为上一组分组数除以2取整数
        // gap 表示组的数量
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
   
     
            // 遍历每一个元素
            for (int i = gap; i < arr.length; i++) {
   
     
                // 对arr的第i个元素进行所在组的插入排序
                // 为什么i从gap开始,而不是从0开始呢?
                // 是因为:
                // 对一个数字列表进行插入排序,开始的时候,默认第一个元素就是一个有序列表
                // 第二个元素开始为无序列表,选择无序表的第一个元素,插入到有序表
                // 第gap组的第二个元素就是arr的第gap个元素
                insert(arr, gap, i);
            }
        }
    }

    /**
     * 对arr数组的第index进行所在分组的插入排序
     * 根据插入排序原理,index所在分组的index之前的元素是有序的
     *
     * @param arr
     * @param gap
     * @param index
     */
    private static void insert(int[] arr, int gap, int index) {
   
     
        int value = arr[index];   // 待插入的值
        int selectedIndex = index - gap;    //

        // 遍历有序表,找出可插入的位置
        // value < arr[selectedIndex] 如果比所选位置的值还小,说明这个位置不合适
        while (selectedIndex >= 0 && value < arr[selectedIndex]) {
   
     
            arr[selectedIndex + gap] = arr[selectedIndex];  // 往后挪
            selectedIndex -= gap;
        }

        // 如果selectedIndex挪过,才进行交换
        if (selectedIndex != index - gap) {
   
     
            arr[selectedIndex + gap] = value;
        }
    }
}

4、时间复杂度

代码中,第一次分组数量是元素数量除以2取整数,后面每次分组的数量是上一次分组数量除以2取整。但希尔排序的分组策略不一定是除以2,有可能是除以3、4等。要计算平均时间复杂度是一个长期未得到解决的问题。
所选分组数s满足 1<s<2时,最差情况下的时间复杂度为O(n²)。

5、稳定性

通过原理可知:每一组进行插入排序时,是不会考虑其他组的情况,因此,两个相同数字就完全有可能被破坏掉前后相对关系,因此,希尔排序是不稳定的。

6、测试

package cn.klb.test.datastructurestest;

import cn.klb.datastructures.sort.*;
import org.junit.Test;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

/**
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @Description: 排序测试
 * @Date: Create in 2023/4/3 21:32
 * @Modified By:
 */
public class SotrTest {
   
     
    @Test
    public void testShell() {
   
     
        int[] arr = new int[80000000];
        for (int i = 0; i < 80000000; i++) {
   
     
            arr[i] = (int) (Math.random() * 80000000);
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date1 = new Date();
        System.out.println("-----" + sdf.format(date1) + "-----");

        Shell.sort(arr);

        Date date2 = new Date();
        System.out.println("-----" + sdf.format(date2) + "-----");

        // 8000万的随机数据进行希尔排序,我的电脑花了大约30秒(质的飞跃)
    }
}