学点算法(三)——数组归并排序

今天来学习归并排序算法。

归并算法的核心思想是分而治之,就是将大问题转化为小问题,在解决小问题的基础上,再去解决大问题。将这句话套用到排序中,就是将一个大的待排序区间分为小的待排序区间,对小的排序区间进行排序,然后再去解决大区间的排序,而对小区间进行排序的时候可以继续使用该方法,将小的待排序区间分为小小的待排序区间… …依次往复。最终将排序区间分到只有一个元素,这个时候,因为一个元素已经就是排好序的,无需继续切分了,然后我们再依照此结果去解决大区间的排序。

假设我们现在有[53, 89, 32, 45, 67, 2, 32, 89, 65, 54]这么一个数组,我们要对它进行归并排序(从小到大排序),整体的过程如下图所示:
归并排序算法完整过程
整个算法分为两大阶段,分割阶段归并阶段

分割阶段
  1. [53, 89, 32, 45, 67, 2, 32, 89, 65, 54]分为[53, 89, 32, 45, 67][2, 32, 89, 65, 54]
  2. [53, 89, 32, 45, 67]分为[53, 89][32, 45, 67][2, 32, 89, 65, 54]分为[2, 32][89, 65, 54]
  3. … …
  4. 数组分割完毕,所有小数组依次为[53][89][32][45][67][2][32][89][65][54]
归并阶段
  1. [53][89]归并为[53, 89][32][45]归并为[32, 45][2][32]归并为[2, 32][65][54]归并为[54, 65](这一步中,[67][89]没有归并,因为在最后一步分割过程中,它们被单独分开了)。
  2. [32, 45][67]归并为[32, 45, 67][89][54, 65]归并为[54, 65, 89]
  3. [53, 89][32, 45, 67]归并为[32, 45, 53, 67, 89][2, 32][54, 65, 89]归并为[2, 32, 54, 65, 89]
  4. [32, 45, 53, 67, 89][2, 32, 54, 65, 89]归并为[2, 32, 32, 45, 53, 54, 65, 67, 89, 89]其中两个32和两个89,在归并的过程中保留它们的原始顺序)。

整个分而治之的过程我们已经清楚了,可还有一个问题没有解决,就是具体应该怎么去归并呢,即如何将两个排序子数组(或子区间)合并为大的排序好的数组(或区间)呢?

我们可以先举个简单的例子:现在有[2][1]两个数组,我们如何把它们合并为[1, 2]整个数组呢?很简单,我们首先会把这两个元素取出来,对比一下,取出21,我们一对比,发现1小于2, 所以我们在结果数组中先放入1,然后再放入2。可以发现,我们就是将两个子数组中的元素取出来比较了一下,哪个小就把哪个先放入结果数组中。

从上面的例子中我们可以得到大概的思路就是,针对两个有序的子数组(或子区间),我们可以从头依次取两个子数组(或子区间)的首元素(因为从小到大排序后首元素肯定最小),然后作对比,把小的元素放入结果数组中,并且这个元素在下次选取的时候剔除,下一个元素也应用同样的方法得到,放入结果数组中,依次进行,直到两个数组的元素都取完为止,如果发现其中一个子数组(或子区间)率先取完,就直接将另外一个子数组(或子区间)中剩下的元素全部放入结果数组中即可。具体步骤描述如下:

  1. 判断两个子数组(或子区间)是否含有剩余元素,如果都有剩余元素,进入第2步;如果只有一个有剩余元素,进入第5步;如果没有,则退出。
  2. 取出左子数组(或左子区间)的首个元素和右子数组(或右子区间)的首个元素。
  3. 两个元素对比,将小的元素放入结果数组,并且从对应数组中剔除该元素。
  4. 回到第1步(上一步选中的元素已被剔除)。
  5. 将剩余元素直接全部放入结果数组中,退出(因为元素全部移动完毕)。

其中,剔除这一步在代码实现中可看成索引的移动。

上述这个过程我们取[53, 89][32, 45, 67]这两个子数组的合并来描述一下,如图所示:
归并

  1. 取出左子数组中的首个元素53和右子数组中的首个元素32,两个作对比,发现32 < 53,所以我们将32放入结果数组:
    在这里插入图片描述
  2. 取出左子数组中的首个元素53和右子数组中的首个元素45,两个作对比,发现45 < 53,所以我们将45放入结果数组:
    在这里插入图片描述
  3. 取出左子数组中的首个元素53和右子数组中的首个元素67,两个作对比,发现53 < 67,所以我们将53放入结果数组:
    在这里插入图片描述
  4. 取出左子数组中的首个元素89和右子数组中的首个元素67,两个作对比,发现67 < 89,所以我们将67放入结果数组:
    在这里插入图片描述
  5. 此时我们发现只有左子数组存在元素,所以直接将左子数组的剩下所有元素,此时只有89放入结果数组:
    在这里插入图片描述
  6. 至此,所有元素移动完毕,退出。

