作为一名对技术充满热情的学习者,我一直以来都深刻地体会到知识的广度和深度。在这个不断演变的数字时代,我远非专家,而是一位不断追求进步的旅行者。通过这篇博客,我想分享我在某个领域的学习经验,与大家共同探讨、共同成长。请大家以开放的心态阅读,相信你们也会在这段知识之旅中找到启示。
前言
我们继续学习下一个排序,归并排序,这是一个重要的排序,排序思想也很重要,这种排序也很高效,希望同学们可以认真阅读。
一、什么是归并排序
归并排序(Merge Sort)是一种高效的排序算法,采用分治
的策略来对数组或列表进行排序。基本思想是将原始数组分成若干子数组,对每个子数组进行排序,然后将它们合并成一个全部有序的数组
。
归并排序算法的主要步骤如下:
- 分解:递归地将当前数组分成两个长度差不多的子数组。
- 解决:递归地对子数组进行归并排序,如果子数组长度是 1 或者更小,则不需要继绀做分解,因为长度为 1 的数组自然而然就是有序的。
- 合并:将两个有序的子数组合并成一个有序的数组。
拿一个简单的数组 [38, 27, 43, 3, 9, 82, 10]
举例,归并排序的过程如下:
- 分解数组为
[38, 27, 43, 3]
和[9, 82, 10]
。 - 继续分解
[38, 27, 43, 3]
为[38, 27]
和[43, 3]
,以及将[9, 82, 10]
分解为[9, 82]
和[10]
。 - 进一步分解
[38, 27]
为[38]
和[27]
,[43, 3]
为[43]
和[3]
,以及[9, 82]
为[9]
和[82]
。因为每个数组只有一个元素,开始合并。 - 合并
[38]
和[27]
成[27, 38]
,合并[43]
和[3]
成[3, 43]
,以及[9]
和[82]
成[9, 82]
。 - 将两个有序数组
[27, 38]
和[3, 43]
合并成一个有序数组[3, 27, 38, 43]
,将[9, 82]
和[10]
合并成[9, 10, 82]
。 - 最终,将
[3, 27, 38, 43]
和[9, 10, 82]
合并成最终的有序数组[3, 9, 10, 27, 38, 43, 82]
。
归并排序需要额外的内存空间来存储临时数组,所以它不是一个原地排序算法。不过,归并排序特别适合处理大数据集,并且是稳定的排序算法,即具有相等键值的元素经排序后其相对位置不变。
归并排序算法的时间复杂度为 O(n log n),其中 n 是数组或列表中元素的数量。
这个复杂度来自于两个主要步骤:
- 分解步骤:数组被一分为二,直到每个子数组只有一个元素。分解数组的过程是对数阶的,即每次操作减少一半的问题规模,所以分解步骤大约需要 log n 层递归。
- 合并步骤:在每一层递归中,都要合并子数组。虽然每层的合并操作涉及到整个数组,但由于有 log n 层,合并操作的总时间复杂度是线性对数阶的,即 O(n log n)。
总结起来:归并排序的时间复杂度主要由多次的切分(对数阶次数,即 log n)和在每个切分阶段执行的合并操作(每次操作需要线性时间,即 n)决定,所以总体上是 O(n log n)。这使得归并排序在最坏、平均和最好情况下的时间复杂度都是 O(n logn),这是一种非常稳定的性能表现。
二、练习
简单练习
考虑以下整数数组:
[29, 10, 14, 37, 13]
让我们用归并排序一步步排序这个数组。
Step 1: 分割数组
首先,将整个数组分割成更小的部分,直到每个部分仅包含一个元素:
- 初始数组:
[29, 10, 14, 37, 13]
- 分割为两部分:
[29, 10]
和[14, 37, 13]
- 继续分割:
[29]
,[10]
,[14]
,[37, 13]
- 最后分割
[37, 13]
为:[37]
和[13]
Step 2: 合并与排序
现在开始合并这些单个元素,组成排序好的子数组:
- 合并
[29]
和[10]
得到[10, 29]
- 合并
[37]
和[13]
得到[13, 37]
- 由于
[14]
已经是排序好的,它可以直接与[13, 37]
合并为[13, 14, 37]
现在,我们有 [10, 29]
和 [13, 14, 37]
,要合并它们成为一个排序好的大数组。
Step 3: 最终合并
- 取
[10]
和[13]
中较小的数,所以[10]
是第一个。 [29]
与[13]
比较,选择[13]
为下一个数,接着是[14]
。[29]
下一个与[37]
比较,我们选[29]
。- 最后的数是
[37]
。
合并后的数组是:[10, 13, 14, 29, 37]
较难练习
让我们对以下数组进行排序:
[34, -42, 0, 15, -9, 27, 51, 3, 12, 18]
Step 1: 分割数组
首先,分割数组直到每个子数组都是单个元素:
- 初始数组:
[34, -42, 0, 15, -9, 27, 51, 3, 12, 18]
- 分割数组为两部分:
[34, -42, 0, 15, -9]
和[27, 51, 3, 12, 18]
- 继续分割前半部分:
[34, -42]
和[0, 15, -9]
- 继续分割后半部分:
[27, 51]
和[3, 12, 18]
- 继续分割,直到每个子数组只包含一个元素,我们得到:
[34]
,[-42]
,[0]
,[15]
,[-9]
,[27]
,[51]
,[3]
,[12]
,[18]
Step 2: 合并与排序
然后开始合并子数组,同时也排序这些数组:
- 合并
[34]
和[-42]
得到[-42, 34]
- 合并
[0]
和[15]
得到[0, 15]
[-9]
已经是单个元素,因此它暂时保持不变- 合并
[27]
和[51]
得到[27, 51]
- 合并
[3]
和[12]
得到[3, 12]
[18]
已经是单个元素,因此它暂时保持不变
现在有,[-42, 34]
,[0, 15]
,[-9]
,[27, 51]
,[3, 12]
和 [18]
。
继续合并剩余的数组:
- 合并
[-42, 34]
和[0, 15]
得到[-42, 0, 15, 34]
- 合并
[-9]
和上一步中已经排序的数组[-42, 0, 15, 34]
得到[-42, -9, 0, 15, 34]
- 合并
[27, 51]
和[3, 12]
得到[3, 12, 27, 51]
- 合并
[18]
和上一步中的[3, 12, 27, 51]
得到[3, 12, 18, 27, 51]
现在,我们有 [-42, -9, 0, 15, 34]
和 [3, 12, 18, 27, 51]
。
Step 3: 最终合并
- 比较
[-42, -9, 0, 15, 34]
和[3, 12, 18, 27, 51]
中的第一个元素,取出-42
。 - 比较
-9
和3
,接下来选择-9
。 - 接着是
0
。 - 之后
3
比15
小,所以选择3
。 - 继续这样比较并取出元素直到两个数组都空了。
最终合并排序后的数组是:
[-42, -9, 0, 3, 12, 15, 18, 27, 34, 51]
这样,通过一系列的分割、合并和排序步骤,我们完成了复杂整数数组的归并排序。
三、经典面试题
- 面试题:
假设你有一个整数数组,数组中的一些数可能重复。请实现一个归并排序的版本,该版本除了排序数组外,还能统计数组中每个数字出现的次数。最后输出排序后的数组以及一个包含每个数字及其出现次数的映射(Map)。
这个问题的复杂之处在于,在对数组进行排序的同时,你还需要跟踪每个数字的出现次数。
- 解法:
以下是一个可能的Java解决方案,其中包含了详细的注释来解释各部分代码的功能。
首先,实现一个标准的归并排序过程:
public static void mergeSort(int[] array, int left, int right, Map<Integer, Integer> frequencyMap) { if (left < right) { // 找到中间位置,分割数组为两部分 int mid = left + (right - left) / 2; // 对两个子数组递归调用归并排序 mergeSort(array, left, mid, frequencyMap); mergeSort(array, mid + 1, right, frequencyMap); // 合并两个已排序的子数组 merge(array, left, mid, right, frequencyMap); } } private static void merge(int[] array, int left, int mid, int right, Map<Integer, Integer> frequencyMap) { // 创建临时数组存放左右两边的数组 int[] leftArray = new int[mid - left + 1]; int[] rightArray = new int[right - mid]; // 填充这两个临时数组 for (int i = 0; i < leftArray.length; ++i) leftArray[i] = array[left + i]; for (int j = 0; j < rightArray.length; ++j) rightArray[j] = array[mid + 1 + j]; // 合并临时数组回原数组中 int i = 0, j = 0; int k = left; while (i < leftArray.length && j < rightArray.length) { if (leftArray[i] <= rightArray[j]) { array[k] = leftArray[i]; updateFrequencyMap(frequencyMap, leftArray[i]); i++; } else { array[k] = rightArray[j]; updateFrequencyMap(frequencyMap, rightArray[j]); j++; } k++; } // 处理剩下的元素 while (i < leftArray.length) { array[k] = leftArray[i]; updateFrequencyMap(frequencyMap, leftArray[i]); i++; k++; } while (j < rightArray.length) { array[k] = rightArray[j]; updateFrequencyMap(frequencyMap, rightArray[j]); j++; k++; } } private static void updateFrequencyMap(Map<Integer, Integer> frequencyMap, int key) { frequencyMap.put(key, frequencyMap.getOrDefault(key, 0) + 1); }
在上面的代码中,我们用 mergeSort
方法递归地将数组分成更小的片段,直到片段只包含单一元素。然后,通过 merge
方法将这些片段合并回去,从而得到排序好的数组。我们也增加了 updateFrequencyMap
辅助方法,用于在合并过程中更新数字的出现次数。
这样的实现能够在排序过程中,忽略额外的遍历,即时地构建频率映射。调用 mergeSort
函数时,应传入一个空的 Map
用来存放数字频率,例如:
Map<Integer, Integer> frequencyMap = new HashMap<>(); int[] array = {4, 5, 6, 4, 6, 3}; mergeSort(array, 0, array.length - 1, frequencyMap); System.out.println(Arrays.toString(array)); // 输出排序后的数组 System.out.println(frequencyMap.toString()); // 输出数字及其频率的映射
这个方法在一遍归并排序过程中,就填入了数字频率的信息,既高效又简洁。面试时,这种能力展示出你有能力处理复杂问题,并能优雅地在现有算法框架中添加额外功能。
四、思考
在
updateFrequencyMap
方法中,为什么要使用frequencyMap.getOrDefault(key, 0) + 1
而不是直接使用frequencyMap.get(key) + 1
?
总结
博主持续输出排序算法,更加偏向Java方面,对于面试题的学习,希望同学们有Java基础,才能更好的吸收知识。
感谢大家抽出宝贵的时间来阅读博主的博客,新人博主,感谢大家关注点赞,祝大家未来的学习工作生活一帆风顺,加油!!!