希尔排序-交换法和位移法-Java实现

希尔排序实际上可以理解为插入排序的升级版,其中加入了折半的思想

希尔排序思想:

        1.对待排序数组进行增量分组,其中每组的大小可以理解为增量(步长) 分组的规律应该是越分组越大,比如数组的长度是 10,第一次除 2 后是 5,第二次除 2 取整就是 2,第三次就是 1,最后不能小于等于 0 得到的这个数,就是需要分多少组

        2.每次分组后,对每组的数组进行排序(分为交换排序和位移排序(只有位移式才是纯正的插入排序)) 实际上是进行直接插入排序,也就是说对每组的每个数都进行一次插入排序,而不是两两比较。

        3.对于组包含的元素,不应该是邻近的元素,而是 i+gap(步长)为一组的元素 比如第一次分组 10/2=5,每组 2 个元素,i 取 0 的时候,下一个元素就是 0+5,索引为 5 的元素

交换法完整版:

public static void shellSort(int[] arr) {
        int temp = 0;
        // 外层循环表示每次分组
        // 因为插入排序是需要从后往前依次排序,依次这里是i是从gap开始
        // 比如gap = 2的时候,i就是从下标2开始,因为0,1下标相当于是分割出来的两组数的首位,不需要进行比较
        // 因此我们分割完成后的第一次比较都是从当前组的第二位比较,与这组的前一位进行比较,0和2,1和3
        // 当i达到8的时候,那么就是6和8,4和6,2和4,0和2依次比较
        // 之所以i是递增的,因为这样可以依次对每组的每个数进行插入排序的比较
        // 第三个for循环就相当于是拿到了一个待插入值,让它与他这组前面的所有值进行比较,虽然一般的插入排序是从末尾开始拿元素
        // 但是插入排序实际上是给值寻找合适的位置
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                // 然后j就是0,那么就是比较0和5,之所以j -= gap是因为这样可以取到同一组的前一位
                for (int j = i - gap; j >= 0; j -= gap) {
                    // arr[j]相当于是待插入数的前一位,只不过这里是针对分割后的同一组数,arr[j+gap]便是待插入数(仅针对于第一次交换的时候)
                    // 因为第一次说不定没交换,即使没交换,循环依然会继续,这样就导致了前面其实已经排好序了
                    if (arr[j] > arr[j+gap]) {
                        temp = arr[j];
                        arr[j] = arr[j+gap];
                        arr[j+gap] = temp;
                    } else { // 交换式优化,当第一次没有交换的时候,前面也本身是一个有序表,就无序再去对前面的进行判断是否交换了(大幅提高时间效率)
                        break;
                    }
                }
            }
            System.out.println(Arrays.toString(arr));
        }
    }

对我来说,希尔排序不是特别好理解,原因在于被这种位移式的说法误解,以为当我们分割了数组后,对每个数组的元素两两进行比较,互相只比较一次即可,类似于双指针。实际上并不是,当我们拿到一个数组的时候,比如说[1,3,0,9,7],实际上在这里我们使用“直接插入排序”。(补充:对于交换法,虽然大体逻辑上是算插入排序,但是在数据处理的时候,由于使用的交换,其实和冒泡排序交换数据一样

但是需要注意的是,插入排序我们是有一个双表的概念,有序表无序表,一般我们使用的直接插入排序是从后往前,比如说拿到无序表末尾的数,然后和有序表的末尾依次往前开始比较,直到遇到比他小的数,就表示找到了合适的位置进行插入。

而希尔排序中说的”直接插入排序“实际上是先默认当前分割数组的第一位是有序的,然后取得第二位和第一位开始比较。上面那个数组中,有序表是1,那么我们就取得第二位3,然后1和3比较,不需要交换,有序表长度增加。等到下一次取到0的时候,有序表实际上是[1,3]了,这个时候0就是先和3比较,然后交换,数组当前为[1,0,3]然后0和1比较,交换,数组为[0,1,3],后面是数也是如此。因此这个直接插入排序取数的时候,是从无序表的第一位开始取的。一般的插入排序是从无序表的末尾开始取的

还有一点需要注意,在上面这个数组中,我们每次都是拿0和前面的依次比较,会让我们觉得是0依次和前面的比较,实际上不一定。比如数组[1,7],这个时候我们拿9就去比较,当7和9比较后,没发生交换,下一次就是1和7比较了,这是和插入排序有所不同的地方。这也是使用交换式希尔排序的一个问题,因为1和7实际上在之前买的比较中已经比较过了,并且是一个有序表了,这就是重复比较了(已经解决:当取到的需要交换的数据的时候,如果第一次没有发生交换,就不需要进行后面的判断了)。所以,我认为实际上我们是通过移动索引,进行索引之间的比较

对于我的难点:

for (int i = gap; i < arr.length; i++)

上面这个循环,实际上是依次取数组中的数,这里是不区分分割后的哪个数组的,因为会在下面的循环中进行区分。

for (int j = i - gap; j >= 0; j -= gap)
  if (arr[j] > arr[j+gap]) {
      temp = arr[j];
      arr[j] = arr[j+gap];
      arr[j+gap] = temp;
  } else {
     break;
 }

在上面这个循环中,实际上不仅仅是实现了数组索引位移,还实现了区分不同数组之间的数,使得不会发生冲突。当 gap 为2的时候,如果 i 是2,那么 j 就是0,这个0实际上是某个分割后数组的首位,然后通过 arr[j+gap] 就拿到了首位的下一位,让他们之间进行比较。

如果 i 移动到了8呢,那么 j 就是6,这个时候 arr[j+gap] 就是8,也就是 i 的值了(从这里可以看出,i 实际上就是待插入数,不过仅限于第三次循环的首次可以这样理解(当交换时优化完后,i也可以叫做被待插入数)),然后6和8的元素进行比较之后,j -= gap,移动到这个数组的前一位,就是4,此时4和6的元素进行比较,然后依次是2和4,0和2,比较完成,i继续移动

其实难点就在于i是一位一位的移动的,可以每次都是不同的分割后的数组,需要知道如何取到同一数组的前一位。再就是为什么是j -= gap 以及 int j = i - gap是因为只有这样,取到的索引才是依次向前(也就是有序表的末尾到前)开始比较。

// 这样也可以,j>=gap会把边界限制到分割后的数组的第二位,不会超过,
// 也就是说不会到达数组的第一位去,因为如果到第一位了,就会因为j-gap导致越界
for (int j = i; j >= gap; j -= gap) {
 if (arr[j] < arr[j - gap]) {
     temp = arr[j];
     arr[j] = arr[j - gap];
     arr[j - gap] = temp;
 } else {
     break;
 }
}

位移式希尔排序(纯正的插入排序升级版):这个时候,开头说的希尔排序思想就可能存在一点点问题

/**
     * 位移法(纯正的插入排序升级)
     * @param arr 待排序数组
     */
    public static void shellSort2(int[] arr) {
        int insertVal;
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                // 找到最适合的位置后再移动
                // 先保存当前值
                insertVal = arr[i];
                int j = i;
                // j >= gap表示j不能小于当前数组的第一位索引,也可以写作 j-gap >= 0
                // insertVal < arr[j - gap]表示待插入的值依然比前面的值小,还可以继续寻找,知道待插入数比某一个数大
                while (j >= gap && insertVal < arr[j - gap]) {
                    // 后移,腾出空位
                    arr[j] = arr[j - gap];
                    // j继续往同一数组前移动,只有移动gap步才是同一数组的前一位
                    j -= gap;
                }
                // 找到合适的位置了
                arr[j] = insertVal;
            }
        }
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值