通过以上分析,我们可以知道整个归并排序算法总体上分为一个整体的大逻辑(分而治之)和一个局部的小逻辑(归并),在大逻辑(分而治之,将整个数组切分,并在确认子数组有序后归并)的基础上,结合使用小逻辑(归并,将两个有序子数组归并为一个大的有序数组)即可实现对整个数组的排序。

最终代码实现如下:

/**
 * 数组的归并排序算法
 *
 * @param nums 数组
 * @param lo 区间的lo索引(包含)
 * @param hi 区间的hi索引(不包含)
 */
public static void mergeSort(int[] nums, int lo, int hi) {
    // 数组为null则直接返回
    if (nums == null) {
        return;
    }
    // 索引检查
    if (lo < 0 || nums.length <= lo) {
        throw new IllegalArgumentException("lo索引必须大于0并且小于数组长度,数组长度:" + nums.length);
    }
    if (hi < 0 || nums.length < hi) {
        throw new IllegalArgumentException("hi索引必须大于0并且小于等于数组长度,数组长度:" + nums.length);
    }
    if (hi <= lo) {
        // lo索引必须小于hi索引(等于也不行,因为区间是左闭右开,如果等于,区间内元素数量就为0了)
        throw new IllegalArgumentException("lo索引必须小于hi索引");
    }
    if (lo + 1 >= hi) {
        // 区间元素个数最多为1
        // 无需排序
        return;
    }
    int mid = (lo + hi) / 2;
    // 对左子区间排序
    mergeSort(nums, lo, mid);
    // 对右子区间排序
    mergeSort(nums, mid, hi);
    // 对两个排序好的子区间归并,得到一个整体有序的区间
    merge(nums, lo, mid, hi);
}

public static void merge(int[] nums, int lo, int mid, int hi) {
    // 这里不用检查索引,调用方已经决定了索引是有效的
    // 结果区间和右子区间使用原有数组
    // 左子区间使用临时数组(因为结果区间可能会覆盖左子区间的元素,所以需要开辟新数组保存)
    int leftLen = mid - lo;
    int[] left = new int[leftLen];
    System.arraycopy(nums, lo, left, 0, leftLen);
    // 左子区间索引
    int leftIdx = 0;
    // 右子区间索引
    int rightIdx = mid;
    // 结果区间索引
    int resultIdx = lo;
    while (true) {
        if (leftIdx < leftLen && rightIdx < hi) {
            // 两个子区间都存在元素
            // 取两个子区间的有效首元素对比
            if (left[leftIdx] <= nums[rightIdx]) {
                // 左子区间首元素小于右子区间首元素
                // 将左子区间首元素放到结果位置,同时更新索引位置
                nums[resultIdx++] = left[leftIdx++];
            } else {
                // 右子区间首元素小于左子区间首元素
                // 将右子区间首元素放到结果位置,同时更新索引位置
                nums[resultIdx++] = nums[rightIdx++];
            }
        } else {
            if (leftIdx < leftLen) {
                // 左子区间还有剩余元素
                // 直接将左区间所有元素一起移动到结果位置
                System.arraycopy(left, leftIdx, nums, resultIdx, leftLen - leftIdx);
            } else {
                // 右子区间还有剩余元素
                // 因为经过上一次判断,左子区间和右子区间只会有一个存在剩余元素
                // 直接将右区间所有元素一起移动到结果位置
                System.arraycopy(nums, rightIdx, nums, resultIdx, hi - rightIdx);
            }
            // 全部元素移动完毕,退出
            break;
        }
    }
}

测试代码如下:

List<Integer> numList = IntStream.range(0, 10).boxed().collect(Collectors.toList());
for (int i = 1; i <= 5; i++) {
    System.out.println("================第" + i + "次================");
    Collections.shuffle(numList);
    int[] nums = new int[numList.size()];
    for (int j = 0; j < nums.length; j++) {
        nums[j] = numList.get(j);
    }
    System.out.println("排序前:" + Arrays.toString(nums));
    mergeSort(nums, 0, numList.size());
    System.out.println("排序后:" + Arrays.toString(nums));
}

运行结果如下:

================1================
排序前:[8, 4, 1, 6, 7, 0, 5, 9, 2, 3]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
================2================
排序前:[2, 5, 6, 7, 9, 4, 3, 1, 0, 8]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
================3================
排序前:[2, 0, 5, 6, 7, 3, 4, 9, 8, 1]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
================4================
排序前:[4, 0, 3, 8, 1, 5, 9, 7, 2, 6]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
================5================
排序前:[7, 9, 8, 2, 0, 5, 6, 3, 4, 1]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

测试代码中5次随机将数组打乱,然后运行我们的归并排序算法,均得到有序结果,符合我们的预期。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值